ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Github Actions 에서 멀티 아키텍쳐 도커이미지 빌드 시 최적화
    삽질 2023. 4. 14. 01:00

    문제점

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

    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"

Designed by Tistory.