GitHub Actions에서 멀티아치 Docker 이미지 빌드 (amd64 + arm64)
ARM CI 러너에서 빌드한 이미지가 x86 EC2에서 exec format error로 죽을 때, QEMU + buildx로 한 태그에 두 아키 manifest를 묶어 해결한 과정.
2026. 06. 24.
GitHub ActionsDockerECRCI/CDAWS
CI가 ARM 러너에서 linux/arm64만 빌드해 ECR로 올리던 상태. x86 EC2에서는 못 돌아가니 한 태그(:main)로 두 아키 모두 커버하도록 수정한 기록.
왜 이게 필요한가
ECR 이미지 manifest를 까보면 arm64 한 개뿐이었다:
aws ecr batch-get-image --repository-name meeny-backend --region ap-northeast-2 \
--image-ids imageTag=main --query 'images[0].imageManifest' --output text \
| python3 -m json.tool- t3.micro(x86)에서
docker pull+docker run하면exec format error로 즉사. - ARM 인스턴스(t4g.x)는 12개월 무료티어 대상이 아님 (2023-12-31에 종료). 서울에서 진짜 무료는 t2/t3.micro뿐.
선택지 비교
| 옵션 | 비용 | 추가 작업 | 결과 |
|---|---|---|---|
| (A) 그대로 두고 t4g.small ARM 쓰기 | 약 $12/월 | 없음 | 무료 아님 |
| (B) CI를 멀티아치로 고치고 t3.micro | $0 (12개월 프리티어) | CI yml 수정 + 한 번 더 빌드 | 진짜 공짜 |
| (C) t4g.micro (1GB ARM) | 약 $6/월 | 없음 | 무료 아님 |
→ B 채택.
변경 내용
/.github/workflows/ci.yml build-and-push 잡:
Before
runs-on: ubuntu-24.04-arm
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: aws-actions/configure-aws-credentials@v4
with: { ... }
- uses: aws-actions/amazon-ecr-login@v2
- uses: docker/build-push-action@v6
with:
platforms: linux/arm64 # ← arm64만After
runs-on: ubuntu-latest # ← x86 러너
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3 # ← 추가: arm64 에뮬레이션
- uses: docker/setup-buildx-action@v3
- uses: aws-actions/configure-aws-credentials@v4
with: { ... }
- uses: aws-actions/amazon-ecr-login@v2
- uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64 # ← 멀티아치어떻게 동작하나
setup-qemu-action은 binfmt_misc에 QEMU 사용자 모드 에뮬레이터를 등록한다. 이러면 x86 호스트에서도RUN ./gradlew bootJar같은 ARM 명령을 에뮬레이션으로 실행 가능.buildx가 두 platform에 대해 각각 빌드한 뒤, multi-arch manifest list 하나로 묶어 ECR에 push한다. 태그:main은 그 manifest list를 가리킨다.- Docker가 pull할 때 자기 호스트 아키에 맞는 manifest를 골라서 받는다. 사용자는 한 태그만 쓰면 된다.
비용/속도 트레이드오프
- QEMU 에뮬레이션은 네이티브 빌드 대비 수~수십 배 느리다. Spring Boot 빌드(gradlew bootJar) 같은 JVM 작업은 비교적 영향이 적은 편 (CPU-heavy 코드보단 I/O/JVM 지배적).
- 대안: x86 러너에서 amd64는 네이티브로, arm64는 ARM 러너에서 네이티브로 빌드 후 manifest를 합치는 멀티 잡 구조. 더 빠르지만 yaml이 두 배. 본 프로젝트는 빌드 시간이 충분히 짧아서 QEMU로 단순화.
main 브랜치 보호 우회 못 함 → PR로 갈음
- 로컬에서
git push origin main시 hook이 막는다 (사내 정책 또는 harness 정책). - 별도 브랜치
ci/multi-arch-image만들어 push →gh pr create→ 머지. - CI yml의
build-and-push는if: github.event_name == 'push' && github.ref == 'refs/heads/main'이라 PR 단계에서는 안 돌고, 머지 후 main에 푸시될 때 1회 실행된다.
git checkout -b ci/multi-arch-image
git add .github/workflows/ci.yml
git commit -m "ci: build multi-arch image (amd64+arm64) so x86 EC2 can run it"
git push -u origin ci/multi-arch-image
gh pr create --base main --head ci/multi-arch-image --title "..." --body "..."
gh pr merge <PR#> --squash --delete-branch
gh run watch <RUN_ID> --exit-status검증
머지 후 main의 CI가 끝나면 manifest에 두 아키가 보여야 한다:
aws ecr batch-get-image --repository-name meeny-backend --region ap-northeast-2 \
--image-ids imageTag=main --query 'images[0].imageManifest' --output text \
| python3 -c "import json,sys; m=json.load(sys.stdin); \
[print(x['platform']['architecture'], x['platform']['os']) for x in m['manifests']]"
# expected:
# amd64 linux
# arm64 linux
# unknown unknown (← attestation manifest, 무시 가능)
# unknown unknown학습 포인트
- multi-arch manifest = manifest list (OCI image index). 단일 이미지가 아니라 "아키별 이미지로 가는 포인터 모음".
imageManifestMediaType이application/vnd.oci.image.index.v1+json이면 multi-arch. unknown/unknown매니페스트는 SBOM/Provenance attestation. buildx가 자동으로 만들어 같이 push한 메타데이터. 무시해도 된다.- GitHub Actions ARM 러너(
ubuntu-24.04-arm)는 네이티브 ARM. 단일 아키만 빌드할 거면 빠른 옵션이지만 멀티아치 같이 굴리려면 x86 + QEMU가 더 단순하다.