이번 블로그는 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,dockerCLI가 동작해야 함 - GitHub 리포지토리(코드 +
.github/workflows), GHCR 사용(또는 원하는 레지스트리) - Jenkins에 Credentials 추가
1. 필수 툴 설치
- Docker
- Docker Desktop 다운로드
- 설치 후 확인:
docker --version
- kubectl (Kubernetes CLI)
- 설치 가이드
- 설치 후 확인:
kubectl version --client
- Minikube 설치
- 설치:
brew install minikube # Mac + Homebrew - 설치 확인:
minikube version
- 설치:
- Helm (Kubernetes Package Manager)
- 설치:
brew install helm - 설치 확인:
helm version
- 설치:
2. 계정 & 토큰 준비
- GitHub
- 리포지토리 준비 (이전 포스팅에서 사용하던 거 그대로 사용 예정, 예시 -
ImageGallery)
- 리포지토리 준비 (이전 포스팅에서 사용하던 거 그대로 사용 예정, 예시 -
- 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 으로 등록됨)
- Unsplash API Key
- Unsplash Developers에서 API Key 발급
- Jenkins에
UNSPLASH_KEY로 Credentials 등록
3. Kubernetes 클러스터 준비 (Minikube)
- 클러스터 생성 및 확인
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 에 아래 두 가지 추가:
- ghcr-credentials
- Kind: Username with password
- Username: GitHub ID
- Password: 2번 과정에서 진행한 GHCR Personal Access Token
- 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 테스트 및 배포
우선 로컬에서 먼저 테스트 해보는 것을 권장합니다.
- 위에서 생성한 cluster 가 제대로 떠 있는지 아래 명령어로 확인해 봅니다.
kubectl get nodes # Ready 상태 확인
- (로컬 테스트) 로컬 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
- 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>
- 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-*.yaml에backend.image/frontend.image가 정확히 지정되어 있는가? - Jenkins에
ghcr-credentials와UNSPLASH_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로 안전하게 관리할 수 있습니다.
'Side Project' 카테고리의 다른 글
| 쿠버네티스 배포 중 마주한 문제와 해결방법 (0) | 2025.09.07 |
|---|---|
| React+Flask 어플리케이션: 코드부터 Jenkins 배포까지 (CI/CD) - 3 (Ingress 활용하기) (0) | 2025.09.07 |
| Jenkins 배포 중 마주한 에러들과 해결방법 (0) | 2025.09.04 |
| React+Flask 어플리케이션: 코드부터 Jenkins 배포까지 (CI/CD) - 1 (0) | 2025.09.04 |
| Docker compose로 MongoDB 사용하기 (Mac) (2) | 2024.11.19 |