junglast
Published on

ArgoCD와 GitHub Actions를 이용해 쿠버네티스에 Next.js 애플리케이션 배포하기

진행 중인 프로젝트에서 Next.js로 작성한 React 애플리케이션을 컨테이너 기반으로 배포할 수 있는 CI/CD 환경을 구성해야 했습니다. 환경 구성시 필요한 사항들을 정리해보니 다음과 같았습니다.

  • 원격 Git 레포지토리를 기준으로 컨테이너 이미지를 빌드하고 이를 이미지 저장소에 업로드
  • 새 이미지가 업로드되면 이를 쿠버네티스 클러스터에 배포
  • 롤백이 쉽게 가능해야 함
  • Rolling update / Blue Green 등의 여러가지 배포 전략을 적용할 수 있으면 좋음

이를 위해 먼저 GitHub Actions로 컨테이너 이미지를 빌드하고 업로드했습니다. 별도의 CI 환경을 구성하지 않고도 레포지토리 내에서 쉽게 여러 작업을 수행할 수 있기도 하고, 미리 정의된 여러 action들을 이용해 대부분의 로직을 처음부터 직접 작성할 필요가 없기 때문이기도 합니다.

이후 ArgoCD를 이용해 빌드된 이미지를 쿠버네티스 클러스터에 반영하도록 구성했습니다. GitHub 레포지토리와 연동하여 레포지토리 내의 manifest 파일을 업데이트하는 것만으로 변경사항을 클러스터에 적용할 수 있고, 또 별도로 제공되는 UI를 통해 모니터링이나 롤백 등의 작업을 손쉽게 수행할 수 있다는 점 때문에 선택했습니다.

아래에서는 이와 같은 CI/CD 흐름을 구성하는 절차뿐만 아니라 작업 과정에서 혼동스러웠던 몇 가지 내용들을 조금 더 상세하게 서술하고자 합니다.

작업 흐름

전체적인 작업의 순서는 다음과 같습니다.

  1. GitHub Actions의 workflow가 trigger되면 Dockerfile을 기준으로 컨테이너 이미지 빌드
  2. 빌드한 이미지를 컨테이너 레지스트리에 업로드
  3. 쿠버네티스에서 사용할 manifest 파일을 위 2번 과정에서 업로드한 새 이미지를 이용하도록 업데이트
  4. 위 3번 과정에서 업데이트한 manifest 파일을 레포지토리의 별도의 branch로 push
  5. ArgoCD에서 위 4번 과정의 별도의 branch 에 새 manifest가 push되었음을 감지하고 이를 통해 쿠버네티스 클러스터 상태 업데이트

쿠버네티스 manifest 파일 작성

쿠버네티스의 오브젝트들의 구성 정보를 담고 있는 manifest 파일이 필요한데, 본 글의 프로젝트에서는 이 manifest yaml 파일을 프로젝트의 소스 코드가 있는 동일한 레포지토리의 /config 폴더에 작성했습니다.

참고로, 프로젝트의 소스 코드가 있는 레포지토리manifest 파일이 있는 레포지토리를 분리해 관리하는 방법도 일반적입니다. 이 경우, 수정한 manifest를 manifest 파일을 관리하는 전용 레포지토리에 push하고, ArgoCD를 이 레포지토리와 연동되도록 설정하면 됩니다.

GitHub Actions workflow 구성

사실상 대부분의 작업(위 과정 중 1~4번)은 GitHub Actions의 workflow에서 수행하게 됩니다. yaml 파일의 전체 내용은 다음과 같습니다.

name: deployment for my-music-app

on:
  workflow_dispatch:
    inputs:
      environment:
        description: "deployment environment"
        required: true
        default: "development"
        type: choice
        options:
          - development
          - production

permissions:
  contents: write
  issues: read
  checks: write
  pull-requests: write

jobs:
  build-and-push-image:
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

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

      - name: Set pnpm
        uses: pnpm/action-setup@v2

      - name: Set Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: "pnpm"
          cache-dependency-path: "./pnpm-lock.yaml"

      - name: Set Docker meta
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: |
            harbor.example.com/my-music-app
          tags: |
            type=raw,value=${{ inputs.environment }}-{{branch}}-{{sha}}

      - name: Login to Container Registry
        uses: docker/login-action@v2.1.0
        with:
          registry: harbor.example.com
          username: ${{ secrets.HARBOR_USERNAME }}
          password: ${{ secrets.HARBOR_TOKEN }}

      - name: Build and push to Container Registry
        uses: docker/build-push-action@v4.0.0
        with:
          context: .
          push: true
          pull: true
          tags: ${{ steps.meta.outputs.tags }}
          build-args: |
            APP_ENV=${{ inputs.environment }}

      - name: Set Kustomize
        uses: yokawasa/action-setup-kube-tools@v0.9.2
        with:
          kustomize: "3.7.0"

      - name: Update kubernetes manifest
        run: |
          git switch manifest@${{ inputs.environment }}
          git reset --hard ${{ github.ref_name }}
          cd config/deploy/overlays/${{ inputs.environment }}
          kustomize edit set image ${{ steps.meta.outputs.tags }}
          git config user.name "GitHub Actions"
          git config user.email "<>"
          git add .
          git commit -m 'k8s manifest for ${{ steps.meta.outputs.tags }}'
          git push origin manifest@${{ inputs.environment }} -f

1. workflow trigger 조건 설정

원격 레포지토리에 push가 일어날 때, 혹은 Pull Request가 merge될 때 등 다양한 trigger 조건을 설정할 수 있지만 본 예제에서는 Actions의 workflow_dispatch를 통해 수동으로 workflow를 trigger할 수 있게 구성했습니다. 추가로 inputs 필드를 이용해 배포 환경을 매개변수로 받을 수 있게 하였습니다.

2. 필요한 action 불러오기

이미지 빌드 등의 작업을 위해 필요한 action을 불러와 설정합니다. 빌드 시 이미지의 태그를 고유하게 지정하기 위해 GitHub Actions의 inputs 값과 커밋의 sha 값을 이용했습니다. 추가로, 여기서는 별도의 컨테이너 레지스트리를 사용하고 있어 docker/login-action action을 이용해 로그인을 해주었습니다.

3. 이미지 빌드 및 업로드

앞서 구성한 이미지 태그 및 GitHub Actions의 inputs를 통해 받은 환경을 기반으로 이미지를 빌드하고 이를 컨테이너 레지스트리에 업로드합니다. 빌드시 사용하는 Dockerfile의 내용은 아래와 같습니다.

FROM node:18-slim

WORKDIR /my-app

RUN corepack enable pnpm

COPY pnpm-lock.yaml .npmrc ./
RUN pnpm fetch

# Install dependencies
COPY package.json ./
RUN pnpm install --offline --prod

# Copy source code
COPY . .

.....

RUN pnpm build

EXPOSE 3000

ENTRYPOINT pnpm start

앱 빌드시 dependency를 설치하는 과정의 최적화를 위해 pnpm fetch 명령어와 pnpm install --offline --prod 명령어를 사용했습니다.

또, Next.js로 작성한 애플리케이션이므로 해당 이미지로 컨테이너가 띄워지면 pnpm start 명령을 통해 Next.js 서버를 시작해 줍니다.

4. manifest 업데이트

GitOps 형태의 배포에서 가장 핵심이 되는 부분입니다. kustomize를 이용해 작성된 기존 쿠버네티스 manifest 파일의 내용을 수정하여, 방금 workflow에서 새로 빌드한 이미지의 태그를 사용하도록 해야합니다.

kustomize edit set image ${{ steps.meta.outputs.tags }}

그 다음 수정한 파일을 소스코드가 있는 GitHub 레포지토리의 특정 branch(여기서는 manifest가 붙은 branch)에 업로드합니다.

레포지토리에 프로그램의 소스 코드가 push 되는 것, 혹은 이미지가 빌드되는 것과 상관 없이, ArgoCD는 이 manifest의 변화만을 바라보고 있습니다. ArgoCD가 새 manifest를 읽어와 이를 쿠버네티스 클러스터에 반영하면서, 새로운 태그를 가진 이미지를 레지스트리로 부터 내려받아 실행하게 되는 흐름입니다. 그런데 이 새 이미지는 GitHub Actions를 통해 새롭게 빌드된 이미지가 될 것입니다.

ArgoCD 설정

ArgoCD에 GitHub 레포지토리를 추가하고 ArgoCD Application을 생성합니다. Application 구성시 연결된 레포지토리의 manifest branch와 연결합니다(manifest 파일을 위한 별도의 레포지토리가 있다면 그 곳과 연결합니다).

이렇게 Application을 생성하고 나면, ArgoCD는 manifest가 위치한 레포지토리에서 manifest가 변경되면 이를 자동으로 감지하고 이를 쿠버네티스 클러스터에 적용합니다. 물론 변경 내용을 클러스터에 업데이트하는 과정은 Sync 버튼을 통해 수동으로 진행할 수도 있습니다.

쿠버네티스 오브젝트가 제대로 업데이트 되고 나면, ArgoCD 대시보드에서 해당 ArgoCD Application의 상태를 아래와 같이 시각적으로 볼 수 있습니다.