Infra
ECS - EC2 본격 배포하기 [4] - Github Actions로 CI/CD Workflow 구현
SungHoJung
2025. 4. 6. 23:03
3편에서 설명했듯이, 기존의 EC2에 직접 접근해서 배포를 진행하는 워크플로우에서, ECS를 활용한 워크플로우로 변경하려고 한다. 이미지를 빌드하는 과정까지는 동일하기 때문에, 그 이후 이미지를 업로드하는 과정부터 두 프로세스가 달라진다.
📦 EC2 직접 배포 방식 워크 플로우 설명
- 코드 체크아웃 및 JDK 설정
- actions/checkout 으로 코드를 가져오고 setup-java로 JDK 21을 설정한다.
- application.yaml 생성
- GitHub Secrets에 저장된 환경 정보를 이용해 application.yaml을 만든다.
- Docker 이미지 빌드 및 Docker Hub 푸시
- Buildx를 사용하여 Docker 이미지를 빌드하고, Docker Hub로 푸시한다.
- EC2에 SSH 로 접속해 배포
- scp 및 ssh를 통해 EC2 인스턴스에 접속하여,
- 기존 컨테이너 삭제
- Docker Hub에서 이미지 pull
- 새로운 컨테이너 실행
- scp 및 ssh를 통해 EC2 인스턴스에 접속하여,
실제 적용한 yml 파일은 아래와 같다
name: CI/CD Pipeline with Docker Hub
on:
push:
branches: [dev]
pull_request:
branches: [dev]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
# 1. 리포지토리 체크아웃
- name: Checkout code
uses: actions/checkout@v3
# 2. JDK 21 설정
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
# 3. application.yml 파일 생성
- name: Create application.yml
run: |
touch ./kobaco/src/main/resources/application.yml
echo "${{ secrets.APPLICATION_PROPERTIES }}" > ./kobaco/src/main/resources/application.yml
# 4. 도커 빌드 환경 설정 (buildx 설치)
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# 5. 도커 로그인 (도커 허브)
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# 6. 도커 이미지 빌드 및 푸시
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: kobaco
file: kobaco/Dockerfile
push: true
tags: ${{ secrets.DOCKER_USERNAME }}/kobaco:latest
# 캐시 활용해서 빌드 속도를 향상시킴
cache-from: type=gha
cache-to: type=gha,mode=max
# 7. EC2로 배포
- name: Deploy to EC2
env:
EC2_PEM_KEY: ${{ secrets.EC2_PEM_KEY }}
EC2_HOST: ${{ secrets.EC2_HOST }}
EC2_USER: ${{ secrets.EC2_USER }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: |
echo "$EC2_PEM_KEY" > ec2_key.pem
chmod 400 ec2_key.pem
ssh -i ec2_key.pem -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST << EOF
# 도커 설치 확인
if ! command -v docker &> /dev/null; then
sudo apt update
sudo apt install -y docker.io
sudo usermod -aG docker $EC2_USER
fi
# 기존 컨테이너 중지 및 삭제
docker stop kobaco || true
docker rm kobaco || true
# 도커 허브에서 이미지 풀
docker pull $DOCKER_USERNAME/kobaco:latest
# 컨테이너 실행
docker run -d --name kobaco -p 8080:8080 $DOCKER_USERNAME/kobaco:latest
EOF
rm ec2_key.pem
☁️ ECR-ECS 기반 배포 방식 워크플로우 설명
- 코드 체크아웃 및 JDK 설정
- actions/checkout 으로 코드를 가져오고 setup-java로 JDK 21을 설정한다.
- application.yaml 생성
- GitHub Secrets에 저장된 환경 정보를 이용해 application.yaml을 만든다.
- Docker 이미지 빌드 및 Amazon ECR 푸시
- Buildx를 사용하여 Docker 이미지를 빌드하고, Amazon ECR로 푸시한다.
- latest 및 커멧 SHA 기반 태그를 함께 사용한다.
- ECS Task Definition 업데이트
- 기존 Task Definition을 가져와서, 새 이미지로 갱신한다.
- 필요 시 jq 등을 활용해 일부 필드를 정리한다.
- ECS 서비스에 배포
- amazon-ecs-deploy-task-definition 액션을 사용하여
- 지정된 ECS 클러스터와 서비스에 새로운 Task Definition을 배포한다.
- 서비스 안정화가 완료될 때까지 대기한다.
- amazon-ecs-deploy-task-definition 액션을 사용하여
실제 적용한 yaml 파일은 아래와 같다.
name: Deploy to Amazon ECS
on:
push:
branches: [dev]
pull_request:
branches: [dev]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# 1. 레포지토리 체크아웃
- name: Checkout Code
uses: actions/checkout@v2
# 2. jdk 21 설정
- name: Set up JDK 21
uses: actions/setup-java@v2
with:
distribution: temurin
java-version: '21'
# 3. application.yml 파일 생성
- name: Create application.yml
run: |
touch ./kobaco/src/main/resources/application.yml
echo "${{ secrets.APPLICATION_PROPERTIES }}" > ./kobaco/src/main/resources/application.yml
# 4. 도커 빌드 환경 설정 (buildx 설치)
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
#
- 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: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
run: |
aws ecr get-login-password --region ${{ secrets.AWS_REGION }} \
| docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
- name: Set short git commit SHA
id: vars
run: |
shortSha=$(git rev-parse --short ${{ github.sha }})
echo "::set-output name=short_sha::$shortSha"
- name: Build, tag, and push Docker image to Amazon ECR
id: build-image
env:
# ECR_REGISTRY를 로그인 단계의 출력이나 직접 하드코딩하여 지정
ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
IMAGE_TAG: ${{ steps.vars.outputs.short_sha }}
run: |
# 하드코딩된 ECR 저장소 이름: kobaco-ecr
docker build -t $ECR_REGISTRY/kobaco-ecr:$IMAGE_TAG ./kobaco
docker tag $ECR_REGISTRY/kobaco-ecr:$IMAGE_TAG $ECR_REGISTRY/kobaco-ecr:latest
docker push $ECR_REGISTRY/kobaco-ecr:$IMAGE_TAG
docker push $ECR_REGISTRY/kobaco-ecr:latest
echo "::set-output name=image::$ECR_REGISTRY/kobaco-ecr:$IMAGE_TAG"
- name: Retrieve current ECS task definition JSON file
id: retrieve-task-def
run: |
TASK_DEF_NAME="ci_cd_task"
aws ecs describe-task-definition --task-definition $TASK_DEF_NAME --query taskDefinition > task-definition.json
cat task-definition.json
echo "::set-output name=task_def_file::task-definition.json"
- name: Render updated task definition with new image
id: render-task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ steps.retrieve-task-def.outputs.task_def_file }}
container-name: "ci_cd_container"
image: ${{ steps.build-image.outputs.image }}
- name: Clean task definition (remove enableFaultInjection)
id: clean-task-def
run: |
jq 'del(.enableFaultInjection)' ${{ steps.render-task-def.outputs.task-definition }} > cleaned-task-def.json
cat cleaned-task-def.json
echo "::set-output name=clean_task_def::cleaned-task-def.json"
- name: Deploy updated task definition to Amazon ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.clean-task-def.outputs.clean_task_def }}
service: "ec2-ecs-service-test"
cluster: "kobaco_cluster"
wait-for-service-stability: true
변경된 부분 1 - AWS 자격 증명 구성
- 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: ${{ secrets.AWS_REGION }}
변경된 부분 2 - Amazon ECR 로그인
- name: Login to Amazon ECR
id: login-ecr
run: |
aws ecr get-login-password --region ${{ secrets.AWS_REGION }} \
| docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
변경된 부분 3 - Git 커밋 SHA 생성 (버전 태그용)
- name: Set short git commit SHA
id: vars
run: |
shortSha=$(git rev-parse --short ${{ github.sha }})
echo "::set-output name=short_sha::$shortSha"
변경된 부분 4 - Docker 이미지 빌드 및 ECR 푸시
- name: Build, tag, and push Docker image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
IMAGE_TAG: ${{ steps.vars.outputs.short_sha }}
run: |
docker build -t $ECR_REGISTRY/kobaco-ecr:$IMAGE_TAG ./kobaco
docker tag $ECR_REGISTRY/kobaco-ecr:$IMAGE_TAG $ECR_REGISTRY/kobaco-ecr:latest
docker push $ECR_REGISTRY/kobaco-ecr:$IMAGE_TAG
docker push $ECR_REGISTRY/kobaco-ecr:latest
echo "::set-output name=image::$ECR_REGISTRY/kobaco-ecr:$IMAGE_TAG"
변경된 부분 5 - 기존 ECS Task Definition 조회
- name: Retrieve current ECS task definition JSON file
id: retrieve-task-def
run: |
TASK_DEF_NAME="ci_cd_task"
aws ecs describe-task-definition --task-definition $TASK_DEF_NAME --query taskDefinition > task-definition.json
cat task-definition.json
echo "::set-output name=task_def_file::task-definition.json"
- 목적: 현재 사용 중인 ECS Task Definition JSON을 가져와 파일로 저장.
- 용도: 이후 단계에서 이미지 정보만 바꾸기 위해 필요
변경된 부분 6 - 새로운 이미지로 Task Definition 생성
- name: Render updated task definition with new image
id: render-task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ steps.retrieve-task-def.outputs.task_def_file }}
container-name: "ci_cd_container"
image: ${{ steps.build-image.outputs.image }}
- 역할: 기존 Task Definition에 새로 빌드한 Docker 이미지 경로를 반영.
- 사용 툴: AWS 공식 amazon-ecs-render-task-definition 액션.
- 전제 조건: container-name이 기존 정의에 있는 것과 동일해야 함.
변경된 부분 7 - 불필요한 필드 제거(선택적 단계)
- name: Clean task definition (remove enableFaultInjection)
id: clean-task-def
run: |
jq 'del(.enableFaultInjection)' ${{ steps.render-task-def.outputs.task-definition }} > cleaned-task-def.json
cat cleaned-task-def.json
echo "::set-output name=clean_task_def::cleaned-task-def.json"
- 목적: Task Definition에서 불필요하거나 ECS에서 오류를 일으킬 수 있는 필드 제거.
- 툴: jq를 사용해 JSON 파일에서 키 제거.
- 예시: enableFaultInjection 필드를 제거.
변경된 부분 8 - ECS 서비스에 새로운 Task Definition 배포
- name: Deploy updated task definition to Amazon ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.clean-task-def.outputs.clean_task_def }}
service: "ec2-ecs-service-test"
cluster: "kobaco_cluster"
wait-for-service-stability: true
- 역할: 지정된 ECS 클러스터와 서비스에 새 Task Definition을 적용해 배포.
- 기능:
- 배포 후 ECS 서비스가 안정화될 때까지 대기 (wait-for-service-stability).
- Task Definition의 새로운 버전을 자동 적용.
설정한 GitHub secrets
- APPLICATION_PROPERTIES : application.yaml 파일을 그대로 넣어놓은 것이다.
- AWS_ACCOUNT_ID : 내 계정 아이디를 넣어주면 된다. 내 계정 ID는 ECR 엔드포인트에 사용된다
- AWS_REGION : ECR 리전을 넣어주면 된다.
- AWS_SECRET_ACCESS_KEY : 깃헙 액션에서 권한을 주기 위해 사용하는 IAM 의 엑세스 키
- AWS_ACCESS_KEY_ID : 깃헙 액션에서 권한을 주기 위해 사용하는 IAM 의 시크릿 키
IAM 권한 설정
IAM > 사용자 선택 > 권한 추가 > 인라인 정책 생성 > JSON
# ecs-task-execution-role-transmisson
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPassEcsTaskExecutionRole",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::050752625892:role/ecs-task-execution-role"
}
]
}
# ecs_RegisterTaskDefinition
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowRegisterTaskDefinition",
"Effect": "Allow",
"Action": [
"ecs:RegisterTaskDefinition"
],
"Resource": "arn:aws:ecs:ap-southeast-2:050752625892:task-definition/ci_cd_task:*"
}
]
}
# githubECSTaskLookUp
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowECSReadActions",
"Effect": "Allow",
"Action": [
"ecs:DescribeTaskDefinition",
"ecs:ListClusters",
"ecs:DescribeClusters",
"ecs:ListTaskDefinitions",
"ecs:DescribeServices"
],
"Resource": "*"
}
]
}
IAM > 사용자 선택 > 권한 추가 > 권한 추가 > 직접 정책 연결
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ECRPermissions",
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:BatchGetImage",
"ecr:CompleteLayerUpload",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
"ecr:UploadLayerPart"
],
"Resource": "*"
},
{
"Sid": "ECSPermissions",
"Effect": "Allow",
"Action": [
"ecs:UpdateService",
"ecs:DescribeServices",
"ecs:ListClusters",
"ecs:DescribeClusters"
],
"Resource": "*"
},
{
"Sid": "LogsPermissions",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}