개발일지

컨테이너 기반 CI/CD 구축해보기 - AWS ECR, ECS, Github Actions 본문

Infra, AWS, Linux

컨테이너 기반 CI/CD 구축해보기 - AWS ECR, ECS, Github Actions

lyjin 2024. 7. 11.

개요

이전 Lightsail과 Github Actions을 사용해 CICD를 구축했을 때 node 버전 이슈로 계속 실패했던 경험이 있다. 그 외에도 하나하나 수동으로 설치해줘야하고.. 스케일링 하기도 번거롭고.. 어렵고.. 이러한 단점을 보완하기위해 “컨테이너 (Container)” 기술을 사용한다고 한다. 이번 시간에는 AWS ECS를 사용해 컨테이너 기반의 배포 환경을 구축해보고자 한다.
 

컨테이너 오케스트레이션 (Container Orchestration)

Container Orchestration이란 다수의 컨테이너를 관리하고 배포하는 자동화된 시스템을 말한다. 이를 위한 도구로 Kubernetes, Docker Swarm, AWS ECS 등이 있으며 컨테이너의 배포, 스케일링, 로깅 등을 효율적으로 관리할 수 있도록 도와준다. 그중 ECS는 AWS에서 지원하는 서비스인 만큼, AWS ELB, RDS 등과 함께 사용할 경우 더욱 시너지가 난다.
 

 


AWS ECR - Docker 이미지 private하게 배포하기

AWS ECR(Elastic Container Registry)은 AWS에서 제공하는 완전관리형 컨테이너 레지스트리 서비스이다. 빌드한 Docker 이미지를 ECR에 push 할 수 있으며, 이후 ECS는 이 ECR에 있는 이미지를 사용해 컨테이너를 실행하고 배포할 것이다.
 
 

1) ECR에 private repository 생성하기

 
당연히 상용화 서비스는 외부에 노출되면 안되므로 private으로 선택해주자.
 

이제 생성된 repo의 url로 접근 가능하다. 

 

해당 repo을 클릭하면 “푸시 명령”이라고 있을 것이다.

 
 
위의 단계에 따라 빌드한 도커 이미지를 ECR로 push 할 수 있다.
이때 AWS CLI로 ECR push 할 수 있는 권한이 있어야 한다. (아래에서 설명)

// aws 설치 후 로그인
$ aws configure

// docker image build, ecr repo push
$ aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 1234567890.dkr.ecr.ap-northeast-2.amazonaws.com
$ docker build -t my-app .
$ docker tag my-app:latest 1234567890.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:latest
$ docker push 1234567890.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:latest

 
 
다시 repo에 들어가보니 push 한 이미지가 추가되어있다.

 
 


2) IAM User에 “ECR private repository Push 권한” 부여하기

권한이 워낙 많아 뭘 추가해야하는건지 당황스러울 뻔 했지만 다행히 공식문서에 잘 설명되어있었다. - 🔗 이미지를 Amazon ECR 사설 리포지토리로 푸시하기 위한 IAM 권한

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:CompleteLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:InitiateLayerUpload",
                "ecr:BatchCheckLayerAvailability",
                "ecr:PutImage"
            ],
            // my-app repo에만 접근할 수 있도록 지정
            "Resource": "arn:aws:ecr:ap-northeast-2:1234567890:repository/my-app"
        },
        {
            "Effect": "Allow",
            "Action": "ecr:GetAuthorizationToken",
            "Resource": "*"
        }
    ]
}

 
해당 정책을 사용할 IAM user에 추가하면 끝이다.
 
 


AWS ECS 구성하기

1) 클러스터 생성하기

 
클러스터 생성하기를 누른다.
 
 

 
Fargate 또는 EC2 두 가지 옵션이 있는데 Fargate를 사용할 것이다.

  • Fargate
    • 서버리스 → AWS가 인프라를 자동 관리 하므로 간편함
    • 사용자는 태스크 단위로만 신경 쓰면 됨
  • EC2
    • 가상 서버 내부에서 컨테이너를 실행함
    • 커스터마이징 가능 → 더많은 제어와 유연성을 제공하지만, 그만큼 사전 지식 필요하고 복잡함

 


2) 태스크 정의하기 (task definition)

왼쪽 메뉴 탭을 보면 “task-definition”라는 메뉴가 있을 것이다. Task definition은 생성할 태스크의 청사진과 같다(JSON 형식 파일). 사용할 Docker 이미지, CPU 및 메모리 할당, 환경 변수 등을 정의하며 이를 바탕으로 인스턴스가 생성되고 실행된다.
 
 
먼저 인프라 설정 값이다.

 
 
그리고 태스크에서 돌아갈 컨테이너를 설정해줘야한다.여러 컨테이너를 지정해줄 수는 있지만 보통 한 태스크 당 하나의 컨테이너만 지정해준다고 한다.

 
이미지 url은 아까 ECR에 배포 했던 도커 이미지 url를 입력하면 된다.
 
 

 
환경변수도 설정 가능하다.
 
 
생성된 태스크 정의에 들어가보면 다음과 같이 JSON 파일이 생성된 것을 볼 수 있다.

 
 


3) 서비스 생성하기

태스크가 ECS의 최소 실행단위라면 Service는 이러한 태스크들을 실행하고 관리하는 단위이다. 태스크가 중단 됐을 때 자동으로 새로운 태스크로 대체하고(self-healing), 자동 스케일링, 태스크 간의 로드 밸런싱 등을 제공한다.
 
 
클러스터에 들어가면 “서비스” 탭이 있다. 생성 버튼을 클릭한다.

 
패밀리는 사용할 태스크 정의를 선택하면 되고 원하는 태스크(desired tasks)에 생성할 태스크 수를 입력해준다.
 
 
로드밸런서도 바로 생성해 줄 수 있다.

 

생성한 후 “생성된 서비스 > 태스크”를 보면 아까 설정한대로 3개의 태스크가 실행 중이다.

 
 


4) 로드밸런서 보안그룹 수정하기

서비스를 보면 다음과 같이 로드밸런서(LB)가 등록된 것을 볼 수 있다.

 
 
그러나 아직 LB의 DNS로 접속 하더라도 정상적으로 연결되지 않을 것이다. 이는 LB를 연결할 때 기본으로 지정되는 보안 그룹이 외부 트래픽을 전부 차단하고 있기 때문이다. 따라서 외부 트래픽을 받을 수 있는 보안 그룹을 생성하여 LB에 추가해줘야한다.

 
 
이제 DNS:80로 접속하면 로드밸런서를 거쳐 ECS 태스크에 도달할 것이다.

 
 


Github Actions으로 CICD 적용하기

지금까지 서버를 컨테이너 기반으로 배포하기 위한 과정, “1. 프로젝트를 도커 이미지로 빌드하고, 2. 빌드한 이미지를 ECR로 push 하고, 3. ECS로 배포”를 수동으로 진행해봤다. 이제 이 과정을 Github Actions을 이용해 자동화할 것이다. 다행히 Github Actions 공식문서에서 상세하게 설명해주고 있기 때문에 절차대로 따라하기만 하면 된다.
 
 

1) OIDC로 AWS CLI 로그인 하기

🔗 Configuring OpenID Connect in Amazon Web Services
 
AWS 리소스에 접근하기 위해서는 접근 권한을 부여받아야 한다. OIDC(OpenID Connect)는 OAuth 2.0 프로토콜을 기반으로 하는 인증 프로토콜로 임시 인증 토큰을 사용한다. 따라서 IAM User로 접근하는 방식보다 안전하며 액세스 키가 노출될 위험을 방지할 수 있다.
 
 
먼저 “IAM > 자격 증명 공급자”를 선택한다.

 
공식 문서를 보면 “provider URL, Audience”가 있는데 그대로 넣어주면 된다.

 
 
생성 완료 했으면 이제 Role을 부여해줘야한다. “역할 할당”을 선택한다.

 
 

 
“웹 자격 증명”을 선택하고
 
 

 
아까 생성한 자격 증명 공급자와 audience로 설정하고 접근할 Github repository 정보를 입력한다.
 
 

 
그리고 역할에 부여할 정책을 추가해주면 끝이다.
 
 
이제 workflow를 작성해주자. 자세한 설명은 공식문서를 참고하자..!

...
      
env:
  AWS_REGION: ap-northeast-2
  
permissions: 
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::1234567890:role/GithubOIDC  # role arn
          role-session-name: samplerolesession
          aws-region: ${{ env.AWS_REGION }}

 
 
 


 

2) ECR push & ECS CD

🔗 Deploying to Amazon Elastic Container Service
 
먼저 프로젝트에 JSON 형식의 task definition 파일을 생성해야한다. 파일 형식은 $ aws ecs register-task-definition --generate-cli-skeleton 명령어로 출력되는 형식과 동일해야한다. 앞서 ECS로 태스크 정의를 생성 했을 때 JSON 형식으로 제공됐었다. 그걸 복사해서 사용하면 된다.
 
 
마지막으로 이미지 빌드부터 배포까지 과정을 workflow에 작성해주자. 마찬가지로 공식문서에 잘 설명되어있다.

...

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: my-app       # ecr repository 이름
  ECS_SERVICE: my-app-service  # ecs service 이름
  ECS_CLUSTER: my-app-cluster  # ecs cluster 이름
  ECS_TASK_DEFINITION: ./task_definition.json # 아까 만든 test definition 파일명
  CONTAINER_NAME: my-app       # 태스크 정의 containerDefinitions 섹션에 정의된 이름과 동일해야 함

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@62f4f872db3836360b72999f4b87f1ff13310f3a

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} # steps.[id].outputs.[key]
          IMAGE_TAG: ${{ github.sha }} # commit id
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

      - name: Fill in the new image ID in the Amazon ECS task definition # 파일만 생성
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@c804dfbdd57f713b6c079302a4c01db7017a36fc
        with:
          task-definition: ${{ env.ECS_TASK_DEFINITION }}
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}

      - name: Deploy Amazon ECS task definition # 실제 배포
        uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true # 배포 후 안정화 될 때까지 기다릴 것인가?

 
 
완성된 최종 workflow는 다음과 같다.

name: test and deploy

on:
  push:
    branches:
      - main

permissions: 
  id-token: write
  contents: read

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: my-app
  ECS_SERVICE: my-app-service
  ECS_CLUSTER: my-app-cluster
  ECS_TASK_DEFINITION: ./task_definition.json
  CONTAINER_NAME: my-app

jobs:
  test:  # CI
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Build and test with Docker Compose
        run: docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit

  deploy:
    runs-on: ubuntu-latest
    needs: test
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::1234567890:role/GithubOIDC
          role-session-name: samplerolesession
          aws-region: ${{ env.AWS_REGION }}
        
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@62f4f872db3836360b72999f4b87f1ff13310f3a

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@c804dfbdd57f713b6c079302a4c01db7017a36fc
        with:
          task-definition: ${{ env.ECS_TASK_DEFINITION }}
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}

      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

 
 
main 브랜치로 push 해보면 워크플로우가 실행 된다. ECR, ECS에 들어가보면 업데이트된 이미지와 컨테이너를 확인할 수 있을 것이다.

 
 


 
첫 취업하면서부터 궁금하지만 시도하지 못했던 컨테이너 CI/CD를 드디어 도전하게 되었다.
배우는 데까지 진입장벽은 조금 있었지만 너무나도 편리한 도구였다. 책 한 권 사서 더 깊이 공부해보고싶다!