[프로젝트] CI/CD 실패기... (feat. CloudFormation)

sookyoung.k·2025년 1월 29일
0

🌿 교보DTS TIL

목록 보기
41/42
post-thumbnail

때가 왔다. 가능하면 피하고 싶던, 그러면서 잘 하고는 싶던 CI/CD를 해야 할 때가 되었다. 그냥... 커비처럼 잘 하는 사람 삼켜서 능력 흡수만 하고 싶었어요.

암튼 지금은 어찌어찌 돌아는 가게 해놨으나, 완벽히 맘에 들지는 않아도 돌아는 가니껜... 나의 지금까지의 개고생.. 아니 노력을 남겨두려고 한다. 권수갱 의식의 흐름 엿보기 시작 😉


🌧️ CloudFormation에 용감히 도전

그러지 말았어야 했음.

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로 구현해보자!

라는 이런 그릇된 생각을 품어버린 것이어요...

이유는 있었다.

  1. 간지.

CloudFormation으로 메인 인프라 스택을 구축해둔 윤재가 너무 간지나보였다. 그거 보면서 많이 배웠거든... 템플릿이 있으면 캡쳐 없어도 다른 팀원들에게 코드 보고 공부하라고 알려줄 수 도 있고, 그래도 개발자 출신이라고... AWS 콘솔이 무섭고 어려운 나는 '이게 코드로 다 된다고...?' 여기에 꽂혀서 너무 신기하고... 재밌다는 생각이 들었다. 템플릿 개꿀인디? 하나 성공하면 나머지도 슉슉 되는 거 아냐... 그래서 우리 조 기술팀장인 갓윤재의 뒤를 이어 나도 CloudFormation으로 CI/CD를 진행하고 싶었다.

  1. 무지.

AWS UI가 별로다. (당당) 그리고 어렵다! ㅡㅡ 교육 과정 중 실습 하면서 뭣도 모르고 댐비니까 계속 에러가 났었고, 모두 고통스러웠던 기억도 있고, 어려워서 건들기가 무서웠다. 사실 어렵고 무서우면 내가 더 열심히 공부해서 안 무섭게 하면 되는데... 걍 좀 외면하고 싶었음. 코드가 더 익숙하고 편하지 않나? 생각하며...

하지만 개발 공부하면서 매번 느끼는건데, 어렵고 힘들다고 외면해봤자 결국 언젠간 다시 마주해야 한다. 알면서도 외면하게 되네요

암튼 그렇게 잘못된 선택을 하였고, 하루 간 절망했음... 하지만 그래도 이거 헤맨 덕에 많이 배우긴 했다.

➡️ 배포 순서

메인 인프라 스택과 서비스 스택 분리

메인 인프라 스택에는

  • VPC, 서브넷, 보안그룹
  • ECS 클러스터
  • ALB, 타겟그룹
  • IAM 역할

등의 내용이 들어간다.

배포 순서
1. 메인 스택 배포 (✔)
1. 각 서비스 별 ECR 리포지토리 생성
2. ECR 리포지토리는 각 서비스마다 별도로 필요함 (독립적으로 관리)
3. 각 서비스별 buildspec.yml 이 해당 서비스의 ECR 리포지토리를 참조
2. 서비스 별 스택 배포

이렇게 생각만큼 잘 되었다면... 참 좋았겠는데 말입니다. 그래도 이거 해보겠다고 열심히 공부하긴 했다.

🪈 CodePipeline 기본 흐름

GitHub (Source) -> CodeBuild (Build) -> ECS (Deploy)
  1. Source 단계
    1. GitHub 저장소에 코드가 push되면 웹훅 트리거
    2. CodePipeline이 소스 코드를 가져옴
  2. Build 단계
    1. buildspec.yml에 정의된 대로 빌드 실행
  3. Deploy 단계
    1. CodePipeline이 새로운 이미지 URI를 받아서
    2. ECS 서비스의 task definition을 업데이트
    3. ECS 서비스에 새 버전 배포 시작

이런 식으로 흘러갈 것이고, 코드야 GitHub에 올라가있으니 먼저 ECR 리포지토리를 만드는 것이 순서였다.

📭 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

명령어로 해도 된다.

📬 ECR 레포지토리에 이미지 빌드 및 푸시

GitHub 연결

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에 의존하면서 했었기 때문에 지금 보니 실제 내 프로젝트와 맞지 않는 설정도 있다. ㅎ... 괜찮아~ 어차피 지금은 성공했어~

CodeBuild 프로젝트를 생성하기 위한 CloudFormation 템프릿

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]'

🌦️ 수많은 시행착오 끝에 배운 내용들

💡 GitHub App과 AWS CodeStar Connection의 역할과 차이점

  • GitHub App: GitHub 측에서 외부 서비스의 권한 관리
    • 외부 서비스(AWS)가 GitHub API를 호출할 때 필요한 인증과 권한 관리
    • 특정 리포지토리나 조직에 대한 접근 범위 정의
    • 웹훅, API 호출 등의 권한을 세부적으로 제어
  • AWS CodeStar Connection (AWS Developer Tools에서 설정): AWS 서비스가 GitHub 리포지토리에 접근하기 위한 연결 관리
    • AWS CodeBuild가 소스코드를 가져오고 웹훅을 설정할 때 사용
    • GitHub App의 권한을 실제로 사용하여 GitHub와 통신

연결이 작동하는 방식

  • GitHub에 커밋이 push되면
  • GitHub App이 설정된 웹훅을 통해 AWS에 알림
  • AWS CodeBuild는 CodeStar Connection을 사용하여
  • GitHub App의 권한으로 리포지토리에 접근하여 코드를 가져옴

💡 AWS와 GitHub 통합하는 두 가지 방식

  1. Legacy GitHub 통합 방식 (Source: Type: GITHUB)
    1. 이전 방식, OAUTH 토큰 사용
    2. 개인 GitHub 계정에 더 적합
    3. AWS CodeBuild 콘솔에서 GitHub 계정을 직접 연결해야 한다
  2. 새로운 CodeStar Connections 방식 (Source: Type: CODESTAR_CONNECTION)
    1. 현대적인 방식, AWS가 권장함
    2. GitHub 조직 계정에 더 적합
    3. 더 강력한 보안과 권한 관리
    4. GitHub Apps를 사용하여 연결

🌧️ 결론

실패함. 안됨. 빌드가 안되어서 수정을 하면 결국에는 Source에서 문제가 생겨서 안되었다. CodeStar 방식이 있다고는 하는데 공식 문서의 Source: Type: 에는 CODESTAR_CONNECTION이 사용 예시에 없었다. (당시 내가 봤을 땐 그랬음...)

당시에는 심신미약이기도 했고, 뭐가 어떻게 돌아가는지 시행착오 끝에 파악해가는 단계였기 때문에 좀 어려웠다. 지금은 그래도 CI/CD를 해결해서 대충은 좀 알겠다만... 나중에 CloudFormation으로 다시 시도해서 성공해보고 싶은 생각은 있다. 하지만 생각만큼 개발이 안끝나지네요...? ^^ 살려줘 곧 백수라 여유가 많기 때문에 그 시기에 한 번 도전해볼까 한다. 강사님께서도 뭐하러 계속 어렵게 이걸로 붙잡고 하냐고 쿠사리 자꾸 잔소리하셔서 (시간 없는데 계속 실패하면? 방향 트는게 맞음. 강사님 말이 맞음. 하지만 난 이렇게 해결하고 싶었는데 안됨. 강사님 말이 맞말이라 분함. 속상했음...)


느낀점

AI에게 너무 의존하는 스스로에 대한 반성

커서야 너 유료야 정신차려 진짜 미친놈인줄 알았다

뭐, 이렇게 반성한다 한들... 이미 너무 AI가 너무나도 깊이 내 삶에 파고들어왔기 때문에 할 말이 없긴 하다. 이제는 구글 검색보다 AI 검색을 더 많이 하고 있으니... 그래도 그게 잘 통해왔었는데, Cloud 관련 CI/CD 설정을 할 때는 오히려 독이었다. 클라우드 쪽은 AI를 신봉해선 절대 안되는 것 같다. 하지만 어렵다, 잘 모른다, 시간이 없다는 핑계로 자꾸 AI에게 의존하는 선택을 하는 바람에 오히려 더 돌아갔다. 시간 더 낭비했쥬? (물론, 그 과정에서 배운게 많으니 완전히 시간 낭비라고 할 순 없지만 허탈하고 힘들긴 했다. 새벽 5시까지 AI한테 쌍욕 박아가면서 안되는거 붙잡고 있었는데... 솔직히 내가 너무 쉽게 가려다가 사서 고생한거긴 함. 미안해 커서야...)

정말 1년 전에 직장 다닐 때만 해도 공식문서 며칠이고 파고들어서 설정이나 옵션 파악하고 내 스스로 했는데, 이제는 AI를 안 쓸 수는 없지만 참... 양날의 검이라는 생각이 들었다. 내 스스로 사고하고, 문서를 읽고 지식을 습득하는 과정이 많이 축소되고 생략된 것 같았다. 하지만 결국엔 공부도 사람이 하는 거고, AI가 그 과정을 굉장히 효율적이게 도움을 주는 건 맞지만 다시 한 번 맹신하지 말고 의존하지 말자는 큰 교훈을 얻었다. 윤재의 도움을 받아 공식 문서 어디를 읽어야 하는지 도움도 받고, 읽어가다보니 내가 뭘 모르고, 뭘 잘못하고 있었는지 대충은 알 것 같았다.

암튼 그렇게 CloudFormation과 새벽 늦게까지 처 싸우면서... 소득이 있지만 없다고 볼 수 있는 이틀을 보냈다. 큰 교훈을 얻고... 다음엔 다른 트러블 슈팅 글을 써야겠다.


본 포스팅은 글로벌소프트웨어캠퍼스와 교보DTS가 함께 진행하는 챌린지입니다.

profile
영차영차 😎

0개의 댓글

관련 채용 정보