이미지를 ECR에 올리는 것까지는 워크플로 몇 줄이면 된다. 그런데 이미지가 올라갔다고 클러스터가 바뀌지는 않는다. ArgoCD가 지켜보는 건 레지스트리가 아니라 Kubernetes manifest가 들어 있는 별도의 GitHub 저장소이기 때문이다.
ArgoCD의 Image Updater라는 것을 사용하면 컨테이너 레지스트리의 이미지 변경을 감지하여 또 다른 방식으로 자동 배포 아키텍쳐를 구현해볼 수도 있다.
SaaS 프로젝트를 하게되면서 실제로 사용해본 경험으로는 분명 유용한 기능이었지만 몇가지 아쉬운 부분도 있었다.
Git Push 충돌이 발생하기 시작. 이에 대한 조치 방법도 있었지만, 그 당시 조치는 보류. 어쨌든 공수가 더 필요한 상황이었고, 만약 이대로 놔두면 서비스 규모가 커지고 협업 인원이 많아질수록 발생 빈도도 높아질 것이 예상되었음
기본적인 설정 및 아키텍쳐로 구현했을 때에는 테넌트 최초 온보딩시에는 manifest에 설정된 버전으로 최초 배포 후 이미지 업데이터가 다시 새로운 신규 이미지를 찾아 업데이트하게되는 불필요한 흐름으로 구현되었다. 이 또한 조치하긴 했으나 결국 추가적인 공수가 발생된 포인트.
Java 애플리케이션 Repo의 GitHub Actions에 필요한 일은 세 가지였다.
애플리케이션을 Docker 이미지로 빌드한 뒤 ECR Private repository에 푸시
애플리케이션 저장소와는 별개인 manifest 저장소에서 특정 파일의 image tag를 갱신
manifest 파일이 바뀌면 ArgoCD가 그걸 감지해 Kubernetes 클러스터에 동기화
배포를 일으키는 건 이미지 푸시가 아니라 manifest 저장소에 들어가는 커밋 하나. 이 글에서는 그 커밋을 워크플로 마지막 job에서 만드는 방법을 중심으로, Docker Hub에 올리던 이전 설정과 멀티 플랫폼 빌드까지 정리한다.
flowchart LR
A[Repository develop push] --> B[build: Maven 패키징]
B --> C[docker: 이미지 빌드 후 ECR 푸시]
C --> D[update-deployment: manifest 저장소 이미지 태그 커밋]
D --> E[ArgoCD 변경 감지]
E --> F[Kubernetes Cluster Sync]세 개의 job이 결과물을 차례로 넘긴다
아래는 Spring Boot 애플리케이션 저장소의
.github/workflows/action.ymlGitHub Actions가 ECR에 접근할 때는 OIDC 방식 사용
name: saas-oil application deploy
on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
permissions:
id-token: write # This is required for requesting the JWT
env:
APP_NAME: saas-oil
BRANCH_NAME: develop
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package --file pom.xml
# 다음 job으로 jar를 넘긴다
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{env.APP_NAME}}-application-jar
path: target/*.jar
- name: List files in the working directory - 1
run: ls target/
docker:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: ${{env.APP_NAME}}-application-jar
path: target/
# OIDC 인증
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@v3
with:
role-to-assume: ${{secrets.AWS_OIDC_ROLE}}
role-session-name: ecrRoleSession
aws-region: ${{secrets.AWS_REGION}}
# ECR 로그인
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
# Docker 이미지 빌드 & ECR에 푸시. 태그는 <브랜치>-<커밋 SHA>
- name: Build Docker image And Push to AWS ECR
run: |
docker build . -t ${{ secrets.AWS_ECR_URL }}/${{ env.APP_NAME }}:${{ env.BRANCH_NAME }}-${{ github.sha }}
docker push ${{ secrets.AWS_ECR_URL }}/${{ env.APP_NAME }}:${{ env.BRANCH_NAME }}-${{ github.sha }}
update-deployment:
needs: docker
runs-on: ubuntu-latest
steps:
# 앱 저장소가 아니라 manifest 저장소를 checkout한다
- name: Checkout Kubernetes manifest repository
uses: actions/checkout@v2
with:
repository: ${{ vars.SAAS_KUBE_REPOSITORY }}
token: ${{ secrets.SAAS_GITHUB_TOKEN }}
ref: ${{ env.BRANCH_NAME }}
# 이미지 줄을 새 태그로 바꾸고 커밋·푸시
- name: Update k8s oil file
run: |
sed -i 's|${{ secrets.AWS_ECR_URL }}/.*|${{ secrets.AWS_ECR_URL }}/${{ env.APP_NAME }}:${{ env.BRANCH_NAME }}-${{ github.sha }}|' kube/${{env.BRANCH_NAME}}/${{env.APP_NAME}}.yaml
git config user.name ${{ vars.SAAS_GITHUB_NAME }}
git config user.email ${{ vars.SAAS_GITHUB_EMAIL }}
git add kube/${{env.BRANCH_NAME}}/${{env.APP_NAME}}.yaml
git commit -m "Update image tag to ${{ github.sha }}"
git push
job은 needs로 이어져 있다. build가 만든 jar를 artifact로 올리면 docker가 내려받아 이미지로 굽고, docker가 끝나야 update-deployment가 실행된다. 이미지 태그를 develop-<커밋 SHA>로 붙인 덕분에, manifest 저장소 커밋만 보고도 어떤 앱 커밋이 배포됐는지 알 수 있다.
다른 저장소를 고치려면 토큰이 따로 필요하다
핵심은 update-deployment job.
actions/checkout은 기본으로 워크플로가 도는 저장소를 받는다. repository에 manifest 저장소를 적고, 워크플로 기본 토큰은 다른 저장소에 쓸 권한이 없어서 token에 그 저장소에 푸시할 수 있는 토큰을 따로 넣어야 한다. 여기서는 SAAS_GITHUB_TOKEN secret이 그 역할이다. ref로 develop 브랜치를 받는다.
파일 수정은 sed 한 줄이다. kube/develop/saas-oil.yaml에서 <ECR URL>/로 시작하는 부분부터 줄 끝까지를 새 이미지 주소로 통째로 바꾼다. 구분자를 / 대신 |로 쓴 건 이미지 주소 안의 /와 부딪히지 않게 하려는 것이다. 커밋 작성자를 설정한 뒤 바뀐 파일만 add해 커밋·푸시한다. 이 푸시가 ArgoCD가 기다리던 변경이다.
주의할 점
이 워크플로는
PR에도 돈다..on에push와pull_request가 둘 다 있어서, develop을 향한 PR이 열리기만 해도 이미지가 푸시되고 manifest 태그가 바뀐다. 머지 전 코드가 develop 환경에 배포될 수 있다. PR에서는build만 돌리고 나머지 job에if: github.event_name == 'push'를 거는 식으로 나누는 편이 안전하다.manifest 저장소 토큰은 쓰기 권한을 가진 별도 secret이다. 앱 저장소 쪽 권한 설정(
permissions)과는 따로 관리한다.
Docker Hub Private repo에 올리던 이전 설정
ECR로 옮기기 전에는 Docker Hub Private repo에 푸시했다. 그때 쓰던 docker job 일부다.
docker:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download artifact
uses: actions/download-artifact@v2
with:
name: spring-application-jar
path: build/libs
- name: Build Docker image
run: docker build . -t ${{ secrets.DOCKER_REPO }}:tag-${{ github.sha }}
- name: Log in to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push Docker image to DockerHub
run: docker push ${{ secrets.DOCKER_REPO }}:tag-${{ github.sha }}
DOCKER_USERNAME: 도커 이미지가 올라갈 저장소에 접근할 사용자 이름DOCKER_PASSWORD: 비밀번호가 아니라 그 사용자의 Access token. Docker Hub 로그인 후 My Account → Security →New Access Token에서 발급한다.
ECR은 OIDC로 역할을 받아 로그인하니 장기 자격 증명을 secret에 넣어 둘 필요가 없다. 반면 Docker Hub는 토큰을 secret으로 들고 있어야 한다.
빌드와 푸시를 한 번에, 여러 아키텍처로
위 설정은 모두 docker build와 docker push를 따로 부른다. buildx를 쓰면 한 명령으로 끝난다.
docker buildx build --platform=linux/amd64,linux/arm64 -t YOUR_DOCKERHUB_ID/YOUR_TAG_NAME . --push
빌드 명령 뒤에 --push를 붙이면 별도 push 없이 빌드와 동시에 이미지를 푸시한다. --platform은 amd64와 arm64 양쪽에서 쓸 수 있는 이미지로 빌드하라는 뜻
아키텍처가 왜 문제가 되냐면, runs-on: ubuntu-latest 러너에서 docker build로 만든 이미지는 기본적으로 amd64용이다. 이미지를 받는 컴퓨터가 x86-64라는 전제가 깔린 셈이다. 이 이미지를 로컬 테스트하겠다고 Apple Silicon 맥북에서 pull해 쓰면 exec format error를 볼 수 있다. Docker Desktop은 에뮬레이션으로 amd64 이미지를 띄워 주기도 해서, 환경에 따라서는 플랫폼 불일치 경고만 나오고 돌아갈 수도 있다.
맥에서도 쓸 이미지라면 arm64까지 함께 빌드하면 된다. 워크플로에서는 docker/setup-buildx-action과 docker/build-push-action으로 바꾼다.
docker:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: ${{env.APP_NAME}}-application-jar
path: target/
# OIDC 인증
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@v3
with:
role-to-assume: ${{secrets.AWS_OIDC_ROLE}}
role-session-name: ecrRoleSession
aws-region: ${{secrets.AWS_REGION}}
# ECR 로그인
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
# Docker 이미지 빌드 & ECR에 푸시
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.AWS_ECR_URL }}/${{ env.APP_NAME }}:${{ env.BRANCH_NAME }}-${{ github.sha }}
platforms: linux/amd64,linux/arm64
Docker 공식 예제는 Buildx 설정 앞에 docker/setup-qemu-action을 둔다. amd64 러너에서 arm64 이미지를 빌드하다 Dockerfile의 RUN 단계를 실행하려면 QEMU 에뮬레이션이 필요하기 때문.