앞의 네 글에서 GitHub, Jenkins, ArgoCD를 하나씩 다뤘습니다. 이 글에서는 이 도구들을 묶어서, 브랜치 전략부터 dev와 prd 환경 분리, 커밋 push에서 AKS 자동 배포까지 이어지는 아키텍처를 설계합니다. 설계 위주의 글이고, 매니페스트 저장소 구성과 ArgoCD 설정은 실제로 만들어 검증한 결과를 함께 붙였습니다.
1. 전체 아키텍처
먼저 전체 그림입니다. 개발자가 코드를 push하면 Jenkins가 이미지를 만들어 ACR(Azure Container Registry)에 올리고, 매니페스트 저장소의 이미지 태그를 바꿉니다. 각 AKS 클러스터 안의 ArgoCD가 그 변경을 보고 자기 환경에 배포합니다.

Jenkins는 ACR과 매니페스트 저장소까지만 다루고, 클러스터 반영은 각 AKS 안의 ArgoCD가 맡습니다
| 구성 요소 | 역할 | dev와 prd를 나누는 방법 |
| GitHub 앱 저장소 | 코드, PR 리뷰, Webhook 발송 | develop 브랜치는 dev, main 브랜치는 prd |
| Jenkins | 테스트, 이미지 빌드와 push, 태그 변경 | 브랜치에 따라 태그 이름과 커밋 방식이 달라짐 |
| ACR | 이미지 보관 | 하나를 같이 쓰고 태그로 구분 |
| GitHub 매니페스트 저장소 | 환경별 원하는 상태 | overlays/dev, overlays/prd 디렉터리 |
| ArgoCD | Git과 클러스터 비교, sync | 클러스터마다 하나씩 설치 |
| AKS | 실제 실행 환경 | dev 클러스터와 prd 클러스터 분리 |
이 구성에서 가장 중요한 결정은 두 가지입니다. 하나는 Jenkins가 클러스터에 직접 배포하지 않는다는 것이고, 다른 하나는 prd 배포가 매니페스트 저장소의 PR 승인을 거쳐야만 일어난다는 것입니다. 앞의 것은 [CI/CD] CI/CD와 GitOps 개념 정리에서 정리한 Pull 방식의 장점(클러스터 권한이 CI 서버에 없음)을 살리기 위해서이고, 뒤의 것은 운영 반영을 사람이 확인하는 지점을 Git 안에 두기 위해서입니다.
2. 브랜치 전략
환경이 dev와 prd 두 개이므로 앱 저장소에도 기준 브랜치를 두 개 둡니다. develop은 dev 환경, main은 prd 환경과 짝을 이룹니다. 브랜치와 환경이 1:1로 맞으면 "지금 dev에 나간 코드"와 "지금 prd에 나간 코드"를 브랜치만 보고 알 수 있기 때문입니다.

feature는 develop으로, develop은 릴리스 PR로 main에 합치고, hotfix는 main에서 따서 main과 develop 모두에 반영합니다
| 브랜치 | 규칙 |
| feature/* | develop에서 분기, develop으로 PR, 테스트 통과와 승인 1명 병합되면 develop 빌드 시작 |
| develop | 상시 유지, 직접 push 금지, PR로만 변경 병합되면 dev 자동 배포 |
| main | 상시 유지, develop이나 hotfix에서 PR, 승인 2명 병합되면 prd 승격 PR 생성 |
| hotfix/* | main에서 분기, main으로 PR, 병합 뒤 develop에도 반영 병합되면 prd 승격 PR 생성 |
develop과 main에는 브랜치 보호 규칙으로 PR 필수, 필수 체크(Jenkins의 PR 빌드 결과), 승인 수를 걸어 둡니다. 설정 방법은 [GitHub] 브랜치, Pull Request, Webhook 사용법 정리에서 다뤘습니다. 환경이 하나뿐이라면 main 하나로 운영하는 GitHub flow가 더 단순하니, 브랜치 수는 환경 수에 맞춰 정하는 것을 권합니다.
3. dev와 prd 인프라 분리
dev와 prd는 AKS 클러스터부터 나눕니다. 한 클러스터 안에서 네임스페이스로만 나누면 비용은 줄지만, 노드 장애나 잘못된 클러스터 설정 변경이 두 환경에 같이 영향을 줍니다. 가능하면 리소스 그룹이나 구독도 나눠서 권한과 비용을 따로 관리하는 편이 좋습니다.
| 항목 | dev | prd |
| AKS 클러스터 | aks-dev | aks-prd |
| ArgoCD | aks-dev 안에 설치 | aks-prd 안에 설치 |
| 바라보는 경로 | overlays/dev | overlays/prd |
| 레플리카, 리소스 | 1개, 제한 없음 | 3개, requests와 limits 지정 |
| 자동 sync, selfHeal | 켬 | 켬 |
| prune | 켬 | 끔, 필요할 때 수동 |
| 배포 승인 | 없음, develop 병합이 곧 배포 | 매니페스트 저장소 PR 승인 |
ArgoCD는 클러스터마다 하나씩 설치했습니다. ArgoCD 하나로 여러 클러스터를 관리할 수도 있지만, 그러면 그 ArgoCD가 prd 클러스터의 관리자 자격 증명을 들고 있게 됩니다. 클러스터마다 두면 prd 권한이 prd 클러스터 밖으로 나가지 않고, dev ArgoCD에 문제가 생겨도 prd 배포에는 영향이 없습니다. 클러스터가 많아지면 중앙 ArgoCD와 ApplicationSet으로 옮기는 것을 검토할 만합니다.
ACR은 하나를 같이 씁니다. 두 AKS 클러스터에 AcrPull 권한을 연결하고, Jenkins에는 AcrPush 권한만 줍니다. Microsoft 문서에 따르면 --attach-acr는 AKS kubelet 관리 ID에 AcrPull 역할을 부여하며, 실행하려면 구독에 Owner 같은 권한이 필요합니다. 아래 명령은 구성 예시입니다.
# 두 클러스터에 ACR pull 권한 연결
az aks update --name aks-dev --resource-group rg-dev --attach-acr exampleacr
az aks update --name aks-prd --resource-group rg-prd --attach-acr exampleacr
# Jenkins용 서비스 주체에는 push 권한만 부여
ACR_ID=$(az acr show --name exampleacr --query id --output tsv)
az role assignment create --assignee <JENKINS_SP_APP_ID> --role AcrPush --scope "$ACR_ID"
4. 매니페스트 저장소 구성
매니페스트 저장소는 Kustomize로 공통 부분(base)과 환경별 차이(overlays)를 나눕니다. 환경마다 YAML을 통째로 복사하면 한쪽만 고치는 실수가 생기기 쉬운데, 이렇게 나누면 환경별 차이만 overlay에 남습니다. 환경을 브랜치가 아니라 디렉터리로 나눈 이유도 같습니다. 브랜치로 나누면 dev와 prd의 차이를 한눈에 비교하기 어렵고, 공통 변경을 브랜치마다 옮겨야 합니다.
구조를 실제로 만들어 확인했습니다. ArgoCD Application 정의도 같은 저장소에 둡니다.
./apps/demo-api/base/deployment.yaml
./apps/demo-api/base/kustomization.yaml
./apps/demo-api/base/service.yaml
./apps/demo-api/overlays/dev/kustomization.yaml
./apps/demo-api/overlays/prd/kustomization.yaml
./apps/demo-api/overlays/prd/resources.yaml
./argocd/dev/demo-api.yaml
./argocd/prd/demo-api.yaml
dev overlay는 네임스페이스와 이미지만 지정합니다. newTag가 Jenkins가 바꾸는 값입니다.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: demo
resources:
- ../../base
images:
- name: demo-api
newName: exampleacr.azurecr.io/demo-api
newTag: develop-0000000
prd overlay는 레플리카 수와 리소스 설정을 더합니다.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: demo
resources:
- ../../base
replicas:
- count: 3
name: demo-api
images:
- name: demo-api
newName: exampleacr.azurecr.io/demo-api
newTag: v1.3.0
patches:
- path: resources.yaml
두 overlay를 렌더링해서 비교하면 환경별 차이가 정확히 이 세 가지(레플리카, 이미지 태그, 리소스)뿐인 것을 확인할 수 있습니다. PR 리뷰 때도 이 diff를 보면 prd에 무엇이 달라지는지 바로 보입니다.
#!/usr/bin/env bash
# 같은 base에서 환경별로 무엇이 달라지는지 렌더링해서 비교
cd manifest-repo/apps/demo-api
kubectl kustomize overlays/dev > /tmp/dev.yaml
kubectl kustomize overlays/prd > /tmp/prd.yaml
diff /tmp/dev.yaml /tmp/prd.yaml
19c19
< replicas: 1
---
> replicas: 3
29c29
< - image: exampleacr.azurecr.io/demo-api:develop-0000000
---
> - image: exampleacr.azurecr.io/demo-api:v1.3.0
32a33,38
> resources:
> limits:
> memory: 512Mi
> requests:
> cpu: 250m
> memory: 256Mi
5. 파이프라인 흐름
dev와 prd 배포는 거의 같은 길을 지나고, 매니페스트 저장소에 들어가는 방식 한 곳만 다릅니다.

dev는 Jenkins가 매니페스트를 바로 커밋하고, prd는 승격 PR이 승인되어야 main에 들어갑니다
5.1 Jenkinsfile
Jenkins에는 앱 저장소를 Multibranch Pipeline으로 등록해서, 브랜치마다 같은 Jenkinsfile이 돌되 develop과 main에서만 이미지를 만들도록 합니다. 아래는 구성 예시이고, [Jenkins] Jenkins 설치와 사용법 정리의 실습 환경은 파이프라인 플러그인을 설치할 수 없어 실행하지 않았습니다.
pipeline {
agent { label 'docker' }
environment {
REGISTRY = 'exampleacr.azurecr.io'
IMAGE = "${REGISTRY}/demo-api"
}
stages {
stage('Test') {
steps { sh 'make test' } // 모든 브랜치와 PR에서 실행
}
stage('Build & Push') {
when { anyOf { branch 'develop'; branch 'main' } }
steps {
script {
// develop은 커밋 해시, main은 VERSION 파일의 버전을 태그로 사용
env.TAG = env.BRANCH_NAME == 'main' ? readFile('VERSION').trim()
: "develop-${env.GIT_COMMIT.take(7)}"
}
withCredentials([usernamePassword(credentialsId: 'acr-push',
usernameVariable: 'ACR_USER', passwordVariable: 'ACR_PASS')]) {
sh '''
echo "$ACR_PASS" | docker login "$REGISTRY" -u "$ACR_USER" --password-stdin
docker build -t "$IMAGE:$TAG" .
docker push "$IMAGE:$TAG"
'''
}
}
}
stage('Update manifest') {
when { anyOf { branch 'develop'; branch 'main' } }
steps {
// develop이면 overlays/dev에 바로 커밋, main이면 승격 브랜치와 PR 생성
sh './ci/update-manifest.sh "$BRANCH_NAME" "$TAG"'
}
}
}
}
이미지 태그에는 latest를 쓰지 않습니다. 태그가 바뀌지 않으면 매니페스트도 바뀌지 않아 ArgoCD 입장에서는 배포할 것이 없고, 어떤 코드가 나가 있는지도 추적할 수 없기 때문입니다.
5.2 dev: 매니페스트에 바로 커밋
develop 빌드가 끝나면 Jenkins가 하는 일을 실제로 실행해 보았습니다. kustomize edit set image로 태그만 바꾸고 커밋합니다.
#!/usr/bin/env bash
# develop 브랜치 빌드가 끝난 뒤 Jenkins가 하는 일: dev overlay의 이미지 태그만 바꿔 main에 커밋
set -e
cd manifest-repo/apps/demo-api/overlays/dev
kustomize edit set image demo-api=exampleacr.azurecr.io/demo-api:develop-3f9c2e1
git -c user.name=jenkins-bot -c user.email=ci@example.com \
commit -qam "dev: demo-api develop-3f9c2e1"
git show --format='%h %an %s' --stat HEAD
git diff HEAD~1 -U0 -- kustomization.yaml | grep "^[-+] "
17284e2 jenkins-bot dev: demo-api develop-3f9c2e1
apps/demo-api/overlays/dev/kustomization.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
- newTag: develop-0000000
+ newTag: develop-3f9c2e1
바뀐 줄은 newTag 한 줄입니다. 이 커밋이 main에 들어가면 dev 클러스터의 ArgoCD가 감지해서 자동으로 sync합니다.
5.3 prd: 승격 PR
main 빌드 뒤에는 매니페스트 저장소의 main에 바로 커밋하지 않고, 승격 브랜치를 만들어 prd overlay를 바꿉니다.
#!/usr/bin/env bash
# main 브랜치 빌드(v1.4.0) 뒤 Jenkins가 하는 일: prd는 바로 커밋하지 않고 승격 브랜치를 만들어 PR로 올린다
set -e
cd manifest-repo
git switch -q -c promote/prd-v1.4.0
cd apps/demo-api/overlays/prd
kustomize edit set image demo-api=exampleacr.azurecr.io/demo-api:v1.4.0
git -c user.name=jenkins-bot -c user.email=ci@example.com \
commit -qam "prd: demo-api v1.4.0"
cd ../../../..
git log --oneline --graph --all --format='%h %an %s%d'
git diff main -U0 -- apps | grep "^[-+] "
* 5964b73 jenkins-bot prd: demo-api v1.4.0 (HEAD -> promote/prd-v1.4.0)
* 17284e2 jenkins-bot dev: demo-api develop-3f9c2e1 (main)
* 81dea1e ops-kim init: demo-api base, dev/prd overlays
- newTag: v1.3.0
+ newTag: v1.4.0
이 브랜치로 PR을 열면 리뷰어는 v1.3.0에서 v1.4.0으로 바뀌는 한 줄을 보고 승인합니다. PR은 GitHub CLI로 만들 수 있고, 아래는 예시입니다. 승인 후 병합되면 prd 클러스터의 ArgoCD가 sync합니다. 롤백도 같은 방식으로, 이 병합 커밋을 git revert하는 PR을 올리면 됩니다.
gh pr create --repo example-org/manifest-repo --base main --head promote/prd-v1.4.0 \
--title "prd: demo-api v1.4.0" --body "main 빌드 결과를 prd에 반영"
6. ArgoCD 설정
각 클러스터의 ArgoCD에는 자기 환경의 Application 하나만 등록합니다. prd Application은 아래와 같고, dev는 이름과 path, prune 값만 다릅니다.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: demo-api-prd
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/example-org/manifest-repo.git
targetRevision: main
path: apps/demo-api/overlays/prd
destination:
server: https://kubernetes.default.svc
namespace: demo
syncPolicy:
automated:
prune: false
selfHeal: true
syncOptions:
- CreateNamespace=true
prd에서 prune을 끈 이유는 [ArgoCD] ArgoCD 설치와 사용법 정리에서 정리한 것과 같습니다. 삭제는 되돌리기 어려워서, Git에서 리소스를 지웠을 때 클러스터에서도 지우는 작업은 확인 후 수동으로 합니다. 두 정의가 올바른지는 ArgoCD CRD가 설치된 실습 클러스터에 server dry-run으로 확인했습니다.
#!/usr/bin/env bash
# ArgoCD Application 정의를 실제 API 서버에 dry-run으로 검증 (argocd CRD가 설치된 클러스터)
cd manifest-repo
kubectl apply --dry-run=server -f argocd/dev/demo-api.yaml -f argocd/prd/demo-api.yaml
application.argoproj.io/demo-api-dev created (server dry run)
application.argoproj.io/demo-api-prd created (server dry run)
7. 권한과 보안에서 챙길 것
| 항목 | 권장 설정 | 이유 |
| Jenkins의 GitHub 권한 | GitHub App으로 인증, 매니페스트 저장소 쓰기만 허용 | 개인 토큰은 퇴사나 권한 변경에 취약 |
| 매니페스트 main 보호 | PR 필수, Jenkins App만 우회 허용 | dev 태그 커밋은 자동, 사람의 직접 push는 차단 |
| prd overlay 리뷰 | CODEOWNERS에 운영팀 지정, 코드 오너 승인 필수 | prd 변경은 운영팀 승인 없이 병합 불가 |
| ACR 권한 | Jenkins는 AcrPush, AKS는 AcrPull | 각자 필요한 권한만 |
| 시크릿 | Git에 평문으로 두지 않고 Key Vault 등에서 주입 | 매니페스트 저장소는 여러 사람이 읽음 |
매니페스트 저장소 main의 우회 권한은 저장소 단위라서, 원칙적으로는 Jenkins가 prd overlay도 직접 바꿀 수 있습니다. 이 구성에서는 파이프라인 스크립트가 prd를 승격 브랜치로만 올리도록 제한했고, 더 엄격하게 막아야 한다면 prd 매니페스트를 별도 저장소로 나누는 방법이 있습니다.
8. 정리
- 앱 저장소는 develop과 main을 dev와 prd에 1:1로 맞추고, 매니페스트 저장소는 Kustomize base와 overlays/dev, overlays/prd로 환경 차이만 관리합니다.
- Jenkins는 이미지를 ACR에 올리고 매니페스트 태그만 바꾸며, dev는 바로 커밋하고 prd는 승격 PR 승인을 거친 뒤에만 main에 들어갑니다.
- AKS 클러스터와 ArgoCD는 환경마다 따로 두고, ACR은 AKS에 AcrPull, Jenkins에 AcrPush만 주어 각자 필요한 권한만 갖게 합니다.
참고
'DevOps > CI & CD' 카테고리의 다른 글
| [ArgoCD] ArgoCD 설치와 사용법 정리 (0) | 2026.09.27 |
|---|---|
| [Jenkins] Jenkins 설치와 사용법 정리 (0) | 2026.09.27 |
| [GitHub] 브랜치, Pull Request, Webhook 사용법 정리 (0) | 2026.09.27 |
| [CI/CD] CI/CD와 GitOps 개념 정리 (0) | 2026.09.27 |