본문 바로가기

Side Project

React+Flask 어플리케이션: 코드부터 Jenkins 배포까지 (CI/CD) - 2

728x90
반응형

이번 블로그는 CI/CD 1탄 에서 작성한 어플리케이션을 쿠버네티스 로 배포하는 과정을 정리했는데요, 진행한 내용은 아래와 같습니다.🚀

 

1. GitHub Actions가 main push를 감지 → Backend/Frontend 이미지를 빌드하여 GHCR(GitHub Container Registry) 로 푸시

2. Actions가 Jenkins Webhook (HTTP POST) 호출 → Jenkins job 시작

3. Jenkins가 GHCR로 로그인 → GHCR 에 있는 이미지를 Pull.

4. Jenkins가 Kubernetes에 docker-registry secret(ghcr-secret)과 unsplash-secret을 생성(또는 업데이트).

5. Jenkins가 Helm으로 image-gallery 차트를 ./helm/image-gallery 경로에서 values-*.yaml로 배포.

6. Kubernetes가 Secret을 Pod로 주입 → 앱은 런타임에 ACCESS_KEY 환경변수로 사용.

 

이번 블로그는 아래 준비사항을 세팅하는 과정부터 시작합니다.


준비사항 (Prerequisites)

이 프로젝트를 실행하기 전에 로컬 환경과 클라우드 환경에서 몇 가지 필수 도구와 계정을 준비해야 합니다.
아래 순서대로 진행하면 됩니다.

  • 로컬 머신: Docker, kubectl, Minikube, helm 설치
  • Jenkins 서버(로컬 도커로 띄워도 됨) — Jenkins에 kubectl, helm, docker CLI가 동작해야 함
  • GitHub 리포지토리(코드 + .github/workflows), GHCR 사용(또는 원하는 레지스트리)
  • Jenkins에 Credentials 추가

1. 필수 툴 설치

  1. Docker
  2. kubectl (Kubernetes CLI)
  3. Minikube 설치
    • 설치:
      brew install minikube   # Mac + Homebrew
    • 설치 확인:
      minikube version
  4. Helm (Kubernetes Package Manager)
    • 설치:
      brew install helm
    • 설치 확인:
      helm version

2. 계정 & 토큰 준비

  1. GitHub
    • 리포지토리 준비 (이전 포스팅에서 사용하던 거 그대로 사용 예정,  예시 - ImageGallery)
  2. GHCR (GitHub Container Registry) Personal Access Token
    • GitHub → Settings → Developer settings → Personal Access Token (Classic) 생성
    • 권한:  read:packages,  write:packages
    • Jenkins에 ghcr-credentials로 Credentials 등록 및 Github Secrets 으로 등록 (나의 경우 MY_TOKEN 으로 등록됨)
  3. Unsplash API Key

3. Kubernetes 클러스터 준비 (Minikube)

  1. 클러스터 생성 및 확인
minikube start --driver=docker
kubectl get nodes

 

결과는 아래처럼 나옵니다.

NAME       STATUS   ROLES           AGE   VERSION
minikube   Ready    control-plane   11h   v1.33.1

4. Jenkins Credentials 설정

Jenkins → Manage Jenkins → Credentials → Global 에 아래 두 가지 추가:

  1. ghcr-credentials
    • Kind: Username with password
    • Username: GitHub ID
    • Password: 2번 과정에서 진행한 GHCR Personal Access Token
  2. UNSPLASH_KEY (이전 포스팅에서 저장함)
    • Kind: Secret text
    • Secret: Unsplash API Key

 여기까지 완료되면, GitHub Actions → GHCR Push → Jenkins CD → Kind 배포를 실행할 모든 준비가 끝납니다.


이제 CI (빌드 & 푸시)  CD (배포) 를 차례대로 설정해 보겠습니다.

1. GitHub Actions (CI): 이미지 빌드 & GHCR push 

먼저, GitHub 리포지토리에 .github/workflows/ci.yml 파일을 추가합니다.
이 워크플로우는 main 브랜치에 push가 발생하면 백엔드와 프론트엔드 이미지를 GHCR로 빌드 & 푸시합니다.

name: Build and Push Docker Images to GHCR

on:
  push:
    branches: ["main"] # main 브랜치에 push 될 때 실행

env: # jenkins 빌드 트리거를 위해 환경변수 지정 (아래에서 사용할 예정)
  JENKINS_JOB: gallery_app 
  JENKINS_PARAMS: environment=dev

jobs:
  build:
    runs-on: ubuntu-latest

    permissions:
      contents: read
      packages: write # GHCR에 푸시하려면 필요

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
	
      # GHCR 에 로그인
      - name: Log in to GitHub Container Registry 
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.MY_TOKEN }} # Github에 저장한 MY_TOKEN secret 사용

      # Frontend 이미지 빌드 & 푸시
      - name: Build and push frontend
        uses: docker/build-push-action@v4
        with:
          context: ./frontend
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/frontend:latest

      # Backend 이미지 빌드 & 푸시
      - name: Build and push backend
        uses: docker/build-push-action@v4
        with:
          context: ./backend
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/backend:latest

      # Jenkins 빌드 트리거
      - name: Trigger Jenkins Job
        uses: appleboy/jenkins-action@master 
        with:
          user: ${{ secrets.JENKINS_USER }} # Github에 저장한 JENKINS_USER secret 사용
          token: ${{ secrets.JENKINS_TOKEN }} # Github에 저장한 JENKINS_TOKEN secret 사용
          url: ${{ secrets.JENKINS_HOST }} # Github에 저장한 JENKINS_HOST secret 사용
          job: ${{ env.JENKINS_JOB }} # 위에서 지정한 environment 값
          parameters: ${{ env.JENKINS_PARAMS }}

 

  • Trigger Jenkins CD 단계에서 Jenkins에 Webhook을 호출 → Jenkins Job이 자동 실행됩니다.
    • JENKINS_HOST의 경우 Jenkins 가 실행되는 주소입니다. 저의 경우 localhost:8080 에 띄웠기 때문에 이 local 주소를 외부에서 접근가능하도록 노출해야 하는데요, 이는 손쉽게 Ngrok 을 사용해서 할 수 있습니다. (참고: https://innerjoin.tistory.com/25 )
    • 이 외에 다른 매개변수들은 https://github.com/appleboy/jenkins-action  를 참고하면 됩니다.
 

GitHub - appleboy/jenkins-action: GitHub Action that trigger Jenkins job.

GitHub Action that trigger Jenkins job. Contribute to appleboy/jenkins-action development by creating an account on GitHub.

github.com

 

 


2. Jenkinsfile (CD): Helm으로 Kubernetes 배포

Jenkins에서 실행할 Jenkinsfile은 아래와 같습니다. 이전 빌드된 작업물은 workspace 에 기록되는데 이로 인해 이전 작업물이 캐시되어 새 버전이 제대로 반영이 안되는 경우가 있었습니다. 저는 첫 번째 단계롤 workspace를 모두 삭제하도록 구성했습니다.

 

workspace 초기화  → GHCR 로그인 → K8s Secret 생성(ghcr + unsplash) → Helm 배포 -> pod 가 잘 떴는지 확인 (선택) 순으로 실행합니다. 

 

그리고, 이번 프로젝트에서는 dev, stg, prod 각각에 대한 환경을 구성해서 배포해 보고자 했는데요, 아래 파이프라인을 보시면 parameters 로 3가지 환경을 선택하도록 했습니다. 이렇게 구성하면 빌드 시 Jenkins에서 파라미터를 선택하라고 나옵니다.

Jenkinsfile

pipeline {
    agent any

    parameters {
        choice(name: 'ENV', choices: ['dev','stg','prod'], description: '배포 환경')
    }

    environment {
        PATH = "/usr/local/bin:/opt/homebrew/bin:$PATH"
        REGISTRY = "ghcr.io"
        BACKEND_IMAGE = "ghcr.io/eunhwa99/backend:latest"
        FRONTEND_IMAGE = "ghcr.io/eunhwa99/frontend:latest"

        // Jenkins Credentials
        GHCR = credentials('ghcr-credentials')      // usernamePassword → GHCR_USR, GHCR_PSW
        UNSPLASH = credentials('UNSPLASH_KEY')      // string → UNSPLASH
    }

    stages {
        stage('Clean Workspace') {
            steps {
                deleteDir() // 현재 workspace 전체 삭제
                git branch: 'main', url: 'https://github.com/eunhwa99/ImageGallery.git'
            }
        }
        stage('Login to GHCR') {
            steps {
                sh '''
                  echo $GHCR_PSW | docker login $REGISTRY -u $GHCR_USR --password-stdin
                '''
            }
        }

        stage('Create Kubernetes Secrets') {
            steps {
                // GHCR Secret (private image pull)
                sh '''
                  kubectl create secret docker-registry ghcr-secret \
                    --docker-server=$REGISTRY \
                    --docker-username=$GHCR_USR \
                    --docker-password=$GHCR_PSW \
                    --docker-email=none \
                    --dry-run=client -o yaml | kubectl apply -f -
                '''

                // Unsplash API Secret
                sh '''
                  kubectl create secret generic unsplash-secret \
                    --from-literal=UNSPLASH_ACCESS_KEY=$UNSPLASH \
                    --dry-run=client -o yaml | kubectl apply -f -
                '''
            }
        }

        stage('Deploy with Helm') {
            steps {
                sh '''
                  helm upgrade --install image-gallery ./helm/image-gallery \
                    -f ./helm/image-gallery/values-${ENV}.yaml 
                '''
            }
        }

        stage('Verify Pods') {
            steps {
                sh '''
                  kubectl get pods
                  kubectl describe pod -l app=image-gallery-backend
                  kubectl describe pod -l app=image-gallery-frontend
                '''
            }
        }
    }
}
  • Create Kubernetes Secret 에서  Unsplash API Secret  을 설정하는 부분에서 에러가 났었습니다.
    • --from-literal=UNSPLASH_ACCESS_KEY=$UNSPLASH  이 부분에서 UNSPLASH_ACCESS_KEY  은 API Key 값을 전달받을 곳에서 사용할 변수이고, UNSPLASH 은 Jenkins에서 API Key 가 저장된 변수명입니다. (credentials에 저장한 secret text 의 name) -> UNSPLASH_ACCESS_KEY 는 Backend 에서 전달받을 예정이고, 따라서 Backend 코드에서 UNSPLASH_ACCESS_KEY = os.getenv("UNSPLASH_ACCESS_KEY") 이런식으로 전달 받아야 합니다.  (--from-literal=UNSPLASH=$UNSPLASH 이렇게 설정했다가 build 는 잘되는데 백엔드에서 응답이 없어서 애 먹었습니다 ㅠㅠ)
  • Deploy with Helm 단계에서, 어떤 파라미터(dev, stg, prod) 인지에 따라 values-dev.yml, values-stg.yaml, values-prod.yaml 이 선택되도록 Jenkins 파이프라인을 구성했습니다.

Github에서 Webhook 이벤트를 전달받으려면 아래와 같이 Github project 추가 및 Github hook trigger 설정이 필요합니다. 추가로 위에서 설명한 Parameters 부분도 (dev, stg, prod 매개변수) 아래 처럼 매개변수 설정이 필요합니다.

 


3. Helm values 설정

 

기존 프로젝트 구조에서 아래 helm 부분이 추가되었습니다. (위 Jenkins 파라미터로 dev, stg, prd 를 설정했는데요, 우선 dev 용 파일인 values-dev.yaml 만 생성했습니다.) 각 파일들은 아래와 같이 정의되어 있습니다. (처음에 ~.yml 로 파일 형식을 지정했다가 계속 실패했었습니다.ㅎㅎ 이 부분 주의하세요 !!)

 

Chart.yaml

apiVersion: v2
name: image-gallery
description: My personal demo app
type: application
version: 0.1.0
appVersion: "1.0"

 

values-dev.yaml

backend:
  image: ghcr.io/eunhwa99/backend:latest
  imagePullSecrets: ghcr-secret # secret 정의
  envFromSecret: unsplash-secret 
  service:
    type: ClusterIP
    port: 8000

frontend:
  image: ghcr.io/eunhwa99/frontend:latest
  imagePullSecrets: ghcr-secret # secret 정의
  service:
    type: ClusterIP
    port: 3000

 

backend-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Chart.Name }}-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: {{ .Chart.Name }}-backend
  template:
    metadata:
      labels:
        app: {{ .Chart.Name }}-backend
    spec:
      containers:
        - name: backend
          image: {{ .Values.backend.image }}
          ports:
            - containerPort: {{ .Values.backend.service.port }}
          envFrom:
            - secretRef:
                name: {{ .Values.backend.envFromSecret }}
      imagePullSecrets:
        - name: {{ .Values.backend.imagePullSecrets }}

 

frontend-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Chart.Name }}-frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: {{ .Chart.Name }}-frontend
  template:
    metadata:
      labels:
        app: {{ .Chart.Name }}-frontend
    spec:
      containers:
        - name: frontend
          image: {{ .Values.frontend.image }}
          ports:
            - containerPort: {{ .Values.frontend.service.port }}
      imagePullSecrets:
        - name: {{ .Values.frontend.imagePullSecrets }}

 

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ .Chart.Name }}-backend
spec:
  type: {{ .Values.backend.service.type }}
  selector:
    app: {{ .Chart.Name }}-backend
  ports:
    - protocol: TCP
      port: {{ .Values.backend.service.port }}
      targetPort: {{ .Values.backend.service.port }}

---
apiVersion: v1
kind: Service
metadata:
  name: {{ .Chart.Name }}-frontend
spec:
  type: {{ .Values.frontend.service.type }}
  selector:
    app: {{ .Chart.Name }}-frontend
  ports:
    - protocol: TCP
      port: {{ .Values.frontend.service.port }}
      targetPort: {{ .Values.frontend.service.port }}

4.  local 테스트 및 배포

우선 로컬에서 먼저 테스트 해보는 것을 권장합니다.

  1. 위에서 생성한 cluster 가 제대로 떠 있는지 아래 명령어로 확인해 봅니다.
kubectl get nodes   # Ready 상태 확인
  1. (로컬 테스트) 로컬 build 후 load 하거나, GHCR에 도커 이미지를 push 했다면 GHCR 에서 이미지를 pull 합니다.
# Docker 이미지 빌드 후 Minikube에 로드
eval $(minikube -p minikube docker-env)
docker build -t my-backend:latest ./backend
docker build -t my-frontend:latest ./frontend

# GHCR 사용 시
docker pull ghcr.io/<YOUR_GH_USERNAME>/backend:latest
docker pull ghcr.io/<YOUR_GH_USERNAME>/frontend:latest
  1. Secret 수동 생성(로컬 테스트):
kubectl create secret docker-registry ghcr-secret \
  --docker-server=ghcr.io \
  --docker-username=<YOUR_GH_USERNAME> \
  --docker-password=<생성한 GHCR 용 token> \
  --docker-email=none

kubectl create secret generic unsplash-secret --from-literal=UNSPLASH_ACCESS_KEY=<Unsplash access token>
  1. Helm 배포:
helm upgrade --install image-gallery ./helm/image-gallery -f ./helm/image-gallery/values-dev.yaml

 

위 단계가 잘 진행되었다면 이미 배포가 되었을 것입니다. 해당 deployement 를 삭제하고 Jenkins에서 배포를 진행합니다.


5. 포트포워딩

ClusterIP로 서비스가 정의된 경우(values-dev.yaml에서는 ClusterIP로 되어 있음) 로컬에서 테스트하려면 아래처럼 내 로컬머신과 해당 서비스를 포트 포워딩 해야 합니다.

# 백엔드
kubectl port-forward svc/image-gallery-backend 8000:8000

# 프론트엔드
kubectl port-forward svc/image-gallery-frontend 3000:3000
  • 그런 다음 브라우저에서 http://localhost:3000 접속하여 어플리케이션을 실행할 수 있습니다!

전체 체크리스트 (배포 전 확인 항목)

  • values-*.yamlbackend.image/frontend.image 가 정확히 지정되어 있는가? 
  • Jenkins에 ghcr-credentialsUNSPLASH_KEY 저장했는가?
  • GitHub Actions가 GHCR에 정상적으로 push 하는가? (Actions 로그 확인)
  • Helm이 차트 폴더(./helm/image-gallery)를 찾을 수 있는 위치에서 실행되는가?
  • kubectl describe pod <pod> 로 Event 확인 — Image pull failed? permission denied? 등

추가 참고사항

  • 핵심: 이미지 Pull 문제(ImagePullBackOff)는 대부분 레지스트리 인증(Secret) 문제이고, Helm nil-pointer 문제는 values 파일의 키 누락 문제입니다.
  • Jenkins에서 “경로가 서로 달라서 Helm 차트를 못 찾음”은 굉장히 흔한 실수입니다 — 항상 dir()로 워킹 디렉터리를 명시하거나, checkout으로 workspace에 코드가 있는지 확인하세요.
  • 민감 정보 (Unsplash API key, 레지스트리 PAT)은 절대 Docker 이미지에 넣지 말고 Kubernetes Secret으로 런타임에 주입하세요. Jenkins에서 withCredentials로 안전하게 관리할 수 있습니다.
728x90
반응형