
지난번 포스트를 통해 로컬에서 만든 도커 이미지를 ECR에 업로드까지 진행했습니다. 이제 해당 이미지를 바탕으로 ECS를 사용해 본격적으로 컨테이너를 띄우고 서버를 실행시켜야합니다.
그리고 ECR를 통한 이미지 업로드부터 ECS 배포까지 이 일련의 작업을 GitHub Action을 통해 자동화해 CI/CD를 구축해보겠습니다.
ECS를 사용하기에 앞서 ECS가 어떤 구조로 동작하고 있는지를 간단하게 정리해보고자 합니다.
ECS의 구조를 이해하기 위해서는 먼저 Task Definition, Task, Service, Cluster의 의미를 파악하는 것이 좋습니다.
위 개념에 따라 이전 포스트에서 올린 image를 컨테이너로 띄우기 위해, 컨테이너를 실행시키는 최소단위를 만드는 설계서인 Task Definition을 만들어야합니다.

먼저 AWS 콘솔에서 ECS에 들어온 후, 좌측 메뉴의 테스크 정의 탭을 클릭한 다음 새 태스크 정의 생성을 통해 태스크 정의 생성을 시작합니다.


만약 추가적으로 띄울 컨테이너가 있으면 컨테이너 추가를 클릭해 같은 방식으로 설정을 해주고 설정을 마친 뒤에 다음을 눌러 추가 설정을 이어나갑니다.

위 설정을 모두 마쳤으면 다음 버튼과 완료 버튼을 눌러 태스크 생성을 마칩니다.

클러스터 생성에는 클러스터 이름, VPC와 서브넷 설정을 하는 것 이외에는 특별히 설정해줄만한 것은 없습니다. VPC와 서브넷이 무엇이고 어떻게 설정하는지에 대해서는 인프라세팅에 중요한 요소인만큼 추후 포스트에서 따로 자세히 다루어보겠습니다.
위 용어 정리에서 다루었던 Task, Cluster, Service 중에 마지막인 service를 생성해야합니다. Service는 하나의 Cluster 위에서 돌아가기 때문에 위에서 생성한 클러스터를 클릭해서 생성버튼을 찾을 수 있습니다.

위 사진처럼 생성 버튼을 찾아 클릭하면 Service에 대해 설정할 수 있는 페이지가 나옵니다.

설정 페이지에서 다음과 같은 내용을 고려하여, 서비스를 생성합니다.
컴퓨팅 옵션
용량 공급자 전략은 용량 공급자를 만들어 클러스터 안에서 Task가 분산되어 실행되는 방식입니다.
하나의 용량 공급자 전략은 여러 개의 용량 공급자를 만들 수 있고, 용량 공급자는 ECS가 scale in, scale out 하는 클러스터 내부의 용량을 정의합니다. 따라서 시작 유형 옵션에 비해 좀 더 유연한 스케일링 방식을 가지고 있다고 볼 수 있습니다.
용량 공급자 옵션에서 "기본"과 "가중치"에 대한 옵션이 있는데, 여기서 "기본"은 용량 공급자에서 실행되는 최소한의 Task 수를 의마하며, 용량 공급자 전략에서 하나의 용량 공급자만 "기본" 값을 정의할 수 있습니다. "가중치" 옵션은 두 개의 용량 공급자가 있고 각각의 가중치 옵션이 1:4 라고 할 때, 첫 번째 용량 공급자의 Task 가 1개 일 때 두 번째 용량 공급자에 대해서 Task가 4개 실행됩니다.
시작유형 옵션은, AWS 클러스터에서 컨테이너를 실행하는데 사용할 수 있는 리소스를 Fargate와 EC2 중에서 결정하게 됩니다. 클러스터에 대한 관리나 스케일링 작업 대신에 서비스를 단순히 정의하고 실행하는 데 초점을 맞춘 컴퓨팅 옵션입니다.
아래 서비스 설정에 보면 Auto Scaling 이라는 옵션이 있는데, 이것은 Service 전체에 대한 Auto Scaling 옵션으로, Task를 대상으로 Scaling 하는 용량 공급자 전략과는 별개입니다.


이로써 ECS 서비스에 대한 설정을 마쳤으며, 생성 버튼을 클릭해서 Task Definition 에 정의된 Task를 실행시켜서 서버를 구동할 수 있게 되었습니다.
이제 ECR를 통한 이미지 업로드부터 ECS 배포까지 일련의 작업을 GitHub Action을 통해 자동화한 과정을 적어보겠습니다.
지금까지 ECS를 사용한 배포 과정을 정리해보자면
1. 프로젝트를 build해서 jar 파일을 생성한다.
2. docker file을 만들어서 jar 파일을 담은 docker image를 생성한다.
3. docker image를 ECR에 업로드한다.
4. ECR에 업로드한 image를 기반으로 Task Definition을 만든다.
5. ECS Cluster와 Service를 생성해 서버를 배포한다.
총 5단계로 구성되어 있습니다.
이 5단계의 과정을 서버를 배포하려고 시도할 때마다 거치는 것은 매번 많은 시간을 잡아먹었으며, 개발의 진행도를 늦추는 큰 요인 중 하나로 자리했었습니다.
이를 자동화 시킬 방안으로 Jenkins, GitHub Action 등의 tool을 염두에 두고 비교해보았고, 결과적으로는 무료로 사용할 수 있으면서 관리가 쉬운 GitHub Action 스크립트를 사용한 자동배포 파이프라인을 구축했습니다.
이제 밍글이 GitHub Action을 도입한 과정과 사용하는 방법에 대해서 적어보겠습니다.
먼저 ECS에 Service를 GitHub Action을 통해 만들기 위해서 Task Definition에 대한 정보를 프로젝트 파일이 가지고 있어야합니다.

위 사진과 같이 AWS 콘솔에서 ECS > 테스크 정의 > 원하는 테스크 > JSON 카테고리에 들어오면 Taks Definition에 대한 JSON 파일을 다운 받을 수 있는데, 해당 파일을 프로젝트 폴더의 루트 디렉토리에 위치시켜줍니다.
GitHub에서는 친절히 처음 사용해보는 유저들도 쉽게 GitHub Action workflow를 만들 수 있도록 많은 유명한 프로세스들에 대해서 템플릿을 제공해주고 있습니다.

위와 같이 리포지토리에서 Actions 탭에 들어가서 New workflow 라는 버튼을 클릭해줍니다.

그러면 많은 자동화 workflow에 대한 템플릿들을 볼 수 있는데 저희는 그 중에서 ecs라고 검색해 Deploy to Amazon ECS 라는 템플릿을 찾은 후 Configure를 눌러서 편집을 시작해줍니다.
템플릿을 다운 받았으면 해당 템플릿에서 요구하는 사항들을 채워 넣어주어야 합니다.
name: Deploy to Amazon ECS CI/CD
on:
push:
branches: [ "dev" ]
env:
AWS_REGION: ap-northeast-2 # set this to your preferred AWS region, e.g. us-west-1
ECR_REPOSITORY: mingle-api-dev # set this to your Amazon ECR repository name
ECS_SERVICE: mingle-api-service-dev # set this to your Amazon ECS service name
ECS_CLUSTER: mingle-api-dev-cluster # set this to your Amazon ECS cluster name
ECS_TASK_DEFINITION: task-definition-dev.json # set this to the path to your Amazon ECS task definition
# # file, e.g. .aws/task-definition.json
CONTAINER_NAME: mingle-api-dev # set this to the name of the container in the
# containerDefinitions section of your task definition
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Copy Secret
env:
CREATE_SECRET: ${{secrets.MINGLE_DEV_APPLICATION_YML}}
CREATE_SECRET_DIR: src/main/resources
CREATE_SECRET_DIR_FILE_NAME: application.yml
run: echo $CREATE_SECRET | base64 --decode > $CREATE_SECRET_DIR/$CREATE_SECRET_DIR_FILE_NAME
- name: Build with Gradle
run: ./gradlew build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- 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: |
# Build a docker container and
# push it to ECR so that it can
# be deployed to ECS.
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@v1
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@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
자세한 정리에 앞서, 위 코드는 밍글에서 dev 서버 배포에 사용 중인 github action script 입니다.
아래 내용들을 통해 자세한 설정 방법들에 대해 정리해 보겠습니다.
on:
push:
branches: [ "dev" ]
env:
AWS_REGION: ap-northeast-2 # set this to your preferred AWS region, e.g. us-west-1
ECR_REPOSITORY: mingle-api-dev # set this to your Amazon ECR repository name
ECS_SERVICE: mingle-api-service-dev # set this to your Amazon ECS service name
ECS_CLUSTER: mingle-api-dev-cluster # set this to your Amazon ECS cluster name
ECS_TASK_DEFINITION: task-definition-dev.json # 위에서 다운받은 Task Definition의 파일 이름
CONTAINER_NAME: mingle-api-dev # set this to the name of the container in the
# containerDefinitions section of your task definition
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: production
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Copy Secret
env:
CREATE_SECRET: ${{secrets.MINGLE_DEV_APPLICATION_YML}}
CREATE_SECRET_DIR: src/main/resources
CREATE_SECRET_DIR_FILE_NAME: application.yml
run: echo $CREATE_SECRET | base64 --decode > $CREATE_SECRET_DIR/$CREATE_SECRET_DIR_FILE_NAME

이러한 secret GitHub Repository에 Setting 탭에 들어온 후 Security > Secrets and variables > Action 에 들어와서 원하는 데이터를 저장할 수 있으며, 이후 정보를 가져오고 싶을 때는 secrets.${secret의 이름} 방식으로 가져올 수 있습니다.
위 코드에서는 secrets에서 밍글 프로젝트의 application.yml 파일을 secrets로부터 가져와 base64로 인코딩 된 값을 decoding해서, resources 파일 내부에 저장하고 있습니다.
- name: Build with Gradle
run: ./gradlew build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- 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: |
# Build a docker container and
# push it to ECR so that it can
# be deployed to ECS.
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@v1
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@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
위 코드에서부터 본격적으로 AWS와 연결하여 서비스 배포를 시작합니다.
밍글은 해당 ID와 Password를 github secret에 저장을 해두었기 때문에, secrets.AWS_ACCESS_KEY_ID 와 같은 방식으로 변수를 가져오게됩니다.
Login to Amazon ECR
위에서 작성한 id와 password를 바탕으로 ECR에 접근, 수정할 수 있는 권한을 가집니다.
Build, tag, and push image to Amazon ECR
해당 script를 통해 docker image를 생성하고 ECR로 push하게 됩니다.
steps.login-ecr.outputs.registry 는 이전 단계인 login to Amazon ECR의 aws-actions/amazon-ecr-login@v1 액션의 출력 중 하나로 ECR 레지스트리 주소를 참조하는 변수입니다.
github.sha는 푸시된 현재 커밋의 sha 값으로 각 커밋마다 고유한 태그를 사용해서 이미지 식별에 사용할 수 있습니다.
이렇게 만들어진 환경변수를 바탕으로 docker를 build 하고, ECR에 push 해 줍니다.
Fill in the new image ID in the Amazon ECS task definition
위 env 에서 작성한 task definition의 값에 이번에 배포하게 될 이미지의 이름과 태그 값을 넣습니다.
Deploy amazon ecs task definition
Task Definition을 롤링배포 바탕으로 배포해서 서비스를 업데이트하고 안정화가 될 때까지 기다립니다.

이와 같이 설정을 하고, 해당 설정 yml 파일을 프로젝트 폴더의 루트 디렉토리에 .github 라는 폴더에 위치시켜서 관리합니다.

이제 설정해둔 branch에 push가 될 시 위와 같이 github action이 자동으로 실행되면서 서버가 매번 새롭게 배포되는 것을 알 수 있습니다.
지금까지 ECS와 Fargate를 이용해 서버를 배포해보고, GitHub Action을 통해 ECS에 서버를 자동으로 배포하는 과정을 정리해보았습니다.
감사합니다.