1/22/2024

GitHub Actions CI/CD

CI/CD

수동으로 배포를 하면 진행하면서 배포 과정의 세부사항을 배우고 오류도 고치고 이렇게 배포 과정을 이해하기 위해서 이러한 수동 배포가 한 번은 필요하지만, 개발 후 테스트 그리고 배포까지의 과정은 쉽지 않고 반복적이고 지루하다. 테스트와 배포같은 수동적이고 반복적이면서 비생산적인 업무를 최대한 줄이고 그 시간에 개발을 더 할 수 있을지를 고민한 결과가 바로 자동화이다. 다시 말해, 단위 테스트, 통합 테스트, E2E 테스트 등의 테스트 과정과 모든 배포 과정을 굳이 사람이 직접 실행할 필요 없이 자동으로 실행 되도록 하고 문제가 생겼을때 자동으로 알려주는 시스템을 구축해서 모든 과정을 자동으로 진행하는 것이다!

CI/CD를 위한 여러 툴이 있는데 나는 GitHub에 내장된 CI/CD 플랫폼인 GitHub Actions을 사용했다. GitHub Actions으로 특정 브랜치 푸시 및 풀 요청과 같은 이벤트가 발생하면 일련의 스크립트를 실행한다. 일단 단위 테스트를 자동화 해본다!

단위 테스트 자동화의 1번째 단계는 테스트가 Docker이미지에서 실행되어야 한다. 4iz Dockerfile 포스트를 보면 Dockerfile에 다단계 빌드를 적용하여 환경에 따라 설치하는 의존성이 다르다. 테스트 환경은 개발 환경과 마찬가지로 package.json 파일의 devDependencies에 나열된 패키지가 필요한데 바로 여기에 Jest와 같은 테스트에 필요한 패키지가 위치하기 때문이다. 포스트를 보면 test 단계를 대상으로 Docker 이미지에서 단위 테스트를 실행한다. 따라서 --target=test 옵션을 사용해야 한다. 이를 바탕으로 브랜치에 푸시가 발생할 때마다 실행되는 GitHub Actions 워크플로우를 만든다.


test.workflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
name: Test the application
# 레포지토리에 모든 푸시 이벤트가 발생할 때마다 워크플로우가 실행된다.
on: push
 
# 테스트에 사용될 Docker 이미지를 지정한다.
env:
  DOCKER_IMAGE_TEST: csup96/4iz-test:latest
 
jobs:
  test:
    name: Run tests
    runs-on: ubuntu-latest
    steps:
      # actions/checkout@v3 액션을 사용하여 레포지토리의 내용을 가져온다.
      - name: Check out the repository
        uses: actions/checkout@v3
      # docker/setup-qemu-action@v2 액션을 사용하여 QEMU를 설정한다(QEMU(Quick Emulator)는 에뮬레이션을 위해 사용된다.).
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2
      # docker/setup-buildx-action@v2 액션을 사용하여 Docker Buildx를 설정한다(Buildx는 Docker를 위한 CLI 플러그인으로, 다중 플랫폼 Docker 이미지를 빌드하는 데 추가 기능을 제공한다.).
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      #  docker/build-push-action@v4 액션을 사용하여 Docker 이미지를 빌드한다.
      - name: Build a Docker image
        uses: docker/build-push-action@v4
        with:
          context: . # 현재 디렉토리를 빌드 컨텍스트로 지정한다.
          load: true # 빌드된 이미지를 Docker 데몬에 로드한다.
          target: test # 빌드 대상을 "test"로 지정한다(다단계 빌드.).
          tags: ${{ env.DOCKER_IMAGE_TEST }} # 정의한 환경 변수를 사용하여 Docker 이미지에 태그를 지정한다.
 
      # 컨테이너를 실행할 Docker 이미지를 지정하고 docker container run 명령을 사용하여 Docker 컨테이너 내에서 단위 테스트를 실행하며 --rm은 컨테이너가 종료된 후에 컨테이너를 제거한다.
      - name: Run unit tests with a Docker container
        run: docker container run --rm ${{ env.DOCKER_IMAGE_TEST }}
cs
요약하면, 이 워크플로우는 Docker 이미지를 빌드하고 Docker 컨테이너 내에서 단위 테스트를 실행함으로써 애플리케이션 테스트 과정을 자동화한다. Docker 이미지는 지정된 이미지 이름으로 태그되며 전체 과정은 레포지토리에 푸시될 때마다 실행된다.

작업 중인 브랜치에서 푸시를 하고 GitHub의 4iz 레포지토리의 Actions에 가면 다음과 같이 자동화가 성공적으로 완료되었다.

구체적인 세부 사항을 보면 총 42개의 단위 테스트가 통과된 것을 볼 수 있다.
걸린 시간은 8.624초~


주의할 점이 있는데 만약에 이 워크플로우가 풀 리퀘스트 이벤트가 발생할 때마다 실행되는데 테스트가 실패하면 어떻게 될까? 아이러니하게도 테스트가 실패하더라도 무시하고 브랜치를 병합할 수 있다! 이를 방지하기 위해 테스트가 실패하면 브랜치를 병합하지 못하도록 브랜치 보호 규칙을 추가해야 한다. GitHub 레포지토리에 들어가서 Settings에서 Branches로 이동해야 한다. 여기서 상태 확인이 성공하지 않은 경우 브랜치를 병합할 수 없도록 하는 보호 규칙을 추가한다. 나의 경우 main 브랜치에만 이를 적용했다~


deploy.workflow

배포 자동화는 테스트 자동화보다 더 어렵고 복잡하다. 뭐 당연하지만! 일단 GitHub Actions이 AWS에서 사용자를 대신해 작업을 수행해야 한다. 이를 위한 방법 중 하나는 GitHub이 AWS 사용자에게 인증할 수 있도록 하는 것이다. 따라서 GitHub이 사용자의 계정에 로그인할 수 있도록 권한을 부여해야 한다. GitHub에게 주어야 할 권한은 ECR 로그인, Docker 이미지 빌드와 레지스트리 푸시, 기존 ECS 서비스 업데이트로 볼 수 있다. 나의 경우, AWS 계정 생성 후 AdministratorAccess 권한을 가지는 admin 그룹에 사용자를 추가해서 이 과정은 생략한다.

GitHub Actions이 사용자를 사용할 수 있도록 하는 방법은 IAM에서 사용자에게 접근 키를 생성하는 것이다. 접근 키는 두 부분(접근 키, 비밀 접근 키)으로 구성되는데 두 키를 모두 가지고 있으면 특정 사용자로 인증할 수 있다. 이제 GitHub 레포지토리에 들어가서 Settings에서 Secrets and variables에서 Actions으로 이동한다. 여기서 Secrets에 앞서 생성한 접근 키의 두 부분을 추가한다.

이제 배포 워크플로우를 작성한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
name: Deploy the application to production
 
# main 브랜치 또는 dev 브랜치를 대상으로 하는 풀 리퀘스트가 발생할 때마다 워크플로우가 실행된다.
on:
  pull_request:
    branches:
      - main
      - dev
 
env:
  DOCKER_IMAGE_TEST: csup96/4iz-test:latest
  DOCKER_COMPOSE_SERVICE: app # E2E 테스트에 사용될 Docker Compose 서비스를 지정한다.
 
jobs:
  build-test-push:
    # Docker 이미지를 빌드하고 테스트 한 후 Amazon ECR에 푸시한다.
    name: Build and test a Docker image and push it to Amazon ECR
    runs-on: ubuntu-latest
    steps:
      - name: Check out the repository
        uses: actions/checkout@v3
 
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
 
      - name: Build a Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          load: true
          target: test
          tags: ${{ env.DOCKER_IMAGE_TEST }}
 
      - name: Run unit tests with a Docker container
        run: docker container run --rm ${{ env.DOCKER_IMAGE_TEST }}
      # Docker Compose로 사용하여 E2E 테스트를 실행한다.
      - name: Run E2E tests with a Docker Compose
        run: docker compose -f docker-compose.ci.yml up --build --exit-code-from ${{ env.DOCKER_COMPOSE_SERVICE }}
      # 인증을 위한 AWS 자격 증명을 구성한다.
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        # GitHub 레포지토리 -> Settings -> Secrets and variables -> Actions -> Secrets
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2
 
      # Amazon ECR에 로그인한다.
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
      # AWS 자격 증명을 사용하여 Docker 이미지를 Amazon ECR에 빌드, 태그 및 푸시한다.
      - name: Build, tag, and push the Docker image to Amazon ECR
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }} # 현재 작업의 이전 단계(Login to Amazon ECR)에서 나온 값을 사용한다.
          REPOSITORY: 4iz
          IMAGE_TAG: latest
        run: |
          docker image build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG .
          docker image push $REGISTRY/$REPOSITORY:$IMAGE_TAG
 
  deploy:
    # 최신 Docker 이미지를 Amazon ECS에 배포한다.
    name: Deploy the latest Docker image to Amazon ECS
    runs-on: ubuntu-latest
    # 이전 작업(build-test-push)이 완료될 때까지 기다린다.
    needs: build-test-push
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-2
      # AWS CLI(aws ecs update service)를 사용하여 ECS 서비스를 최신 Docker 이미지로 업데이트하고 새로운 배포를 실행한다.
      - name: Update ECS service
        # GitHub 레포지토리 -> Settings -> Secrets and variables -> Actions -> Variables
        # --force-new-deployment는 서비스 정의가 변경되지 않았을 때도 새로운 배포를 강제로 수행한다.
        run: |
          aws ecs update-service \
          --cluster ${{ vars.AWS_CLUSTER }} \
          --service ${{ vars.AWS_SERVICE }} \
          --task-definition ${{ vars.AWS_TASK_DEFINITION }} \
          --force-new-deployment
cs

클러스터의 이름은 --cluster 인자로, 서비스의 이름은 --service로, 적절한 작업 정의는 --task-definition 인자로 지정하는데 이 이름은 GitHub 레포지토리의 Settings의 Secrets and variables의 Actions의 Variables에서 지정할 수 있다. 이 이름들은 4iz 인프라 포스트에서 ECS의 클러스터, 서비스, 작업 정의의 이름과 같아야 한다.

build-test-push

빌드, 테스트, 푸시 작업이 완료되고 나면 Amazon ECR의 사설 레포지토리에 다음과 같이 최신 이미지가 푸시된다!
27개의 E2E 테스트 통과, 걸린 시간은 16초!

latest 태그!

deploy

배포가 완료되면 ECS 클러스터의 서비스에 새로운 작업이 시작된다! ECS 롤링 업데이트의 실행되는 작업의 최소 퍼센트가 100이라서 새로운 컨테이너가 추가된다!

update: 2024.01.27

댓글 없음:

댓글 쓰기