Infra

ECS - EC2 본격 배포하기 [4] - Github Actions로 CI/CD Workflow 구현

SungHoJung 2025. 4. 6. 23:03
3편에서 설명했듯이, 기존의 EC2에 직접 접근해서 배포를 진행하는 워크플로우에서, ECS를 활용한 워크플로우로 변경하려고 한다. 이미지를 빌드하는 과정까지는 동일하기 때문에, 그 이후 이미지를 업로드하는 과정부터 두 프로세스가 달라진다.

📦 EC2 직접 배포 방식 워크 플로우 설명

  1. 코드 체크아웃 및 JDK 설정
    • actions/checkout 으로 코드를 가져오고 setup-java로 JDK 21을 설정한다.
  2. application.yaml 생성
    • GitHub Secrets에 저장된 환경 정보를 이용해 application.yaml을 만든다.
  3. Docker 이미지 빌드 및 Docker Hub 푸시
    • Buildx를 사용하여 Docker 이미지를 빌드하고, Docker Hub로 푸시한다. 
  4. EC2에 SSH 로 접속해 배포
    • scp 및 ssh를 통해 EC2 인스턴스에 접속하여, 
      • 기존 컨테이너 삭제
      • Docker Hub에서 이미지 pull
      • 새로운 컨테이너 실행

실제 적용한 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 기반 배포 방식 워크플로우 설명

  1. 코드 체크아웃 및 JDK 설정
    • actions/checkout 으로 코드를 가져오고 setup-java로 JDK 21을 설정한다.
  2. application.yaml 생성
    • GitHub Secrets에 저장된 환경 정보를 이용해 application.yaml을 만든다.
  3. Docker 이미지 빌드 및 Amazon ECR 푸시
    • Buildx를 사용하여 Docker 이미지를 빌드하고, Amazon ECR로 푸시한다. 
    • latest 및 커멧 SHA 기반 태그를 함께 사용한다.
  4. ECS Task Definition 업데이트
    • 기존 Task Definition을 가져와서, 새 이미지로 갱신한다.
    • 필요 시 jq 등을 활용해 일부 필드를 정리한다.  
  5. ECS 서비스에 배포
    • amazon-ecs-deploy-task-definition 액션을 사용하여
      • 지정된 ECS 클러스터와 서비스에 새로운 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": "*"
        }
    ]
}