github actions + ECS+ ECR CI/CD 구축

민정·2025년 2월 4일
post-thumbnail

서론

이전에 github actions + codedeploy + Docker + ecr + s3 + asg로 CI/CD 구축을 힘겹게 진행했다. 당시에는 CI/CD가 무엇인지도 제대로 이해하지 못하고 구축했으며, 구축 과정이 너무 힘들어서 그냥 수동 배포하는게 낫겠다고 생각했었다.
그러나 막상 CI/CD를 구축하지 않고 수동 배포를 진행하니 단순한 push 하나에도 프로젝트를 빌드하고, jar 파일을 bastion host에 올리고, 이를 프라이빗 서브넷 내의 ec2에 전송해야 해 굉장히 번거로웠다.
이번 기회에 CI/CD의 소중함을 크게 느꼈으며... 제대로 학습해서 구축하기로 결심했다.


CI/CD란?

CI/CD는 지속적 통합(Continuous Integration) 및 지속적 제공/배포(Continuous Delivery/Deployment)을 말한다.

CI는 애플리케이션에 대한 새로운 변경 사항을 정기적으로 빌드 및 테스트되어 공유 리포지토리에 통합하는 것을 말한다.

CD는 지속적 제공(Continuous Delivery)과 지속적 배포(Continuous Deployment)로 의미가 나뉜다. 둘은 CI단계에서 빌드, 테스트가 진행된 후 배포 준비상태를 확인하는 것은 동일하나 최종 단계에서 지속적 제공은 수동으로 배포를 진행하고, 지속적 배포는 자동으로 배포가 진행된다. 이번 포스트에서 사용하는 CD는 지속적 배포(Continuous Deployment)이다.


Github actions 알아보기

github actions는 workflow → job → step -> action으로 구성되어 있다.

  • Runner

    • Github actions 워크플로우가 실행되는 인스턴스를 말한다.
    • Runner에 github actions 실행에 필요한 시스템을 설치해야 한다.
    • 설치한 시스템이나 설정은 현재 실행중인 러너에만 적용되며, 해당 워크플로우가 종료되는 경우 사라진다.
  • workflow

    • name: workflow의 이름을 설정한다.
    • on: workflow의 트리거를 설정한다.
    • env: 모든 job, step에서 공통으로 사용 가능한 환경 변수를 지정한다.
    • 하나의 workflow는 여러 job으로 구성되어 있다.
  • job

    • 여러 개의 job은 병렬적으로 수행될 수 있고, job 간 의존 관계를 가질 수 있다.
    • runs-on: job이 실행될 머신 타입을 설정한다.
      • 각각의 job마다 설정 가능하다.
      • 각각의 job은 독립된 가상 환경으로 구분되어 있기 때문이다.
  • step

    • job 안에서 순차적으로 실행되는 프로세스 단위이다.
  • action

    • workflow에서 가장 작은 단위이다.
    • 사용자가 직접 커스터마이징하거나, 마켓플레이스에 있는 action을 가져와 사용할 수 있다.


AWS의 CI/CD 서비스

AWS에는 CI/CD를 위한 여러 서비스가 제공된다.

  • CodeCommit
    • git을 기반으로 작동하는 소스 리포지토리
    • github와 유사하며, aws 전용 서비스이다.
  • CodeBuild
    • CI 서비스
    • 코드를 컴파일, 테스트, 빌드한다.
  • CodeDeploy
    • CD 서비스
    • 코드 기반으로 동작하며, AppSpec.yml를 바탕으로 배포 프로세스가 정의된다.
    • 소스를 자동 배포한다.
  • CodePipeline
    • CI/CD 서비스를 모두 통합, 관리한다.

AWS 서비스와의 높은 통합을 갖는 CI/CD를 구축하고 싶다면, CodePipeline을 사용하는 것을 추천한다.


어떤 방법을 사용할까?

CI/CD 구축에 있어 고려했던 방법은 다음과 같다.

  • github actions → EC2 배포

    • github actions로 도커 이미지 빌드
    • github actions에서 ssh 접속을 통해 EC2 접속 및 배포 진행
      • 보안상 부적절하다 느꼈다.
    • 혹은 github actions에서 System Manager Run Command를 통해 배포 진행
  • github actions + CodeDeploy → EC2 배포

    • github actions로 s3에 appspec.yml 업로드 및 CodeDeploy 트리거
    • appspec.yml에서 deploy.sh 파일을 트리거, deploy.sh 파일의 명령어 실행
      • 이전에 프로젝트를 진행할 때 위와 같은 방식으로 CI/CD를 구축했다.
      • 과정이 복잡하고 어려우며, 비효율적으로 느껴져 이 방식이 아닌 더 나은 다른 방식을 사용하기로 했다.
  • CodePipeline → ECR 배포

  • github actions → ECR 배포

    • github actions로 ECR에 Docker 이미지를 push
    • github actions로 ECR 이미지를 바탕으로 ECS 서비스 배포 진행

최종적으로는 마지막 방식을 사용해 배포를 진행했다. 그 이유는 다음과 같다. - 프로젝트의 목적성에 맞춰 ASG가 아닌 ECS를 사용하게 되었다. - CodePipeline보다는 github actions가 익숙하고 쉽다.

CI/CD 구축하기

CI/CD는 github actions + ECR + ECS를 사용해 구축했다.

CI는 다음과 같이 진행된다.

  • github에서 dev 브랜치에 push하는 경우, github actions가 트리거된다.
  • github actions를 통해 Spring boot 코드가 빌드 및 테스트된다.
  • Docker 이미지를 생성하고, ECR에 push한다.

CD는 다음과 같이 진행된다.

  • github actions에서 가장 최신의 ECS 태스크 정의를 가져온다.
  • github actions에서 가져온 태스크 정의와 앞서 생성한 ECR의 이미지를 바탕으로 ECS 태스크 정의를 생성한다.
  • 생성한 ECS 태스크 정의를 바탕으로 ECS 서비스에 태스크를 배포한다.
  • ECS 서비스에서 롤링 배포가 진행된다.


Dockerfile

도커 이미지를 빌드하는데 필요한 Dockerfile을 Spring boot 루트 디렉토리에 생성한다.

FROM openjdk:17-jdk
ARG JAR_FILE=./build/libs/*-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]



github actions 코드

name: CICD

on:
  push:
    branches: [ "dev" ]

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
    - uses: actions/checkout@v4
    
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'
            
    - name: Gradle Cache
      uses: actions/cache@v4.2.0
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    - name: Build with Gradle
      run: ./gradlew build -x test

    - name: Configure AWS IAM credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
        aws-region: ap-northeast-2

    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v2

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

    - name: Retrieve most recent ECS task definition JSON file
      env:
        TASK_DEFINITION: [태스크 정의 이름]
      run: |
        aws ecs describe-task-definition --task-definition $TASK_DEFINITION --query taskDefinition > task-definition.json

    - name: Create ECS task definition
      id: setting-task-definition
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      with:
        task-definition: task-definition.json
        container-name: [컨테이너 이름]
        image: ${{ env.IMAGE_URI }}

    - name: Clean up task definition file
      run: |
        rm task-definition.json

    - name: Deploy Amazon ECS task definition
      uses: aws-actions/amazon-ecs-deploy-task-definition@v2
      with:
        task-definition: ${{ steps.setting-task-definition.outputs.task-definition }}
        service: [ecs 서비스명]
        cluster: [ecs 클러스터명]
        wait-for-service-stability: true

github actions 워크플로우 코드는 위와 같은 코드를 사용했다. 각각의 action이 무엇을 실행하는지 하나씩 설명해보겠다.

workflow 설정

name: CICD

on:
  push:
    branches: [ "dev" ]

workflow 이름과 트리거 조건을 설정한다. 트리거 조건은 dev 브랜치에 push되는 경우로 설정했다.

job 설정

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read

가상 러너와 권한을 설정한다. 가상 러너는 우분투로, 권한은 읽기 전용으로 한정한다.

step + action 설정

- name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'

github actions의 가상 러너에 JDK 17을 설치한다.

- name: Gradle Cache
      uses: actions/cache@v4.2.0
      with:
        path: |
          ~/.gradle/caches
          ~/.gradle/wrapper
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

gradle 캐싱 작업을 실행한다. actions의 Caches에 캐싱되며, CI 속도가 향상된다.

- name: Grant execute permission for gradlew
	run: chmod +x gradlew

- name: Build with Gradle
	run: ./gradlew build -x test

빌드를 위한 권한을 부여하고, Spring boot 애플리케이션을 빌드한다.

- name: Configure AWS IAM credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
    aws-region: ap-northeast-2

Access Key와 Secret Key를 사용해 IAM 자격 증명을 인증한다.
Secret에서 AWS_ACCESS_KEY, AWS_SECRET_KEY 환경변수를 생성해야 한다.

- name: Login to Amazon ECR
  id: login-ecr
  uses: aws-actions/amazon-ecr-login@v2

ECR에 로그인한다.

- name: Build, tag, and push docker image to Amazon ECR
  id: build-image
  env:
    REGISTRY: ${{ steps.login-ecr.outputs.registry }}
    REPOSITORY: [ecr 리포지토리 이름]
    IMAGE_TAG: ${{ github.sha }}
  run: |
    docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG .
    docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG
    echo "IMAGE_URI=$REGISTRY/$REPOSITORY:$IMAGE_TAG" >> $GITHUB_ENV

Docker 이미지를 빌드하고, ECR에 이미지를 push한다.
도커 이미지 URI를 환경변수로 저장한다.

- name: Retrieve most recent ECS task definition JSON file
  env:
    TASK_DEFINITION: [태스크 정의 이름]
  run: |
    aws ecs describe-task-definition --task-definition $TASK_DEFINITION --query taskDefinition > task-definition.json

ECS 태스크 정의 중 가장 최신 개정을 task-definition.json 파일로 가져온다.

- name: Create ECS task definition
  id: setting-task-definition
  uses: aws-actions/amazon-ecs-render-task-definition@v1
  with:
    task-definition: task-definition.json
    container-name: [컨테이너 이름]
    image: ${{ env.IMAGE_URI }}

- name: Clean up task definition file
  run: |
    rm task-definition.json

가져온 태스크 정의와 위에서 생성한 도커 이미지의 URI를 바탕으로 새로운 태스크 정의를 생성한다. 태스크 정의를 생성한 후 task-definition.json 파일은 삭제한다.

- name: Deploy Amazon ECS task definition
  uses: aws-actions/amazon-ecs-deploy-task-definition@v2
  with:
    task-definition: ${{ steps.setting-task-definition.outputs.task-definition }}
    service: [ecs 서비스명]
    cluster: [ecs 클러스터명]
    wait-for-service-stability: true

생성한 태스크 정의를 바탕으로 ECS 서비스에 태스크를 배포한다.

profile
시스템 + 리눅스 + 클라우드

0개의 댓글