grep

Engineering

NHN Cloud Pipeline을 이용한 CD 파이프라인 구성하기!

NHN

2024년 4월 24일

원문에서 보기 ↗

cicd_thumbnail.png

1. Overview

배포 자동화는 소프트웨어 개발과 운영에서 중요한 부분을 차지합니다. 자동화된 배포 방식은 사람의 실수를 없애고, 빠른 주기로 소프트웨어 개발과 지속적 배포를 가능하게 하며, 팀의 생산성을 올리는 것에 많은 부분을 기여할 수 있습니다. 이외에도 많은 장점이 이 있기 때문에 소프트웨어 개발에서 배포 자동화는 필수적입니다. 이 글에서는 NHN Cloud 서비스와 Actions, Helm을 이용해 Kubernetes 환경의 CI/CD 파이프라인을 구성해보겠습니다.

2. Prerequisites

3. Architecture & Flow

CI 아키텍처

01_arch_ci.png

CD 아키텍처

02_arch_cd.png

플로우

  1. 애플리케이션 리포지터리에서 Actions Workflow를 통해 Helm State 리포지터리에 저장된 이미지 정보를 업데이트합니다.
  2. 해당 워크플로우에서 Helm State 리포지터리에 깃 태그(git tag)를 생성합니다.
  3. Helm State 리포지터리에서 태그가 생성되었다는 웹훅을 NHN Cloud Pipeline으로 전달합니다.
  4. NHN Cloud Pipeline은 웹훅을 수신하면 Bake 스테이지를 실행합니다.
  5. Bake 스테이지에서 차트와 values 파일을 통해 manifest를 렌더링합니다.
  6. Deploy 스테이지에서 Kubernetes로 배포합니다.

4. Helm Chart Repository

Helm 차트는 외부에 공개된 차트 리포지터리를 사용하거나, 직접 비공개 차트 리포지터리를 만들어서 사용할 수도 있습니다. 외부에서 생성된 Helm 차트 사용은 비교적 간단합니다. NHN Cloud Pipeline에 차트 리포지터리만 등록하고 Bake 스테이지에서 사용하면 됩니다. 외부 차트 등록은 References의 NHN Cloud Pipeline 사용자 가이드를 참고해 주세요. 이 글에서는 프라이빗 Helm 차트 관리에 대해 알아보겠습니다.

References:

💡 이 글에서는 GitHub을 Helm 차트 리포지터리로 활용합니다. Helm에서 제공하는 Actions을 사용해 GitHub 리포지터리를 Helm 차트 리포지터리로 사용할 수 있게 변환합니다.

GitHub 리포지터리 생성 및 워크플로우 작성

helm > chart-release-action을 참고해 차트 리포지터리로 사용합니다. 워크플로우 작성은 공식 문서 예제가 잘 작성되어 있으므로 참고해 주세요.

name: release helm charts

on:
  pull_request:
    types:
      - closed

jobs:
  release:
    if: github.event.pull_request.merged == true
    permissions:
      contents: write
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Configure Git
        run: |
          git config user.name "$GITHUB_ACTOR"
          git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
      - name: Install Helm
        uses: helm/setup-helm@v4.1.0
      - name: Run chart-releaser
        uses: helm/chart-releaser-action@main
        with:
          config: config.yaml
          skip_existing: true
          packages_with_index: false
          pages_branch: gh-pages
        env:
          CR_TOKEN: ${{ secrets.GH_PAT }}

💡예시에서는 CR_TOKEN의 값에 GitHub Personal Access Token을 사용합니다. 관리의 용이성을 위해 GitHubApp의 토큰 사용을 권장합니다. GitHubApp을 사용하는 방법은 References의 공식 문서를 참고해 주세요.

GitHub 리포지터리를 Helm 차트 리포지터리로 만들어주고, 선언된 차트 파일들을 패키징 해주는 액션입니다. 차트에 대한 PR Merged 이벤트가 발생하는 경우 gh-pages 브랜치에 차트 패키지를 생성합니다. 이후에 소개할 NHN Cloud Pipeline에서 gh-pages 브랜치를 참조합니다.

References:

Helm 설치

Helm 차트를 다루기에 앞서 Helm 설치가 필요합니다. References의 공식 문서를 참고하여 로컬에 Helm을 설치합니다.

References:

Helm 차트 생성

이 글에서는 차트 생성에 필수적인 요소들만 간단하게 소개하고 있습니다. Helm에서 제공하는 차트의 더 많은 구성 요소들은 References의 공식 문서를 참고하시고, 필요에 따라 추가해 주세요.

├── README.md
└── charts
    ├── foo
    └── bar

/chart 디렉토리 아래에 차트 폴더를 직접 생성합니다. 또는 아래 명령어로 자주 사용하는 파일들을 생성할 수 있습니다. 자세한 예제들이 포함되어 있어서 처음 Helm을 접하는 분들은 아래 명령어를 통해 생성하여 각 리소스를 참고해 보는 것을 권장합니다.

$ helm create charts/foo

다음으로, 간단한 애플리케이션을 배포하기 위한 차트로 가정하고, 많이 사용하는 필수적인 파일들만 생성해 보겠습니다. 아래와 같은 구조로 파일을 생성했습니다.

./charts/foo
├── Chart.yaml
├── templates
│   ├── configmap.yaml
│   ├── deployment.yaml
└── values.yaml

References:

Chart.yaml

apiVersion: v2
name: foo
version: 0.1.0

필수적인 내용만 작성합니다. 자세한 내용은 References의 공식 문서를 참고합니다.

References:

templates

간단한 템플릿을 예시로 작성해 보겠습니다. Kubernetes ConfigMap과 Deployment 리소스에 대한 템플릿입니다.

configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: foo
  namespace: {{ .Release.Namespace }}
data:
  ENVIRONMENT: {{ .Values.environment }}
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Chart.Name }}
  labels:
    app: {{ .Chart.Name }}
  namespace: {{ .Release.Namespace }}
spec:
  selector:
    matchLabels:
      app: {{ .Chart.Name }}
  template:
    metadata:
      {{- with .Values.deployment.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      labels:
        app: {{ .Chart.Name }}
        {{- with .Values.deployment.podLabels }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
    spec:
      {{- with .Values.deployment.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      containers:
        - name: {{ .Chart.Name }}
          image: {{ required "An image path for deployment is required!" .Values.deployment.image }}
          imagePullPolicy: {{ .Values.deployment.pullPolicy }}
          {{- with .Values.deployment.ports }}
          ports:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          livenessProbe:
            {{- toYaml .Values.deployment.livenessProbe | nindent 12 }}
          readinessProbe:
            {{- toYaml .Values.deployment.readinessProbe | nindent 12 }}
          {{- if .Values.deployment.startupProbe.enabled }}
          startupProbe:
            {{- toYaml .Values.deployment.startupProbe | nindent 12 }}
          {{-end }}
          resources:
            {{- toYaml .Values.deployment.resources | nindent 12 }}

템플릿에는 Built-in objects들이 있습니다. 위 예제에서의 .Release, .Chart, .Values 들이 해당됩니다. 자세한 내용은 References의 공식 문서를 참고합니다. .Values는 values.yaml 파일로부터 값을 읽어오거나 사용자가 전달할 수도 있습니다.

References:

values.yaml

environment: alpha
deployment:
  podAnnotations: {}
  podLabels: {}
  imagePullSecrets:
    - name: foo-registry
  image: ""
  pullPolicy: IfNotPresent
  ports:
    - name: http
      containerPort: 8080
      protocol: TCP
  resources: {}
  livenessProbe:
    httpGet:
      path: /health
      port: 8080
    timeoutSeconds: 5
    failureThreshold: 5
  readinessProbe:
    httpGet:
      path: /health
      port: 8080
    timeoutSeconds: 5
    failureThreshold: 5
  startupProbe:
    enabled: false
    httpGet:
      path: /health
      port: 8080
    periodSeconds: 10
    failureThreshold: 30

차트와 같이 작성하는 values.yaml은 기본값으로 사용됩니다.

References:

디버깅

아래의 명령어들을 통해 차트가 Best Practice를 따르고 있는지, 정상적으로 작성되었는지 확인할 수 있습니다.

$ helm lint
$ helm template --debug
$ helm install --dry-run --debug

💡이 가이드에서 다루지는 않지만 Actions에서 위 명령어들을 수행하여 패키지 이전 단계에서 Helm 차트를 검증하는 절차를 추가할 수도 있습니다. 🙂

References:

패키지

작성한 차트를 하나로 묶기 위한 단계입니다. 위에서 구성한 helm-chart-releaser에서 자동으로 패키징을 수행합니다. 아래의 명령어를 통해 직접 패키징을 해볼 수도 있습니다.

$ helm package ./charts/foo

실행 결과

Helm 차트를 릴리스하는 워크플로우를 실행한 결과입니다.

03_result.png

워크플로우가 정상적으로 수행되었다면 차트 리포지터리의 Releases와 Tag가 생성된 것을 확인할 수 있습니다.

04_workflow.png

References:

5. Helm State Repository

values.yaml

템플릿 기본값이 아닌 다른값을 사용하기 위해서는 템플릿에 값을 전달해야 합니다. --set 혹은 --set-string flag를 통해 값을 전달할 수도 있지만 쉽게 적용하기 위해 values.yaml을 작성하고 파일 형식으로 전달하도록 합니다.

먼저, 여러 배포 환경이 구성되어있다면 환경별 Helm values를 관리할 디렉터리와 파일을 생성합니다.

.  
├── README.md
└── foo 
    ├── README.md
    ├── alpha
    │   └── values.yaml
    └── beta
         └── values.yaml

정해진 틀은 없지만 위와 같이 {chart-name}/{environment} 구조로 작성하는 것을 권장합니다.

# /foo/alpha/values.yaml
environment: alpha
deployment:
  image: ""
  resources:
    limits:
      cpu: 1
      memory: 2Gi
    requests:
      cpu: 100m
      memory: 2Gi

위에서 비어있는 deployment.image 값은 이후 애플리케이션 리포지터리에서 수행하는 CI 과정에서 갱신합니다.

웹훅

여러 이벤트(Actions Workflow, PR Merged 등)로 인해 Helm State 리포지터리의 값이 변경되었다면 이를 NHN Cloud Pipeline에 알려서 자동으로 파이프라인을 수행할 수 있도록 해야 합니다. NHN Cloud Pipeline에서는 웹훅을 통해 파이프라인을 실행하는 기능을 지원하고 있습니다. References의 NHN Cloud Pipeline 사용자 가이드와 GitHub 공식 문서를 참고하여 태그 생성에 대한 웹훅을 보낼 수 있도록 설정합니다.

References:

워크플로우

애플리케이션에 변경사항이 있어서 이미지를 변경하는 경우 외에도 manifest를 수정해야하는 경우가 있습니다. values.yaml에 있는 다른 값들을 수정하는 경우도 자동 배포가 될 수 있도록 워크플로우를 작성해 봅시다.

name: deploy-foo-alpha

on:
  pull_request:
    types:
      - closed
    paths:
      - foo/alpha/**
      - .github/workflows/deploy-foo-alpha.yaml

env:
  REGISTRY_NAME: foo
  ENVIRONMENT: alpha
  TAG: ${{ github.sha }}

jobs:
  push-tag:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    environment: alpha
    steps:
      - name: Check out code
        uses: actions/checkout@v4
      - name: Push tag
        run: |
          git tag ${{ env.REGISTRY_NAME }}/${{ env.ENVIRONMENT }}/${{ env.TAG }}
          git push origin main --tags

PR Merged 이벤트가 발생하면 태그를 생성하는 간단한 워크플로우입니다.

실행 결과

04.5_.png

6. NHN Cloud Pipeline

References의 NHN Cloud Pipeline 사용자 가이드를 참고합니다.

배포 대상

배포 대상에 사용 중인 Kubernetes에 대한 정보를 추가합니다.

Reference:

차트 리포지터리

앞서 구성한 Helm 차트 리포지터리를 차트 저장소에 추가합니다.

05_chartrepo.png

차트 저장소 URL 값에 주의합니다.

차트 저장소 아이디와 토큰은 앞서 준비한 GitHub 계정 이름과 발급받은 토큰입니다.

Reference:

파이프라인 생성 & Bake 스테이지

namespace 값 설정에 주의합니다. 템플릿에는 Helm 차트 리포지터리 및 사용할 차트 정보를 입력합니다. 오버라이드에는 Helm State 리포지터리의 values.yaml 경로를 입력합니다.

Reference:

Deploy 스테이지

Manifest 소스를 외부입력으로 선택하여 Bake 스테이지의 결과물을 가져오도록 합니다.

Manifest 아티팩트를 Bake 스테이지에서 입력한 값으로 선택합니다.

06_deploystage.png

Reference:

자동실행 설정

이 파이프라인을 자동으로 실행하도록 설정합니다. Helm State 리포지터리에서 웹훅을 수신하면 실행하도록 설정할 수 있습니다.

예시:

KeyValue
자동 실행 유형GitHub
조직명 혹은 사용자 이름NHNCloud
프로젝트 이름helm-state
브랜치 또는 태그refs/tags/{APP_NAME}/alpha/(.+)+
시크릿SECRET

태그 예시: refs/tags/foo\/alpha\/(.+)+ 태그는 아래 워크플로우에서 생성하는 태그의 형식과 같아야 합니다. 예시에서는 중복된 태그를 생성하지 않기 위해 {이름}/{환경}/{SHA값}을 사용했습니다.

07_.png

자동실행 설정을 마치면 해당 파이프라인의 기본 정보에서 자동 실행 방지를 비활성화합니다.

Reference:

실행 결과

Helm State에서 NHN Cloud Pipeline으로 웹훅을 전송해, 자동으로 파이프라인을 수행합니다. 08_.png

7. Application Repository

워크플로우

아래 예시 워크플로우는 애플리케이션 리포지터리에서 수행합니다. 컨테이너 이미지를 빌드 & 푸시 후 Helm State 리포지터리에 있는 앱의 이미지를 변경해 주는 작업입니다.

워크플로우는 다음과 같이 작성합니다. 애플리케이션 언어에 따라 적절히 수정합니다.

name: CI(push)

on:
  pull_request:
    types:
      - closed

env:
  REGISTRY_HOST: ${{ secrets.REGISTRY_HOST }}
  REGISTRY_NAME: foo
  IMAGE_NAME: foo
  IMAGE_TAG: ${{ github.sha }}
  REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
  REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
  GH_PAT: ${{ secrets.GH_PAT }}
  ENVIRONMENT: alpha

jobs:
  build-and-push:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v4
        with:
          persist-credentials: false
      - name: Cache go modules
        uses: actions/cache@v3
        with:
          path: |
            ~/.cache/go-build
            ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-
      - name: Login to NCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY_HOST }}
          username: ${{ env.REGISTRY_USERNAME }}
          password: ${{ env.REGISTRY_PASSWORD }}
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: |
            ${{ env.REGISTRY_HOST }}/${{ env.REGISTRY_NAME}}/${{ env.IMAGE_NAME}}:latest
            ${{ env.REGISTRY_HOST }}/${{ env.REGISTRY_NAME}}/${{ env.IMAGE_NAME}}:${{ env.IMAGE_TAG }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
  update-image:
    runs-on: ubuntu-latest
    needs: [ build-and-push ]
    environment: alpha
    steps:
      - name: Check out code
        uses: actions/checkout@v4
        with:
          repository: NHNCloud/helm-state
          token: ${{ env.GH_PAT }}
      - name: Configure Git
        id: configure-git
        run: |
          git config user.name "$GITHUB_ACTOR"
          git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
      - name: Create new helm values
        id: substitute-helm-values
        uses: mikefarah/yq@master
        with:
          cmd: |
            yq e -i '.deployment.image = "${{ env.REGISTRY_HOST }}/${{ env.REGISTRY_NAME }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}"' ${{ env.REGISTRY_NAME }}/${{ env.ENVIRONMENT }}/values.yaml
      - name: Replace image tag and push substitute
        run: |
          git add .
          git commit -am "update ${{ env.REGISTRY_NAME }}/${{ env.ENVIRONMENT }} values"
          git tag ${{ env.REGISTRY_NAME }}/${{ env.ENVIRONMENT }}/${{ env.IMAGE_TAG }}
          git push origin main --tags

build-and-push

애플리케이션을 빌드 및 컨테이너화하고 NHN Container Registry(NCR)로 업로드하는 job입니다. GitHub Repository Secret에 워크플로우에서 사용할 NCR에 대한 정보를 등록합니다.

예시:

KeyValueDescription
REGISTRY_HOSTfoo-kr1-registry.container.nhncloud.com프로젝트에서 사용하는 NCR URL
REGISTRY_NAMEfoo레지스트리 이름
REGISTRY_USERbar발급받은 User Access Key ID
REGISTRY_PASSWORDbaz발급받은 Secret Access Key

References:

update-image

Create new helm values 단계에서는 .deployment.image의 값을 변경해 주고 있습니다. Replace image tag and push substitute 단계에서 생성하는 태그의 형식에 주의해 주세요. 파이프라인 자동실행 설정에 사용한 태그의 형식과 같아야 합니다.

References:

실행 결과

애플리케이션 리포지터리에서 도커 이미지를 빌드 및 NCR로 푸시하고, Helm State의 values.yaml 내 이미지 주소를 변경해주는 워크플로우를 수행한 결과입니다. 09_.png

NCR의 해당 레지스트리에 업로드된 이미지를 확인할 수 있습니다. 10_.png

Helm State의 values.yaml이 갱신된 결과입니다. 11_.png

8. Wrap Up

지금까지 CI/CD 파이프라인을 구성해보았습니다. 이 파이프라인을 통해 애플리케이션에서 변경사항이 발생했을 때 자동으로 Kubernetes 클러스터까지 배포할 수 있게 되었습니다! 유즈 케이스마다 조금씩 워크플로우나 파이프라인을 알맞게 수정해서 유연하게 사용할 수도 있습니다. 🚀

이 구조에서는 애플리케이션 리포지터리와 Helm State 리포지터리가 분리되어 있습니다. 따라서 애플리케이션 설정과 인프라 설정을 분리할 수 있는 장점이 있으며, 필요시 인프라 설정만 간편하게 Helm State 리포지터리에서 변경할 수 있다는 이점이 있습니다.