로컬 환경(Kubernetes + ArgoCD)에서
Spring Boot 애플리케이션을 CI/CD로 자동 배포하고, Argo Rollouts를 이용해 Canary 배포까지 테스트하는 과정을 정리했습니다.
먼저 테스트용 Spring Boot 애플리케이션을 만듭니다.
이 애플리케이션은 간단히 /api/hello 엔드포인트를 통해 현재 실행 중인 프로파일과 버전 정보를 반환하도록 구성합니다.
HelloController
@RestController
@RequestMapping("/api")
public class HelloController {
@Autowired
private Environment environment;
@Value("${app.version:unknown}")
private String version;
@GetMapping("/hello")
public Map<String, String> hello() {
String activeProfile = environment.getActiveProfiles()[0];
return Map.of("message", "Hello from Spring DevOps Lab!",
"version", version,
"profile", activeProfile);
}
}
application.yml
server:
port: 8080
spring:
application:
name: spring-cicd-k8s-demo
app:
version: "v1"
app.version은 나중에 카나리 배포 테스트 시 "v2"로 변경하여 배포 버전 차이를 확인할 예정입니다.
Dockerfile
FROM eclipse-temurin:21-jdk-jammy
WORKDIR /app
COPY build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
브라우저에서 http://localhost:8080/api/hello를 호출하면 다음과 같은 응답을 확인할 수 있습니다:
{
"message": "Hello from Spring DevOps Lab!",
"version": "v1",
"profile": "default"
}
이제 GitHub에 코드를 push하면, 이후 CI/CD 파이프라인에서 해당 소스를 기반으로 배포가 이루어집니다.
로컬 테스트를 위해 Docker Desktop의 Kubernetes 기능을 활성화합니다.

kubectl cluster-info
kubectl get nodes
ArgoCD는 GitOps 기반 CD 도구입니다.
아래 명령으로 설치합니다.
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
Argo CD 서버를 로컬에서도 접근하려면 Service 타입을 NodePort로 바꿔야합니다.
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort"}}'
ArgoCD로 접근하기 위해 아래 명령으로 포트를 확인합니다.
$ kubectl get svc -n argocd
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
...
argocd-server NodePort 10.99.127.32 <none> 80:30066/TCP,443:31182/TCP 2d5h
...
위 예시에서 argocd-server의 NodePort가 30066으로 설정되어 있습니다.
이후 브라우저에서 https://localhost:30066 접속 → 기본 계정 admin으로 로그인합니다.
초기 비밀번호는 다음으로 확인할 수 있습니다.
kubectl get secret -n argocd argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d && echo
로그인 후 비밀번호는 User Info → UPDATE PASSWORD 에서 변경할 수 있습니다.

이제 애플리케이션을 컨테이너 이미지로 빌드하고,
이를 Docker Hub에 업로드(push)하여 배포 파이프라인에서 사용할 수 있도록 설정하겠습니다.
도커 이미지를 푸시할 Docker Hub 저장소를 하나 생성합니다.
예: docker.io/<username>/spring-cicd-k8s-demo
Access Token 생성
GitHub Actions가 Docker Hub에 접근하려면 인증 토큰이 필요합니다.
다음 경로에서 Access Token을 발급받습니다.
Docker Hub → Account Settings → Security → New Access Token
- 토큰 이름 예시: github-action-token
- 발급된 토큰은 한 번만 표시되므로, 복사해두었다가 GitHub Secrets에 등록해야 합니다.
GitHub Repository → Settings → Secrets and variables → Actions 에 다음을 추가합니다.
- DOCKERHUB_USERNAME : Docker Hub 사용자명
- DOCKERHUB_TOKEN : 방금 만든 Access Token
이렇게 설정하면 GitHub Actions 워크플로에서 secrets.DOCKERHUB_USERNAME 과 secrets.DOCKERHUB_TOKEN을 사용해 안전하게 로그인할 수 있습니다.
GitHub Action 권한 설정
GitHub Actions 워크플로에서 git push 명령을 사용하려면, 기본적으로 레포지토리에 대한 쓰기 권한(write permission) 이 필요합니다.
이 설정이 없으면, 워크플로가 매니페스트 파일(patch-develop.yaml)을 수정한 후 커밋/푸시를 시도할 때 오류가 발생합니다.
GitHub Repository → Actions → General

Read and wirte permissions 설정해줘야 GitHub Actions에서 push가 가능해집니다.
현재 예시에서는 Spring 애플리케이션 코드와 ArgoCD 배포 매니페스트를 하나의 저장소에서 관리하고 있습니다.
이 구조는 테스트나 개인 프로젝트에서는 간단하고 빠르게 구축할 수 있다는 장점이 있습니다.
하지만 실무 환경에서는 애플리케이션 저장소와 배포 저장소를 분리하는 것이 일반적입니다.
.github/workflows 폴더를 생성한 후 안에 스크립트를 작성하면 됩니다.
예시에서는 실무환경과 유사하게 진행하기 위해 Profile별로 분리했습니다.
.github/workflows/docker-build-develop
name: Deploy to Develop
on:
push:
branches: [ develop ]
permissions:
contents: write
packages: write
id-token: write
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Build JAR
run: ./gradlew clean build -x test
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/spring-cicd-k8s-demo:develop-${{ github.run_number }}
${{ secrets.DOCKERHUB_USERNAME }}/spring-cicd-k8s-demo:develop-latest
- name: Update image tag in Kustomize (develop)
run: |
sed -i "s|image: .*|image: ${{ secrets.DOCKERHUB_USERNAME }}/spring-cicd-k8s-demo:develop-${{ github.run_number }}|g" k8s/develop/patch-develop.yaml
- name: Commit and push manifest
run: |
git config --global user.name "github-actions"
git config --global user.email "actions@github.com"
git add k8s/develop/patch-develop.yaml
git commit -m "Update develop image tag"
git push
develop 브랜치에 코드가 푸시되면이제 GitHub Actions에서 빌드된 이미지를 Kubernetes에 배포하기 위한 매니페스트를 구성합니다.
여기서는 Kustomize를 활용해 환경별(develop, prod) 설정을 관리합니다.
Kustomize는 kubectl에 기본 내장되어 있으며, base 디렉토리에 공통 리소스를 정의하고 환경별(develop, prod) 폴더에서 이를 상속(overlay)하는 방식으로 동작합니다.
k8s/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── kustomization.yaml
├── develop/
│ ├── kustomization.yaml
│ ├── namespace.yaml
│ ├── patch-develop.yaml
└── prod/
├── kustomization.yaml
├── namespace.yaml
├── patch-prod.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cicd-k8s-demo
labels:
app: spring-cicd-k8s-demo
spec:
replicas: 1
selector:
matchLabels:
app: spring-cicd-k8s-demo
template:
metadata:
labels:
app: spring-cicd-k8s-demo
spec:
containers:
- name: spring-cicd-k8s-demo
image: <username>/spring-cicd-k8s-demo:latest
ports:
- containerPort: 8080
apiVersion: v1
kind: Service
metadata:
name: spring-cicd-k8s-demo-service
labels:
app: spring-cicd-k8s-demo
spec:
selector:
app: spring-cicd-k8s-demo
ports:
- name: http
port: 80
targetPort: 8080
resources:
- deployment.yaml
- service.yaml
Kustomize가 참조할 리소스 목록을 정의합니다.
이 파일을 통해 base 디렉토리를 하나의 단위로 구성할 수 있습니다.
apiVersion: v1
kind: Namespace
metadata:
name: develop
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-cicd-k8s-demo
spec:
template:
spec:
containers:
- name: spring-cicd-k8s-demo
image: <username>/spring-cicd-k8s-demo:develop-latest
env:
- name: SPRING_PROFILES_ACTIVE
value: develop
---
apiVersion: v1
kind: Service
metadata:
name: spring-cicd-k8s-demo-service
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: 8080
nodePort: 30801
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
- namespace.yaml
namespace: develop
patchesStrategicMerge:
- patch-develop.yaml
Argo CD는 GitOps 기반의 배포 도구로,
Git 저장소의 선언적 매니페스트(YAML)를 기준으로 Kubernetes 환경을 자동 동기화(Sync)합니다.
Argo CD에서 Application 리소스는
“어떤 Git 리포지토리의 어떤 경로를, 어떤 클러스터의 어떤 네임스페이스에 배포할지”를 정의하는 핵심 객체입니다.
다음은 develop 브랜치의 코드를 로컬 쿠버네티스 클러스터에 자동 배포하도록 설정한 예시입니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: spring-cicd-k8s-demo-develop
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/<username>/spring-cicd-k8s-demo.git
targetRevision: develop
path: k8s/develop
destination:
server: https://kubernetes.default.svc
namespace: develop
syncPolicy:
automated:
prune: true
selfHeal: true
k8s/develop/patch-develop.yaml 파일의 이미지 태그를 업데이트합니다.path: k8s/develop)을 기준으로 Kubernetes에 새로운 이미지 버전을 자동 반영합니다. Argo CD UI에 접속하면 spring-cicd-k8s-demo-develop Application이 생성되어 있는 것을 확인할 수 있습니다.
이 Application의 상태가 Synced / Healthy로 표시된다면 정상적으로 배포가 완료된 것입니다.
이제 Git의 변경사항이 자동으로 쿠버네티스 환경에 반영되는 GitOps 기반 CI/CD 파이프라인이 완성되었습니다.
카나리 배포는 새로운 버전의 애플리케이션을 전체 트래픽에 바로 적용하지 않고, 일부 사용자 또는 트래픽만 새로운 버전으로 라우팅하여 검증하는 배포 방식입니다.
이 방식의 이름은 “광산에서 유독가스를 감지하기 위해 카나리아를 먼저 보낸다”는 비유에서 유래되었습니다.
이런 전략은 장애로 인한 사용자 영향 범위를 최소화하고, 실제 운영 환경에서 새 버전의 안정성을 빠르게 확인할 수 있다는 장점이 있습니다.
Argo Rollouts는 Argo 프로젝트의 확장 기능으로, Kubernetes에서 카나리 및 블루-그린 배포 전략을 자동화하기 위해 만들어진 오픈소스 도구입니다.
Argo Rollouts는 기본 Deployment 리소스 대신 Rollout이라는 새로운 CRD(Custom Resource Definition)를 사용합니다.
이를 통해 다음과 같은 기능을 제공합니다:
일반적인 Deployment는 새 버전을 배포할 때
기존 파드를 한 번에 교체(rolling update)하지만,
Rollout은 트래픽 비율을 세밀하게 조절하면서 점진적으로 배포합니다.
이를 통해 서비스 중단 없이 안정적으로 새 버전을 검증할 수 있습니다.
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
Canary 배포를 적용하기 위해 기존 Deployment 기반 구조를 다음과 같이 변경합니다.
k8s/
├── base/
│ ├── rollout.yaml
│ ├── service-stable.yaml
│ ├── service-canary.yaml
│ ├── ingress.yaml
│ └── kustomization.yaml
├── develop/
│ ├── kustomization.yaml
│ ├── namespace.yaml
│ └── patch-develop.yaml
Argo Rollouts는 Deployment 대신 Rollout 리소스를 사용합니다.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: spring-cicd-k8s-demo
spec:
replicas: 3
revisionHistoryLimit: 2
selector:
matchLabels:
app: spring-cicd-k8s-demo
template:
metadata:
labels:
app: spring-cicd-k8s-demo
spec:
containers:
- name: spring-cicd-k8s-demo
image: <username>/spring-cicd-k8s-demo:develop-latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: develop
strategy:
canary:
canaryService: spring-cicd-k8s-demo-canary
stableService: spring-cicd-k8s-demo-stable
trafficRouting:
nginx:
stableIngress: spring-cicd-k8s-demo-ingress
steps:
- setWeight: 20
- pause: { duration: 30s }
- setWeight: 50
- pause: { duration: 1m }
- setWeight: 100
trafficRouting.nginx를 통해 Ingress Controller를 이용한 트래픽 분할이 가능합니다.Argo Rollouts는 canary 전략을 쓰면 다음 2개의 Service가 필요합니다.
service-stable.yaml
apiVersion: v1
kind: Service
metadata:
name: spring-cicd-k8s-demo-stable
spec:
selector:
app: spring-cicd-k8s-demo
ports:
- name: http
port: 80
targetPort: 8080
service-canary.yaml
apiVersion: v1
kind: Service
metadata:
name: spring-cicd-k8s-demo-canary
spec:
selector:
app: spring-cicd-k8s-demo
ports:
- name: http
port: 80
targetPort: 8080
Argo Rollouts는 Ingress 기반 트래픽 분배를 사용하기 때문에
nginx-ingress-controller를 설치해야 합니다.
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml
적용 후 상태 확인:
kubectl get pods -n ingress-nginx
아래 Ingress는 develop.localtest.me 도메인으로 접근할 수 있게 설정합니다.
localtest.me는 자동으로 127.0.0.1을 가리키므로 /etc/hosts 수정이 필요 없습니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: spring-cicd-k8s-demo-ingress
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: develop.localtest.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: spring-cicd-k8s-demo-stable
port:
number: 80
이제 브라우저에서 http://develop.localtest.me/api/hello 주소로 접근이 가능해집니다.
Rollout 리소스에 새로운 이미지 버전을 반영할 수 있도록 patch-develop.yaml을 수정합니다.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: spring-cicd-k8s-demo
spec:
template:
spec:
containers:
- name: spring-cicd-k8s-demo
image: whwp4151/spring-cicd-k8s-demo:develop-17
env:
- name: SPRING_PROFILES_ACTIVE
value: develop
기존에는 NodePort를 통해 접근했지만,
이제는 Ingress를 통해 트래픽이 관리되므로 NodePort 설정이 필요 없습니다.
base/kustomization.yaml에서 기존 deployment.yaml 대신 rollout.yaml을 사용하도록 수정합니다.
resources:
- rollout.yaml
- service-stable.yaml
- service-canary.yaml
- ingress.yaml
이제 실제로 Canary 전략이 정상적으로 동작하는지 확인해보겠습니다.
Spring 애플리케이션의 버전을 v2 → v3로 수정한 뒤 GitHub에 push합니다.
GitHub Actions가 트리거되며 자동으로 Docker 이미지가 빌드되고, ArgoCD를 통해 Kubernetes 환경에 새로운 버전이 배포됩니다.
배포 후 API 응답을 확인하면 아래와 같은 메시지를 확인할 수 있습니다.
{"message":"Hello from Spring DevOps Lab!","version":"v3","profile":"develop"}

ArgoCD에 접속 후 상태를 보면, v2 파드와 v3 파드가 일정 비율로 공존하고 있습니다.(맨위가 v3 파드 입니다.)
지정한 Canary 전략(20% → 50% → 100%)에 따라 점진적으로 트래픽이 새로운 버전(v3)으로 이동하는 것을 확인할 수 있습니다.
이때, 브라우저 또는 API 호출을 여러 번 수행하면 v2와 v3 버전이 번갈아 응답하는 것을 볼 수 있습니다.
이는 트래픽이 Canary 비율에 따라 분배되고 있음을 의미합니다.
결과적으로 서비스 중단 없이 새로운 버전이 순차적으로 배포되는 과정을 경험할 수 있었습니다.
{"message":"Hello from Spring DevOps Lab!","version":"v3","profile":"develop"}
이번 실습에서는 GitHub Actions, ArgoCD, Argo Rollouts를 이용해 Spring 애플리케이션의 자동 배포 및 Canary 전략을 구성했습니다.
전체적인 배포 흐름은 다음과 같습니다.
전체 구조