본문으로 건너뛰기
← Systems Notebook / Ops
MLOps Notes11 / 15

dev/stg/prod 멀티클러스터 GitOps: 환경 승격은 Git 변경이다

Argo CD ApplicationSet과 AppProject로 여러 클러스터를 관리하고, 같은 이미지를 dev에서 stg와 prod로 옮길 때 Git 변경과 Merge Request로 승인하는 과정을 살펴봅니다.

이 글의 배경

HPE 엔지니어의 작업 자료를 바탕으로 정리한 글입니다. 개인 실험 기록이나 HPE 공식 문서와는 구분합니다. 본문의 환경과 버전은 글 작성 당시를 기준으로 합니다.

개발 클러스터에서 자동 배포를 구성한 뒤에는 staging과 production으로 배포 범위를 넓히는 방법을 정해야 합니다. 같은 pipeline이 각 환경에 직접 배포할지, 이미지를 환경마다 다시 빌드할지, 운영 배포 승인을 GitLab과 Argo CD 중 어디에서 받을지가 주요 결정 사항입니다. 한 Argo CD로 여러 클러스터를 관리한다면 잘못된 설정이 모든 환경에 퍼지는 상황도 고려해야 합니다.

멀티클러스터 GitOps에서는 클러스터 수보다 한 번 만든 동일한 artifact를 어떤 검증과 승인으로 다음 환경의 원하는 상태에 채택할지를 먼저 정해야 합니다. 개발에서 검증한 이미지를 staging에서 다시 빌드하면 같은 소스라도 다른 artifact가 됩니다. 동일한 commit SHA tag나 digest를 환경별 values에 순서대로 반영하면 환경이 바뀌어도 같은 대상을 검증할 수 있습니다.

이 글에서는 다음 운영 규칙을 구현합니다.

빌드는 한 번만 합니다. dev는 CI가 갱신하고, stg와 prod는 승인된 Merge Request로 같은 이미지 참조를 승격합니다. Argo CD는 승인 이후 모든 환경을 자동으로 Git에 기록된 상태에 맞춥니다.

이 글에서 다루는 것

  • 중앙 Argo CD가 dev, stg, prod 클러스터를 관리하는 구조
  • 외부 클러스터 등록과 cluster credential의 보안 의미
  • AppProject로 source, cluster, namespace 경계를 강제하는 방법
  • ApplicationSet List Generator로 환경별 Application을 생성하는 방법
  • 같은 이미지 tag를 dev → stg → prod로 승격하는 Git 절차
  • GitLab 보호 브랜치, Merge Request, CODEOWNERS와 Argo CD RBAC의 역할 분리
  • 자동 sync, self-heal, Sync Window, 롤백과 비상 변경의 운영 원칙

실습 환경과 치환값

아래 stg/prod IP는 문서용 TEST-NET 대역입니다. 10편의 dev는 Argo CD가 실행되는 내부 클러스터로 유지하고, 외부 두 주소는 실제 argocd cluster list 출력과 정확히 맞춥니다.

항목 문서 예시 의미
중앙 Argo CD argocd.lab.example.com 세 대상 클러스터를 관리
dev API https://kubernetes.default.svc Argo CD 내부 개발 클러스터
stg API https://192.0.2.22:6443 검증 클러스터 API
prod API https://192.0.2.23:6443 운영 클러스터 API
kubeconfig context dev-admin, stg-admin, prod-admin 등록 시 사용할 로컬 context
배포 저장소 https://gitlab.lab.example.com/platform/student-01-web-deploy.git 환경별 values와 ApplicationSet
namespace student-01 각 클러스터의 애플리케이션 경계
이미지 harbor.lab.example.com/mlops-training/student-01-web 세 환경이 공유하는 artifact 저장소
Gateway nginx-gateway/platform-gateway 각 클러스터의 HTTPS 진입점
그룹 예시 platform-viewers, prod-breakglass IdP에서 전달할 예시 그룹명

실제 cluster token, CA, 사용자 계정, 조직 그룹 이름은 본문에 쓰지 않습니다. cluster API 주소도 문서 IP로 공개 가능한 경우가 아니라면 외부 게시 전에 안전한 예시로 치환합니다.

세 대상 클러스터에는 2편과 같은 Gateway, TLS 인증서, DNS 경로가 준비돼 있다고 가정합니다. 환경마다 Gateway 이름이나 namespace가 다르면 values-dev.yaml, values-stg.yaml, values-prod.yaml의 parentRef도 함께 분리합니다. 같은 Chart라고 해서 모든 클러스터의 플랫폼 리소스 이름까지 같다고 가정하지 않습니다.

시작 전 도구 확인

이 글의 values 조회와 수정 명령은 Mike Farah의 yq v4 문법을 사용합니다. Python 패키지 등 이름만 같은 다른 yq는 strenv()와 -i 동작이 달라 그대로 실행할 수 없습니다.

bash
git --version
kubectl version --client
argocd version --client
helm version --short
yq --version

yq --version | grep -Eq 'version v4\.' || {
  echo 'Mike Farah yq v4가 필요합니다.' >&2
  exit 1
}

팀에서는 설치할 때마다 최신 버전을 받기보다 검증한 CLI 버전과 checksum을 고정합니다. 이후의 yq -i 명령은 작업 트리의 values 파일을 바꾸므로 새 branch에서 실행하고 git diff로 결과를 검토합니다.

멀티클러스터에서 책임을 세 층으로 나누기

구조 살펴보기 / 01환경별 배포와 동일 이미지 사용
GitLab 배포 저장소values-dev/stg/prod
ApplicationSet환경별 Application 생성
01 / 04
환경별 원하는 상태 기록

values-dev/stg/prod는 각 환경의 배포 선언입니다. 환경별 변경과 승인은 Git에 남깁니다.

1

GitLab 배포 저장소 → ApplicationSet환경별 원하는 상태

구성 관계를 차례로 강조합니다. 단계는 실제 실행 순서가 아닙니다.

전체 단계 한눈에 읽기
  1. 환경별 원하는 상태 기록

    values-dev/stg/prod는 각 환경의 배포 선언입니다. 환경별 변경과 승인은 Git에 남깁니다.

    • GitLab 배포 저장소 → ApplicationSet: 환경별 원하는 상태
  2. 환경별 Application 생성

    ApplicationSet이 환경별 Application을 만들고 중앙 Argo CD가 관리합니다.

    • GitLab 배포 저장소 → ApplicationSet: 환경별 원하는 상태
    • ApplicationSet → 중앙 Argo CD: Application 생성
  3. 각 클러스터로 동기화

    Argo CD는 각 대상 클러스터를 동기화합니다. dev에서 stg, prod로 패킷이 순차 이동하는 경로는 아닙니다.

    • 중앙 Argo CD → dev cluster: dev 동기화
    • 중앙 Argo CD → stg cluster: stg 동기화
    • 중앙 Argo CD → prod cluster: prod 동기화
  4. 같은 artifact를 사용

    각 클러스터가 Harbor에서 동일한 image tag/digest를 pull합니다. 원본의 Harbor→클러스터 선은 이미지 사용 관계로 표시하며 pull 요청 방향을 뜻하지 않습니다.

    • Harbor → dev cluster: 동일 이미지 사용
    • Harbor → stg cluster: 동일 이미지 사용
    • Harbor → prod cluster: 동일 이미지 사용
도식 원문
flowchart TD
    git["GitLab 배포 저장소<br/>values-dev/stg/prod"] --> appset["ApplicationSet<br/>환경별 Application 생성"]
    appset --> argo["중앙 Argo CD"]
    argo --> dev["dev cluster"]
    argo --> stg["stg cluster"]
    argo --> prod["prod cluster"]
    harbor["Harbor<br/>동일 image tag/digest"] -. pull .-> dev
    harbor -. pull .-> stg
    harbor -. pull .-> prod

GitLab, Argo CD, Kubernetes RBAC은 다음과 같이 서로 다른 단계의 권한을 담당합니다.

  1. GitLab: 누가 어떤 환경의 원하는 상태 변경을 승인했습니까?
  2. Argo CD: 어떤 source를 어느 cluster와 namespace에 적용할 수 있습니까?
  3. Kubernetes RBAC: Argo CD가 대상 클러스터에서 실제로 어떤 리소스를 읽고 쓸 수 있습니까?

GitLab에서 변경을 승인했더라도 Argo CD가 배포할 수 있는 클러스터는 제한해야 합니다. Argo CD RBAC을 엄격하게 설정하는 것과 함께, Git 저장소의 운영 values를 merge할 수 있는 사람도 제한해야 합니다. 변경 승인, 배포 대상, 실제 리소스 접근 권한을 각각 제한해야 같은 환경의 보호 범위를 유지할 수 있습니다.

“파이프라인 실행자의 Argo CD 권한”이 배포 권한은 아니다

Argo CD 자동 동기화를 사용하면 pipeline은 Argo CD API를 호출하지 않습니다. 따라서 pipeline을 시작한 사람의 Argo CD UI 권한으로 자동 배포의 허용 여부가 결정되지는 않습니다. 자동 배포를 통제하려면 다음 권한과 설정을 관리해야 합니다.

  • GitLab에서 대상 환경 values 변경을 merge할 권한
  • Argo CD Application이 참조하도록 허용된 source와 destination
  • Argo CD application-controller의 대상 클러스터 Kubernetes 권한

운영자가 Argo CD에서 수동 sync할 권한은 별도의 비상 운영 권한입니다. 정상적인 prod 승격을 수동 sync 버튼 권한에 의존시키지 않습니다. 정상 경로에서는 승인된 Git 변경이 merge되면 Argo CD가 자동으로 수렴합니다.

배포 승인 이력을 확인할 때도 이 구분을 적용합니다. “누가 pipeline 버튼을 눌렀는가”만으로는 승인자를 알 수 없습니다. 정상 배포의 승인 기록은 “누가 prod values 변경을 제안·검토·merge했는가”를 기준으로 확인합니다.

1. 세 클러스터에 namespace와 Harbor pull Secret 준비

세 kubeconfig context가 올바른 클러스터를 가리키는지 먼저 확인합니다.

bash
kubectl config get-contexts
kubectl --context dev-admin cluster-info
kubectl --context stg-admin cluster-info
kubectl --context prod-admin cluster-info

pull 전용 Harbor Robot Account를 환경변수로 주입한 관리 터미널에서 Docker config를 한 번 만듭니다.

bash
export HARBOR_REGISTRY=harbor.lab.example.com
read -r -p "Harbor pull robot username: " HARBOR_PULL_USER
read -r -s -p "Harbor pull robot secret: " HARBOR_PULL_PASSWORD
printf '\n'
export HARBOR_PULL_USER HARBOR_PULL_PASSWORD

HARBOR_AUTH="$(printf '%s:%s' "$HARBOR_PULL_USER" "$HARBOR_PULL_PASSWORD" | base64 | tr -d '\n')"
DOCKER_CONFIG_JSON="$(printf '{"auths":{"%s":{"auth":"%s"}}}' "$HARBOR_REGISTRY" "$HARBOR_AUTH" | base64 | tr -d '\n')"
export DOCKER_CONFIG_JSON

각 context에 namespace와 Secret을 적용합니다.

bash
for CONTEXT in dev-admin stg-admin prod-admin; do
  envsubst <<'EOF' | kubectl --context "$CONTEXT" apply -f -
apiVersion: v1
kind: Namespace
metadata:
  name: student-01
  labels:
    gateway-access: "true"
---
apiVersion: v1
kind: Secret
metadata:
  name: harbor-pull
  namespace: student-01
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: ${DOCKER_CONFIG_JSON}
EOF
done

unset HARBOR_AUTH DOCKER_CONFIG_JSON

운영에서는 같은 Robot secret을 모든 환경에 복제하기보다 환경별 pull 계정을 분리하면 한 secret 유출의 범위를 줄일 수 있습니다.

2. Argo CD에 stg와 prod 외부 클러스터 등록

SSO로 Argo CD CLI에 로그인합니다.

bash
export ARGOCD_SERVER=argocd.lab.example.com
argocd login "$ARGOCD_SERVER" --sso

dev는 이미 Argo CD의 in-cluster destination이므로 다시 등록하지 않습니다. 같은 물리 클러스터를 외부 API URL로 중복 등록하면 Argo CD에는 다른 cluster identity로 보여 기존 Application 이관이 꼬일 수 있습니다. 관리 대상 namespace를 명시해 stg와 prod만 등록합니다.

bash
argocd cluster add stg-admin  --name stg  --namespace student-01
argocd cluster add prod-admin --name prod --namespace student-01

argocd cluster add는 지정하지 않으면 대상 클러스터에 기본 관리용 ServiceAccount를 만듭니다. 빠른 실습에는 편하지만 생성된 Kubernetes 권한이 조직의 최소 권한 기준을 만족하는지 반드시 검토해야 합니다. 운영에서는 전용 ServiceAccount와 제한된 Role/ClusterRole을 미리 만들고 --service-account로 사용하거나, namespace와 resource kind 범위를 직접 설계합니다.

등록 결과를 확인합니다.

bash
argocd cluster list

예상 형태는 다음과 같습니다. SERVER 값은 이후 manifest에 그대로 사용합니다.

text
SERVER                       NAME   STATUS
https://kubernetes.default.svc in-cluster Successful
https://192.0.2.22:6443      stg    Successful
https://192.0.2.23:6443      prod   Successful

Argo CD는 외부 cluster credential을 argocd namespace의 argocd.argoproj.io/secret-type: cluster 라벨이 있는 Secret으로 저장합니다. 이 Secret을 읽을 권한은 곧 대상 클러스터 접근 권한이므로 일반 사용자에게 허용하지 않습니다.

3. 환경별 Helm values 만들기

이전 글의 배포 저장소에서 values-dev.yaml에 더해 두 파일을 만듭니다.

charts/web/values-stg.yaml

yaml
replicaCount: 2

image:
  repository: harbor.lab.example.com/mlops-training/student-01-web
  tag: "PROMOTE_FROM_DEV"

gateway:
  hostname: web-stg.lab.example.com

resources:
  requests:
    cpu: 100m
    memory: 96Mi
  limits:
    cpu: 500m
    memory: 256Mi

charts/web/values-prod.yaml

yaml
replicaCount: 3

image:
  repository: harbor.lab.example.com/mlops-training/student-01-web
  tag: "PROMOTE_FROM_STG"

gateway:
  hostname: web.lab.example.com

resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: "1"
    memory: 512Mi

sentinel 문자열은 아직 승인된 이미지가 없다는 뜻입니다. 첫 ApplicationSet 적용 전 dev에서 검증한 실제 commit SHA tag로 stg와 prod를 순서대로 승격합니다.

Chart가 모든 환경에서 렌더되는지 확인합니다.

bash
for ENV in dev stg prod; do
  helm lint charts/web -f "charts/web/values-${ENV}.yaml"
  helm template web charts/web -f "charts/web/values-${ENV}.yaml" > "/tmp/web-${ENV}.yaml"
done

4. AppProject로 환경 경계 만들기

Argo CD AppProject는 UI에서 Application을 묶는 역할 외에도 허용 source, destination, resource kind를 제한하는 정책 객체입니다. 환경별 배포 범위를 제한하기 위해 다음 파일을 argocd/projects-multicluster.yaml로 저장합니다. server 값은 자신의 argocd cluster list 출력과 정확히 일치시켜야 합니다.

yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: web-dev
  namespace: argocd
spec:
  sourceRepos:
    - https://gitlab.lab.example.com/platform/student-01-web-deploy.git
  destinations:
    - server: https://kubernetes.default.svc
      namespace: student-01
  namespaceResourceWhitelist:
    - group: apps
      kind: Deployment
    - group: ""
      kind: Service
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: web-stg
  namespace: argocd
spec:
  sourceRepos:
    - https://gitlab.lab.example.com/platform/student-01-web-deploy.git
  destinations:
    - server: https://192.0.2.22:6443
      namespace: student-01
  namespaceResourceWhitelist:
    - group: apps
      kind: Deployment
    - group: ""
      kind: Service
    - group: gateway.networking.k8s.io
      kind: HTTPRoute
---
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: web-prod
  namespace: argocd
spec:
  sourceRepos:
    - https://gitlab.lab.example.com/platform/student-01-web-deploy.git
  destinations:
    - server: https://192.0.2.23:6443
      namespace: student-01
  namespaceResourceWhitelist:
    - group: apps
      kind: Deployment
    - group: ""
      kind: Service
    - group: gateway.networking.k8s.io
      kind: HTTPRoute

이제 web-stg Project의 Application은 dev나 prod server를 destination으로 바꾸더라도 거부됩니다. GitLab 승인 규칙이 잘못 구성돼도 Argo CD가 두 번째 경계를 제공합니다.

실제 Chart가 ClusterRole, CRD, Namespace 같은 cluster-scoped 리소스를 만들 필요가 없다면 clusterResourceWhitelist를 추가하지 않습니다. namespace-scoped resource도 운영 정책에 맞게 더 좁힐 수 있습니다.

5. ApplicationSet으로 세 Application 생성

argocd/applicationset-web.yaml을 만듭니다.

yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: student-01-web
  namespace: argocd
spec:
  goTemplate: true
  goTemplateOptions:
    - missingkey=error
  generators:
    - list:
        elements:
          - env: dev
            server: https://kubernetes.default.svc
            project: web-dev
          - env: stg
            server: https://192.0.2.22:6443
            project: web-stg
          - env: prod
            server: https://192.0.2.23:6443
            project: web-prod
  template:
    metadata:
      name: 'student-01-web-{{.env}}'
      labels:
        app.kubernetes.io/part-of: student-01-web
        environment: '{{.env}}'
    spec:
      project: '{{.project}}'
      source:
        repoURL: https://gitlab.lab.example.com/platform/student-01-web-deploy.git
        targetRevision: main
        path: charts/web
        helm:
          releaseName: web
          valueFiles:
            - 'values-{{.env}}.yaml'
      destination:
        server: '{{.server}}'
        namespace: student-01
      syncPolicy:
        automated:
          enabled: true
          prune: true
          selfHeal: true

List Generator를 사용한 이유는 환경과 대상 server의 대응을 한눈에 검토하기 위해서입니다. 등록된 cluster Secret의 label을 기준으로 자동 발견해야 하는 대규모 환경이라면 Cluster Generator가 더 적합합니다. 자동 발견을 사용하더라도 prod label을 누가 바꿀 수 있는지, 새 클러스터가 어떤 Application을 자동으로 받는지 정책을 먼저 정합니다.

missingkey=error는 오타나 누락된 env, server, project를 빈 문자열로 조용히 렌더하지 않고 실패하게 합니다.

변경을 push합니다.

bash
git add charts/web/values-stg.yaml charts/web/values-prod.yaml \
  argocd/projects-multicluster.yaml argocd/applicationset-web.yaml
git commit -m "feat: add multi-cluster environments"
git push

관리 클러스터에 선언을 적용합니다.

10편을 그대로 수행했다면 student-01-web-dev는 독립 Application으로 이미 존재합니다. ApplicationSet이 같은 이름의 객체를 만들기 전에 기존 Application만 non-cascade 삭제해 workload는 유지하고 관리 주체를 이관합니다. 먼저 현재 source와 destination을 확인합니다.

bash
kubectl apply -f argocd/projects-multicluster.yaml

if kubectl get application -n argocd student-01-web-dev >/dev/null 2>&1; then
  kubectl get application -n argocd student-01-web-dev \
    -o jsonpath='{.spec.source.repoURL}{" "}{.spec.source.path}{" "}{.spec.destination.server}{"\n"}'
  argocd app delete student-01-web-dev --cascade=false --yes
fi

kubectl apply -f argocd/applicationset-web.yaml

--cascade=false를 지정하면 기존 Deployment·Service·HTTPRoute는 유지하고 Application 객체만 제거합니다. workload를 유지한 채 관리 주체를 이관하기 위해 필요한 옵션입니다. 이관 전후의 repo, path, values, destination이 같은지 검토하고, 짧은 관리 공백 동안에는 배포 저장소를 변경하지 않습니다. ApplicationSet이 새 dev Application을 만든 뒤 정상적으로 동기화되는지 확인합니다.

ApplicationSet과 생성된 Application을 확인합니다.

bash
kubectl -n argocd get applicationset student-01-web
kubectl -n argocd get application \
  -l app.kubernetes.io/part-of=student-01-web \
  -o custom-columns=NAME:.metadata.name,PROJECT:.spec.project,SERVER:.spec.destination.server,SYNC:.status.sync.status,HEALTH:.status.health.status

PROMOTE_FROM_DEV, PROMOTE_FROM_STG가 실제 image tag가 아니므로 stg와 prod는 초기에는 실패할 수 있습니다. 다음 승격 단계에서 같은 dev image를 순서대로 반영합니다.

6. dev에서 stg로 승격

먼저 values-dev.yaml이 가리키는 tag가 dev에서 실제 검증된 값인지 확인합니다.

bash
DEV_TAG="$(yq -r '.image.tag' charts/web/values-dev.yaml)"
test -n "$DEV_TAG"
test "$DEV_TAG" != "REPLACED_BY_CI"
kubectl --context dev-admin -n student-01 get deployment web \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

Harbor에서 이 tag의 스캔 결과와 불변 상태를 확인한 뒤 승격 branch를 만듭니다.

bash
SHORT_TAG="$(printf '%s' "$DEV_TAG" | cut -c1-12)"
git switch -c "promote/stg-${SHORT_TAG}"
export RELEASE_TAG="$DEV_TAG"
yq -i '.image.tag = strenv(RELEASE_TAG)' charts/web/values-stg.yaml
helm lint charts/web -f charts/web/values-stg.yaml
git add charts/web/values-stg.yaml
git commit -m "promote(stg): student-01-web ${SHORT_TAG}"
git push -u origin "promote/stg-${SHORT_TAG}"

GitLab에서 main 대상 Merge Request를 만듭니다. reviewer는 다음을 확인합니다.

  • values-stg.yaml 외의 예상치 못한 변경이 없습니까?
  • tag가 현재 dev가 실행 중인 값과 정확히 같습니까?
  • Harbor에서 그 tag가 스캔과 정책을 통과했습니까?
  • stg resource와 hostname 설정이 유지됐습니까?

MR이 merge되면 Argo CD가 student-01-web-stg를 자동 sync합니다.

bash
argocd app get student-01-web-stg
kubectl --context stg-admin -n student-01 rollout status deployment/web --timeout=180s
kubectl --context stg-admin -n student-01 get deployment web \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

dev와 stg 출력의 image tag가 같아야 합니다. 환경별로 다른 것은 replica, resource, hostname 같은 배포 설정이지 실행 artifact가 아닙니다.

7. stg에서 prod로 승격

stg 검증이 끝나면 같은 방식으로 현재 stg tag를 prod에 반영합니다.

bash
git switch main
git pull --ff-only
STG_TAG="$(yq -r '.image.tag' charts/web/values-stg.yaml)"
SHORT_TAG="$(printf '%s' "$STG_TAG" | cut -c1-12)"
git switch -c "promote/prod-${SHORT_TAG}"
export RELEASE_TAG="$STG_TAG"
yq -i '.image.tag = strenv(RELEASE_TAG)' charts/web/values-prod.yaml
helm lint charts/web -f charts/web/values-prod.yaml
git add charts/web/values-prod.yaml
git commit -m "promote(prod): student-01-web ${SHORT_TAG}"
git push -u origin "promote/prod-${SHORT_TAG}"

prod MR에는 stg보다 강한 승인 규칙을 적용합니다. 승격 시 이미지를 다시 빌드하거나 새 tag로 복사하지 않습니다. 같은 Harbor artifact를 가리켜야 stg에서 확인한 대상과 prod에서 실행하는 대상이 동일합니다.

merge 후 확인합니다.

bash
argocd app get student-01-web-prod
kubectl --context prod-admin -n student-01 rollout status deployment/web --timeout=300s
kubectl --context prod-admin -n student-01 get deployment web \
  -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

세 환경의 tag를 한 번에 비교합니다.

bash
for CONTEXT in dev-admin stg-admin prod-admin; do
  printf '%-12s ' "$CONTEXT"
  kubectl --context "$CONTEXT" -n student-01 get deployment web \
    -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
done

정상 승격 직후에는 세 줄이 같은 image tag를 가리킵니다.

8. GitLab을 정상 배포 승인 지점으로 만들기

보호 브랜치와 Merge Request

배포 저장소의 main은 direct push를 막고 Merge Request로만 변경되게 합니다. 단, 이전 글의 dev 자동 갱신이 main에 직접 push하도록 구성되어 있다면 다음 중 하나를 명시적으로 선택해야 합니다.

  • dev 자동화 전용 branch를 만들고 dev Application만 그 branch를 추적합니다.
  • CI가 branch와 Merge Request를 만들고 자동 merge 정책을 둡니다.
  • 제한된 bot만 특정 규칙으로 push하게 하되 stg/prod 파일은 CODEOWNERS로 보호합니다.

운영에서는 첫 번째나 두 번째 방식이 변경 경계를 더 분명히 만듭니다.

CODEOWNERS 예시

배포 저장소 루트의 CODEOWNERS에 경로별 소유자를 선언할 수 있습니다.

text
/charts/web/values-dev.yaml   @platform/dev-approvers
/charts/web/values-stg.yaml   @platform/stg-approvers
/charts/web/values-prod.yaml  @platform/prod-approvers
/argocd/                      @platform/platform-admins

CODEOWNERS 승인을 필수로 강제하는 기능과 세부 승인 규칙은 GitLab edition과 구독에 따라 다를 수 있습니다. 사용 중인 GitLab의 공식 문서와 라이선스를 확인하고, 기능이 없다면 보호 브랜치와 명시적 Maintainer 승인 절차로 같은 통제 목표를 구현합니다.

9. Argo CD RBAC은 수동 운영 권한을 제한한다

자동 sync를 기본으로 하면 일반 배포자는 Argo CD sync 권한이 없어도 됩니다. 읽기 권한으로 상태를 보고, prod 수동 sync는 비상 그룹에만 허용합니다.

기존 argocd-rbac-cm 전체를 새 manifest로 덮어쓰면 5편의 기존 역할과 policy.matchMode를 잃을 수 있습니다. Argo CD가 병합해 읽는 별도 policy.<이름>.csv 키만 추가합니다.

yaml
# argocd/rbac-multicluster-patch.yaml
data:
  policy.multicluster.csv: |
    p, role:web-viewer, applications, get, web-dev/student-01-web-dev, allow
    p, role:web-viewer, applications, get, web-stg/student-01-web-stg, allow
    p, role:web-viewer, applications, get, web-prod/student-01-web-prod, allow
    p, role:web-viewer, logs, get, web-dev/student-01-web-dev, allow
    p, role:web-viewer, logs, get, web-stg/student-01-web-stg, allow

    p, role:prod-breakglass, applications, get, web-prod/student-01-web-prod, allow
    p, role:prod-breakglass, applications, sync, web-prod/student-01-web-prod, allow

    g, platform-viewers, role:web-viewer
    g, prod-breakglass, role:prod-breakglass
bash
kubectl get configmap -n argocd argocd-rbac-cm -o yaml > /tmp/argocd-rbac-before.yaml
kubectl patch configmap -n argocd argocd-rbac-cm \
  --type merge \
  --patch-file argocd/rbac-multicluster-patch.yaml
kubectl get configmap -n argocd argocd-rbac-cm \
  -o jsonpath='{.data.policy\.multicluster\.csv}'

<project>/<application> 형식을 정확히 사용합니다. patch 전 백업은 민감한 그룹 매핑을 포함할 수 있으므로 공유하거나 Git에 넣지 않고 검증 후 안전하게 삭제합니다. 비상 그룹 구성원, 사용 조건, 만료, 사후 검토 절차도 별도로 정합니다.

10. Sync Window로 배포 시간대 제한하기

Git merge 승인이 곧바로 prod 반영되면 안 되는 조직은 web-prod AppProject에 allow Sync Window를 둘 수 있습니다.

yaml
spec:
  syncWindows:
    - kind: allow
      schedule: '0 10 * * 1-5'
      timeZone: Asia/Seoul
      duration: 8h
      applications:
        - student-01-web-prod
      manualSync: true

이 예시는 평일 10시부터 8시간 동안 prod sync를 허용하고, 비상 수동 sync 우회 가능성을 남깁니다. 조직의 실제 변경 시간대와 당직 체계에 맞게 바꿉니다. allow window 밖에서 MR이 merge되면 Application은 OutOfSync로 기다렸다가 다음 허용 시간에 수렴합니다.

Sync Window는 승인 절차를 대신하지 않습니다. “누가 무엇을 승인했는가”는 GitLab에, “언제 적용 가능한가”는 Argo CD에 둡니다.

11. self-heal 검증과 롤백

dev에서 drift 복구 확인

운영이 아닌 dev에서만 수동 변경을 만들어 봅니다.

bash
kubectl --context dev-admin -n student-01 scale deployment web --replicas=5
kubectl --context dev-admin -n student-01 get deployment web -w

selfHeal: true이므로 잠시 후 replicas가 values-dev.yaml의 값으로 돌아와야 합니다. 돌아오지 않으면 Application의 auto-sync와 self-heal 설정, resource ownership, sync 오류를 확인합니다.

롤백은 promotion commit을 revert

prod 장애 시 배포 저장소에서 직전 promotion commit을 되돌리는 MR을 만듭니다.

bash
git switch -c rollback/prod
read -r -p "되돌릴 prod promotion commit SHA: " PROD_PROMOTION_COMMIT
printf '%s' "${PROD_PROMOTION_COMMIT}" | grep -Eq '^[0-9a-fA-F]{7,40}$' || {
  echo "유효한 Git commit SHA가 아닙니다." >&2
  exit 1
}
git revert "${PROD_PROMOTION_COMMIT}"
git push -u origin rollback/prod

검토 후 merge하면 Argo CD가 이전 image tag를 적용합니다. 자동 sync가 켜진 Application에서는 Argo CD의 과거 revision rollback 기능에 의존하지 않습니다. Argo CD가 적용할 원하는 상태를 Git에서 읽으므로 정상 롤백도 Git 변경으로 남겨야 합니다.

DB schema처럼 이전 버전으로 단순 복귀할 수 없는 변경은 이미지 롤백과 별도의 호환성·마이그레이션 전략이 필요합니다.

검증 체크리스트

  • argocd cluster list의 세 cluster가 Successful입니까?
  • 각 AppProject가 정확히 하나의 server와 student-01 namespace만 허용합니까?
  • ApplicationSet이 정확히 세 Application을 만들었습니까?
  • 각 Application의 spec.project, destination server, values file이 환경과 일치합니까?
  • dev, stg, prod가 승격 완료 후 같은 immutable image tag 또는 digest를 가리킵니까?
  • prod values 변경은 보호 브랜치와 승인 MR 없이는 merge할 수 없습니까?
  • 일반 사용자는 Argo CD에서 prod sync를 수동 실행할 수 없습니까?
  • rollback이 Git revert로 기록됩니까?

자주 발생하는 문제

cluster 상태가 Unknown 또는 Failed다

중앙 Argo CD에서 대상 API server로 네트워크가 열려 있는지, cluster CA와 server 이름이 일치하는지, ServiceAccount token이 유효한지 확인합니다. 방화벽을 열기 전에 필요한 source, destination, port만 정의합니다.

bash
argocd cluster get dev
kubectl -n argocd get secret -l argocd.argoproj.io/secret-type=cluster

Secret의 token 값을 출력하지 않습니다.

application destination server is not permitted 오류가 난다

ApplicationSet element의 server와 AppProject destination의 server, 등록된 cluster server가 문자열까지 정확히 같은지 비교합니다. 클러스터 이름 prod와 API URL은 같은 필드가 아닙니다.

ApplicationSet이 Application을 만들지 않는다

bash
kubectl -n argocd describe applicationset student-01-web
kubectl -n argocd get applicationset student-01-web -o yaml

Go template의 missing key, 잘못된 project, 존재하지 않는 values file을 확인합니다. missingkey=error가 원인을 빨리 드러내는 데 도움이 됩니다.

Argo CD가 values-prod.yaml not found라고 한다

helm.valueFiles 경로는 Chart의 path를 기준으로 해석됩니다. charts/web/values-prod.yaml을 path: charts/web에서 읽을 때는 values-prod.yaml로 적습니다.

prod MR을 merge했는데 배포되지 않는다

Application이 OutOfSync라면 Sync Window, automated sync의 enabled, 이전 sync 실패 상태를 확인합니다. Synced인데 image가 예상과 다르면 Helm parameter override가 Git values보다 우선하고 있지 않은지 확인합니다. 운영에서는 UI의 임시 parameter override를 사용하지 않는 편이 좋습니다.

세 환경에서 같은 tag인데 내용이 다르다

tag가 덮어써진 것입니다. Harbor의 digest를 환경별로 비교하고 Tag Immutability를 적용합니다. 가장 강한 방식은 Chart가 image digest 참조를 지원하도록 만들어 digest 자체를 승격하는 것입니다.

한 cluster 장애가 전체 ApplicationSet을 막는다

Application은 각각 독립적으로 reconcile됩니다. 문제가 있는 cluster의 Application 상태와 controller 로그를 분리해 봅니다. 급하게 전체 ApplicationSet을 수정해 정상 cluster까지 destination을 바꾸지 않습니다. 필요하면 해당 cluster Secret에 reconcile 중지 annotation을 적용하는 운영 절차를 검토합니다.

보안과 운영 설계

  • 외부 cluster credential Secret은 argocd namespace에서 가장 민감한 자산 중 하나입니다. Kubernetes 암호화 at rest, namespace RBAC, 백업 암호화와 회전을 적용합니다.
  • argocd cluster add가 만든 기본 권한을 그대로 신뢰하지 말고 대상 namespace와 resource kind에 맞게 줄입니다.
  • AppProject의 sourceRepos, destinations, resource whitelist에 *를 불필요하게 사용하지 않습니다.
  • ApplicationSet 변경은 여러 cluster에 동시에 영향을 줍니다. argocd/ 경로에 강한 CODEOWNERS와 승인 규칙을 적용합니다.
  • prod 격리가 특히 중요하면 별도 Argo CD instance, 별도 관리 cluster, 별도 Harbor Project와 pull 계정으로 장애 영역을 더 나눕니다.
  • CI는 dev 원하는 상태를 갱신할 수 있어도 stg/prod 파일을 직접 변경하지 못하게 token과 저장소 정책을 분리합니다.
  • 환경 승격 시 이미지를 다시 빌드하지 않습니다. 같은 digest 또는 불변 tag를 이동시킵니다.
  • 수동 kubectl 변경은 비상 절차로 제한하고 반드시 Git에 후속 반영하거나 self-heal로 폐기합니다.
  • 자동 sync와 prune을 켤 때 빈 렌더, 잘못된 path, 대량 삭제를 탐지할 사전 검증과 알림을 둡니다.

요약

이 구성에서는 pipeline이 다른 cluster에 직접 접속하지 않고 Git 변경으로 환경을 승격합니다. dev에서 검증한 같은 이미지 tag나 digest를 values-stg.yaml, values-prod.yaml에 순서대로 반영하는 방식입니다. GitLab Merge Request에서 정상 배포를 승인하고, Argo CD AppProject에서 허용 source와 destination을 제한합니다. Kubernetes RBAC은 Argo CD가 실제 cluster에서 행사할 수 있는 권한을 제한합니다.

ApplicationSet은 반복되는 Application을 줄여 주지만 변경 범위도 넓힙니다. 따라서 generator와 template을 강하게 보호하고, 환경별 AppProject를 두며, prod는 승인 규칙과 필요 시 Sync Window를 추가합니다. 롤백 역시 Git revert로 수행해 원하는 상태와 감사 이력을 하나로 유지합니다.

공식 참고자료