때가 왔다. 가능하면 피하고 싶던, 그러면서 잘 하고는 싶던 CI/CD를 해야 할 때가 되었다. 그냥... 커비처럼 잘 하는 사람 삼켜서 능력 흡수만 하고 싶었어요.
암튼 지금은 어찌어찌 돌아는 가게 해놨으나, 완벽히 맘에 들지는 않아도 돌아는 가니껜... 나의 지금까지의 개고생.. 아니 노력을 남겨두려고 한다. 권수갱 의식의 흐름 엿보기 시작 😉
그러지 말았어야 했음.
GAENCHWIS
├── client
├── server
│ ├── crawler
│ ├── essay_service
│ ├── job_posting
│ ├── scheduling
│ └── user_service
└── chrome_extension
우리 서비스의 디렉토리 구조도는 이러하다.
마이크로서비스 아키텍처를 기반으로 한 웹 애플리케이션 개발을 진행 중이고, 폴더 별 역할은 아래와 같다.
client
: 프론트엔드 애플리케이션server
: 백엔드 서비스들의 집합crawler
: 데이터 수집 서비스essay_service
: 에세이 관련 서비스job_posting
: 채용 공고 관련 서비스shceduling
: 스케줄링 작업 관리 서비스user_service
: 사용자 관리 서비스chrome_extension
: 크롬 확장 프로그램각 서비스 별로 독립적인 Dockerfile이 있고, 이를 AWS ECS(Elastic Container Service)를 사용하여 컨테이너화 된 마이크로 서비스로 배포하는 것이 CI/CD 단계의 최종 목표였다.
💡 AWS CloudFormation을 이용해 CI/CD 파이프라인을 infrastructure as code로 구현해보자!
라는 이런 그릇된 생각을 품어버린 것이어요...
이유는 있었다.
CloudFormation으로 메인 인프라 스택을 구축해둔 윤재가 너무 간지나보였다. 그거 보면서 많이 배웠거든... 템플릿이 있으면 캡쳐 없어도 다른 팀원들에게 코드 보고 공부하라고 알려줄 수 도 있고, 그래도 개발자 출신이라고... AWS 콘솔이 무섭고 어려운 나는 '이게 코드로 다 된다고...?' 여기에 꽂혀서 너무 신기하고... 재밌다는 생각이 들었다. 템플릿 개꿀인디? 하나 성공하면 나머지도 슉슉 되는 거 아냐... 그래서 우리 조 기술팀장인 갓윤재의 뒤를 이어 나도 CloudFormation으로 CI/CD를 진행하고 싶었다.
AWS UI가 별로다. (당당) 그리고 어렵다! ㅡㅡ 교육 과정 중 실습 하면서 뭣도 모르고 댐비니까 계속 에러가 났었고, 모두 고통스러웠던 기억도 있고, 어려워서 건들기가 무서웠다. 사실 어렵고 무서우면 내가 더 열심히 공부해서 안 무섭게 하면 되는데... 걍 좀 외면하고 싶었음. 코드가 더 익숙하고 편하지 않나? 생각하며...
하지만 개발 공부하면서 매번 느끼는건데, 어렵고 힘들다고 외면해봤자 결국 언젠간 다시 마주해야 한다. 알면서도 외면하게 되네요
암튼 그렇게 잘못된 선택을 하였고, 하루 간 절망했음... 하지만 그래도 이거 헤맨 덕에 많이 배우긴 했다.
메인 인프라 스택과 서비스 스택 분리
메인 인프라 스택에는
등의 내용이 들어간다.
배포 순서
1. 메인 스택 배포 (✔)
1. 각 서비스 별 ECR 리포지토리 생성
2. ECR 리포지토리는 각 서비스마다 별도로 필요함 (독립적으로 관리)
3. 각 서비스별buildspec.yml
이 해당 서비스의 ECR 리포지토리를 참조
2. 서비스 별 스택 배포
이렇게 생각만큼 잘 되었다면... 참 좋았겠는데 말입니다. 그래도 이거 해보겠다고 열심히 공부하긴 했다.
GitHub (Source) -> CodeBuild (Build) -> ECS (Deploy)
buildspec.yml
에 정의된 대로 빌드 실행 이런 식으로 흘러갈 것이고, 코드야 GitHub에 올라가있으니 먼저 ECR 리포지토리를 만드는 것이 순서였다.
GAENCHWIS
└── server
└── user_service
├── Dockerfile
├── infrastructure/
│ └── ecr.yaml # 여기에 ECR 템플릿 생성
├── app/
└── ...
그 와중에 ECR은 CloudFormation으로 스택 잘 만들기 성공함 ^^
AWSTemplateFormatVersion: '2010-09-09'
Description: 'ECR Repository for User Service'
Resources:
UserServiceRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: gaenchwis/user-service
ImageScanningConfiguration:
ScanOnPush: true
ImageTagMutability: MUTABLE
EncryptionConfiguration:
EncryptionType: AES256
LifecyclePolicy:
LifecyclePolicyText: |
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 5 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 5
},
"action": {
"type": "expire"
}
}
]
}
Outputs:
RepositoryUri:
Description: URI for User Service Repository
Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${UserServiceRepository}
Export:
Name: !Sub ${AWS::StackName}-UserServiceRepositoryUri
RepositoryName:
Description: Name of User Service Repository
Value: !Ref UserServiceRepository
Export:
Name: !Sub ${AWS::StackName}-UserServiceRepositoryName
이렇게 템플릿을 만들었기 때문에~ AWS 콘솔에서 올려도 되고,
# infrastructure 폴더로 이동
cd server/user_service/infrastructure
# CloudFormation 스택 생성
aws cloudformation create-stack \
--stack-name gaenchwis-user-service-ecr \
--template-body file://ecr.yaml
명령어로 해도 된다.
GitHub 소스에 접근하기 위해서는 연결이 필요하다. 이건.. 혼자 뻘짓 하다가 은권님이 알려주셔서 잘 함... 사실 여기부터 내 머리가 엉켜버렸어~...
암튼 이건 그냥 AWS console에서 CodeBuild 서비스에서 설정 > 연결 > 연결 들어가서 '연결' 생성 버튼 누르고 Provider만 GitHub로 설정해서 하고, 레포지토리 선택 후 권한 부여만 하면 된다.
buildspec.yml
파일 작성version: 0.2
phases:
pre_build:
commands:
# ECR 로그인
- echo Logging in to Amazon ECR...
- aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com
# 환경변수 설정
- REPOSITORY_URI=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/gaenchwis/user-service
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- IMAGE_TAG=${COMMIT_HASH:=latest}
build:
commands:
# Docker 이미지 빌드
- echo Building the Docker image...
- cd server/user_service
- docker build -t $REPOSITORY_URI:$IMAGE_TAG -f server/user_service/Dockerfile .
- docker tag $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:latest
post_build:
commands:
# ECR에 이미지 푸시
- echo Pushing the Docker images...
- docker push $REPOSITORY_URI:$IMAGE_TAG
- docker push $REPOSITORY_URI:latest
# ECS 배포를 위한 imagedefinitions.json 생성
- echo Writing image definitions file...
- printf '{"ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDefinitions.json
artifacts:
files:
- imageDefinitions.json
- server/user_service/infrastructure/*
- server/user_service/appspec.yaml
discard-paths: no
당시엔 이렇게 작성했다.
pre_build, build, post_build 단계로 나뉘어있고, artiacts 들에 대해 정의하는 부분도 있다. 사실 이 때는 정말 잘 몰라서 ai에 의존하면서 했었기 때문에 지금 보니 실제 내 프로젝트와 맞지 않는 설정도 있다. ㅎ... 괜찮아~ 어차피 지금은 성공했어~
AWSTemplateFormatVersion: '2010-09-09'
Description: 'CodeBuild Project for User Service with GitHub Organization'
Parameters:
GitHubOrg:
Type: String
Description: GitHub organization name
GitHubRepo:
Type: String
Description: GitHub repository name
GitHubBranch:
Type: String
Description: GitHub branch name
Default: main
GitHubConnectionArn:
Type: String
Description: ARN of the GitHub connection created in AWS Console
Resources:
CodeBuildServiceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess
Policies:
- PolicyName: CodeBuildServiceRolePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Resource: '*'
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- Effect: Allow
Resource:
- !Sub arn:aws:s3:::${ArtifactBucket}/*
Action:
- s3:GetObject
- s3:PutObject
- s3:GetObjectVersion
- Effect: Allow
Resource: '*'
Action:
- codestar-connections:UseConnection
ArtifactBucket:
Type: AWS::S3::Bucket
Properties:
VersioningConfiguration:
Status: Enabled
LifecycleConfiguration:
Rules:
- Id: DeleteOldArtifacts
Status: Enabled
ExpirationInDays: 30
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Sub ${AWS::StackName}-build
ServiceRole: !GetAtt CodeBuildServiceRole.Arn
Artifacts:
Type: S3
Location: !Ref ArtifactBucket
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:4.0
PrivilegedMode: true
EnvironmentVariables:
- Name: AWS_DEFAULT_REGION
Value: !Ref AWS::Region
- Name: AWS_ACCOUNT_ID
Value: !Ref AWS::AccountId
Source:
Type: GITHUB
Location: !Sub https://github.com/${GitHubOrg}/${GitHubRepo}.git
BuildSpec: server/user_service/buildspec.yml
GitCloneDepth: 1
GitSubmodulesConfig:
FetchSubmodules: false
Auth:
Type: OAUTH
Resource: !Ref GitHubConnectionArn
ReportBuildStatus: true
BuildStatusConfig:
Context: 'CodeBuild'
TargetUrl: !Sub https://${AWS::Region}.console.aws.amazon.com/codebuild/home?region=${AWS::Region}#/projects/${AWS::StackName}-build/view/new
Cache:
Type: LOCAL
Modes:
- LOCAL_DOCKER_LAYER_CACHE
LogsConfig:
CloudWatchLogs:
Status: ENABLED
GroupName: !Sub /aws/codebuild/${AWS::StackName}
StreamName: build-log
Outputs:
CodeBuildProjectName:
Description: Name of the CodeBuild project
Value: !Ref CodeBuildProject
Export:
Name: !Sub ${AWS::StackName}-CodeBuildProjectName
CodeBuildProjectArn:
Description: ARN of the CodeBuild project
Value: !GetAtt CodeBuildProject.Arn
Export:
Name: !Sub ${AWS::StackName}-CodeBuildProjectArn
ArtifactBucketName:
Description: Name of the S3 bucket for build artifacts
Value: !Ref ArtifactBucket
Export:
Name: !Sub ${AWS::StackName}-ArtifactBucketName
Build Stage: CodeBuild를 통한 도커 이미지 빌드 및 ECR 푸시
우선 CI 단계에 해당하는 CodeBuild 테스트를 위한 코드이다.
aws cloudformation create-stack \
--stack-name gaenchwis-user-service-codebuild \
--template-body file://codebuild.yaml \
--capabilities CAPABILITY_IAM \
--parameters \
ParameterKey=GitHubOrg,ParameterValue=Nimbus-2025 \
ParameterKey=GitHubRepo,ParameterValue=gaenchwis \
ParameterKey=GitHubBranch,ParameterValue=dev \
ParameterKey=GitHubConnectionArn,ParameterValue=arn:aws:codeconnections:ap-northeast-2:463470980614:connection/3eead416-ec3d-4410-9bae-75a5be32161f
배포 시에는 이런 명령어를 썼고...
실패할 경우에는
# 이벤트 보기
aws cloudformation describe-stack-events --stack-name gaenchwis-user-service-codebuild
# 실패한 이유만 보기
aws cloudformation describe-stack-events \
--stack-name gaenchwis-user-service-codebuild \
--query 'StackEvents[?ResourceStatus==`CREATE_FAILED`].[LogicalResourceId,ResourceStatusReason]'
연결이 작동하는 방식
Source: Type: GITHUB
)Source: Type: CODESTAR_CONNECTION
)실패함. 안됨. 빌드가 안되어서 수정을 하면 결국에는 Source에서 문제가 생겨서 안되었다. CodeStar 방식이 있다고는 하는데 공식 문서의 Source: Type:
에는 CODESTAR_CONNECTION
이 사용 예시에 없었다. (당시 내가 봤을 땐 그랬음...)
당시에는 심신미약이기도 했고, 뭐가 어떻게 돌아가는지 시행착오 끝에 파악해가는 단계였기 때문에 좀 어려웠다. 지금은 그래도 CI/CD를 해결해서 대충은 좀 알겠다만... 나중에 CloudFormation으로 다시 시도해서 성공해보고 싶은 생각은 있다. 하지만 생각만큼 개발이 안끝나지네요...? ^^ 살려줘 곧 백수라 여유가 많기 때문에 그 시기에 한 번 도전해볼까 한다. 강사님께서도 뭐하러 계속 어렵게 이걸로 붙잡고 하냐고 쿠사리 자꾸 잔소리하셔서 (시간 없는데 계속 실패하면? 방향 트는게 맞음. 강사님 말이 맞음. 하지만 난 이렇게 해결하고 싶었는데 안됨. 강사님 말이 맞말이라 분함. 속상했음...)
커서야 너 유료야 정신차려
진짜 미친놈인줄 알았다
뭐, 이렇게 반성한다 한들... 이미 너무 AI가 너무나도 깊이 내 삶에 파고들어왔기 때문에 할 말이 없긴 하다. 이제는 구글 검색보다 AI 검색을 더 많이 하고 있으니... 그래도 그게 잘 통해왔었는데, Cloud 관련 CI/CD 설정을 할 때는 오히려 독이었다. 클라우드 쪽은 AI를 신봉해선 절대 안되는 것 같다. 하지만 어렵다, 잘 모른다, 시간이 없다는 핑계로 자꾸 AI에게 의존하는 선택을 하는 바람에 오히려 더 돌아갔다. 시간 더 낭비했쥬? (물론, 그 과정에서 배운게 많으니 완전히 시간 낭비라고 할 순 없지만 허탈하고 힘들긴 했다. 새벽 5시까지 AI한테 쌍욕 박아가면서 안되는거 붙잡고 있었는데... 솔직히 내가 너무 쉽게 가려다가 사서 고생한거긴 함. 미안해 커서야...)
정말 1년 전에 직장 다닐 때만 해도 공식문서 며칠이고 파고들어서 설정이나 옵션 파악하고 내 스스로 했는데, 이제는 AI를 안 쓸 수는 없지만 참... 양날의 검이라는 생각이 들었다. 내 스스로 사고하고, 문서를 읽고 지식을 습득하는 과정이 많이 축소되고 생략된 것 같았다. 하지만 결국엔 공부도 사람이 하는 거고, AI가 그 과정을 굉장히 효율적이게 도움을 주는 건 맞지만 다시 한 번 맹신하지 말고 의존하지 말자는 큰 교훈을 얻었다. 윤재의 도움을 받아 공식 문서 어디를 읽어야 하는지 도움도 받고, 읽어가다보니 내가 뭘 모르고, 뭘 잘못하고 있었는지 대충은 알 것 같았다.
암튼 그렇게 CloudFormation과 새벽 늦게까지 처 싸우면서... 소득이 있지만 없다고 볼 수 있는 이틀을 보냈다. 큰 교훈을 얻고... 다음엔 다른 트러블 슈팅 글을 써야겠다.
본 포스팅은 글로벌소프트웨어캠퍼스와 교보DTS가 함께 진행하는 챌린지입니다.