中庸
article thumbnail

문제점

핵심 : 하나의 머신에서 멀티 아키텍쳐 이미지 빌드하는 것은 에뮬레이터를 거쳐야 해서 매우 느리다.

20분....

SW Academy 프로젝트를 React App 과 SpringBoot 를 Dockerize 한 뒤 Docker-Compose 를 통해 GCP 로 배포하기 위해 Github Action 으로 CI/CD 파이프라인을 구축하고자 했다. CI/CD 구축에 문외한인 나는 가장 빠른 방법인 ChatGPT 에게 파이프라인을 작성해달라고 했다. Github Action 을 정의하는 YAML 이 직관적이라서 이해하는데 어려움은 없었다.

 

ChatGPT 로 얻은 초기 CI/CD 코드

더보기

name: CI/CD

on:
  push:
    branches:
      - main
      - Refactoring_ilmo

jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Log in to Docker Hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

      - name: Build and push Backend Docker image
        uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: dockerid/imagename:test
          platforms: linux/amd64,linux/arm64

      - name: Build and push Frontend Docker image
        uses: docker/build-push-action@v2
        with:
          context: .

          push: true
          tags: dockerid/imagename:test
          platforms: linux/amd64,linux/arm64

      - name: Setup Google Cloud SDK
        uses: google-github-actions/setup-gcloud@v0.2.1
        with:
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          project_id: ...

      - name: Deploy to GCP VM Instance
        run: |
          gcloud config set project ...
          gcloud compute ssh ${{ secrets.GCP_VM_USERNAME }}@instance-1 --zone asia-northeast3-a -- "sudo docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} -p ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}"
          gcloud compute ssh ${{ secrets.GCP_VM_USERNAME }}@instance-1 --zone asia-northeast3-a -- "sudo sudo aa-remove-unknown"
          gcloud compute ssh ${{ secrets.GCP_VM_USERNAME }}@instance-1 --zone asia-northeast3-a -- "sudo docker-compose -f ~./docker-compose.yml down"
          gcloud compute ssh ${{ secrets.GCP_VM_USERNAME }}@instance-1 --zone asia-northeast3-a -- "sudo docker-compose -f ~./docker-compose.yml pull"
          gcloud compute ssh ${{ secrets.GCP_VM_USERNAME }}@instance-1 --zone asia-northeast3-a -- "sudo docker-compose -f ~./docker-compose.yml up -d"

 

ChatGPT가 짠! 하고 완전 잘돌아가는 코드를 줬으면 모르겠지만, 세세한 설정도 다르고, 오탈자, GCP Instance 의 Capacity 가 작아 오류가 나는 등 아주 각양각색의 이유로 Github Action 실패를 매우 매우 많이 겪었다. 그리고 이 액션이 성공/실패하기까지 요구되는 시간이 짧으면 모르겠는데 20분이나 걸렸다! 그래서 뜬 눈으로 밤을 지새우며 계속 테스트를 했다. 그리고 마침내 성공하긴 했는데, 20분이나 걸리는 빌드 시간은 도저히 용납할 수가 없었다.

왜 이렇게 느린가? Github Action 돌아가는 것을 보니 다른 아키텍쳐의 도커 이미지를 빌드할 때 굉장히 시간이 많이 소요된다는 것을 확인할 수 있었다.

 

알아보니 Github Action 에서 기본으로 제공해주는 Runner 는 x86 아키텍쳐 인스턴스였고, docker buildx 를 통해 멀티 아키텍쳐 이미지를 빌드하려면 QEMU(Quick EMUlator) 라는 에뮬레이터를 통해서 빌드하기 때문에 굉장히 느린 것이었다. 정확하게 왜 느린지는 모르겠지만, 잠시 추론해본걸로는 ARM CPU와 x86 CPU 의 Instruction 을 Translate 한 뒤 빌드해야 하니 그 과정에서 느려지는게 아닐까 생각했다.

해결책

요약 : arm64 이미지는 arm64 머신에서, amd64 이미지는 amd64 머신에서 빌드 후 manifest 수정을 통해 멀티 아키텍쳐 이미지를 만들자!

문제를 파악하니 해결책은 단순했다. 아키텍쳐별로 도커 이미지를 빌드하면 되겠다고 생각했다. 당연히 도커에는 manifest 라는 것을 이용해 digest 를 합친 멀티 아키텍쳐 이미지를 설정하는 기능이 있었다. 그리고 Github Action 도 self-hosted Runner 라는 것을 제공했다.  그래서 Github Action 이 기본으로 제공하는 x86 Instance 와 self-hosted runner 를 둘 다 활용하여 도커 이미지를 빌드하는 YAML 을 작성했다. manifest 는 docker buildx imagetools 를 사용해서 손쉽게 조정할 수 있었다. 

 

이와 더불어 ChatGPT 가 처음 짜줬던 모든 Sequence 가 하나의 Job 안에 들어있는 것을 Job 을 분리하여 Concurrent 하게 실행하도록 최적화까지 진행하니, 초기 20분가량 걸리던 시간을 7분대로 줄일 수 있었다!!

구글에 이와 같이 멀티 아키텍쳐 이미지 빌드하는 법을 검색해보면 다 그냥 docker buildx 의 --platform 옵션을 쓰라고만 돼있어서 글을 남긴다.

7분!

 

아래는 향상된 YAML 설정이다.

더보기

name: CI/CD

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
    types: [closed]

jobs:
  build_backend_amd64:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Log in to Docker Hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

      - name: Build and push Backend Docker image
        uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: dockerid/imagename:test-amd64
          platforms: linux/amd64

  build_frontend_amd64:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Log in to Docker Hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

      - name: Build and push Frontend Docker image
        uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: dockerid/imagename:test-amd64
          platforms: linux/amd64

  build_frontend_arm64:
    runs-on: [self-hosted, linux, ARM64]
    steps:
      - name: Checkout Code
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Log in to Docker hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

      - name: Cleanup Storage
        run: |
          docker system prune -a -f --volumes

      - name: Build and push Frontend Docker image
        uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: dockerid/imagename:test-arm64
          platforms: linux/arm64

  build_backend_arm64:
    runs-on: [self-hosted, linux, ARM64]
    needs: [build_frontend_arm64]
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Log in to Docker Hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

      - name: Cleanup Storage
        run: |
          docker system prune -f -a --volumes

      - name: Build and push Backend Docker image
        uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: dockerid/imagename:test-arm64
          platforms: linux/arm64

  merge_multi_arch_images:
    runs-on: ubuntu-latest
    needs: [build_backend_amd64, build_frontend_amd64, build_backend_arm64]
    continue-on-error: true
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Log in to Docker hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_HUB_USERNAME }}
          password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}

      - name: Merge images
        run: |
          docker buildx imagetools create -t dockerid/imagename:test dockerid/imagename:test-arm64 dockerid/imagename:test-amd64
          docker buildx imagetools create -t dockerid/imagename:test dockerid/imagename:test-arm64 dockerid/imagename:test-amd64

  deploy:
    runs-on: ubuntu-latest
    needs: [merge_multi_arch_images]
    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Setup Google Cloud SDK
        uses: google-github-actions/setup-gcloud@v0.2.1
        with:
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          project_id: ...

      - name: Deploy to GCP VM Instance
        run: |
          gcloud config set project ...
          gcloud compute ssh ${{ secrets.GCP_VM_USERNAME }}@instance-1 --zone asia-northeast3-a -- "mkdir -p ~/nginx"
          gcloud compute scp --recurse ./nginx ${{ secrets.GCP_VM_USERNAME }}@instance-1:~/ --zone asia-northeast3-a
          gcloud compute scp ./docker-compose.yml ${{ secrets.GCP_VM_USERNAME }}@instance-1:~/WProject/docker-compose.yml --zone asia-northeast3-a
          gcloud compute ssh ${{ secrets.GCP_VM_USERNAME }}@instance-1 --zone asia-northeast3-a -- "sudo docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} -p ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}"
          gcloud compute ssh ${{ secrets.GCP_VM_USERNAME }}@instance-1 --zone asia-northeast3-a -- "sudo aa-remove-unknown"
          gcloud compute ssh ${{ secrets.GCP_VM_USERNAME }}@instance-1 --zone asia-northeast3-a -- "sudo docker system prune -f -a --volumes"
          gcloud compute ssh ${{ secrets.GCP_VM_USERNAME }}@instance-1 --zone asia-northeast3-a -- "sudo docker stack deploy -c ~./docker-compose.yml app"

profile

中庸

@짱일모

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!