무색
기술블로그
에세이
연구
프로덕트
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

[email protected]

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관
INCHEON, KR
GitOps Production 배포 자동화 — 두 갈래 파이프라인
museck 만들기
2026. 1. 8.

GitOps로 Production 배포 자동화하기

GitOpsKubernetesArgoCDCI/CDDevOps

k8s/ 디렉토리가 거슬리기 시작했다

무색 홈페이지의 CI/CD 파이프라인을 지난 글에서 구축했다. master에 push하면 Docker 이미지를 빌드하고 Staging에 자동 배포되는 구조였는데, 한 가지 찝찝한 부분이 있었다. 앱 코드 레포 안에 k8s/ 디렉토리가 들어 있었던 것이다.

앱 코드를 고칠 때마다 K8s 매니페스트가 같이 보이고, 인프라 설정을 바꾸려면 앱 레포에 커밋해야 하는 상황. GitOps의 기본 원칙인 "앱 코드와 인프라 매니페스트의 분리"를 어기고 있었다.

그래서 두 가지를 동시에 진행했다.

  1. K8s 매니페스트를 별도 homelab 레포로 이관
  2. Production 배포 워크플로우 추가 (Staging과 분리된 수동 트리거)

전체 구조

완성된 배포 구조를 먼저 보자.

왼쪽은 기존 Staging 자동 배포 흐름이고, 오른쪽이 이번에 추가한 Production 수동 배포다. 둘 다 최종적으로 homelab 레포의 kustomization.yaml을 수정하고 ArgoCD가 변경을 감지해서 클러스터에 적용한다.

핵심은 앱 레포에서 k8s/ 디렉토리를 완전히 삭제한 것이다. 이제 앱 레포는 순수하게 앱 코드만 관리하고, 인프라 매니페스트는 homelab 레포가 단독으로 책임진다.

Production 배포 워크플로우

Staging은 master push만 하면 알아서 배포된다. Production은 그래서는 안 된다. 검증된 이미지만 올라가야 하니까. workflow_dispatch를 써서 Gitea UI에서 수동으로 트리거하는 방식을 택했다.

워크플로우가 하는 일은 세 가지다.

  1. 이미지 태그 결정 (입력값 또는 Staging에서 자동 해석)
  2. 레지스트리에서 해당 이미지가 실제로 존재하는지 검증 (docker pull)
  3. homelab 레포의 production/kustomization.yaml에 태그 반영

태그 자동 해석

처음에는 Production 배포할 때 이미지 태그를 직접 입력하게 만들었다. 근데 실제로 써보니 매번 Staging에 배포된 태그를 찾아서 복사하는 게 너무 번거로웠다. 대부분의 경우 "Staging에서 잘 돌아가는 버전을 그대로 Production에 올리겠다"는 건데, 그걸 왜 사람이 수동으로 해야 하나.

그래서 태그 입력을 빈칸으로 두면 Staging에서 현재 돌아가는 태그를 자동으로 가져오게 했다.

- name: Resolve tag
  run: |
    TAG="${{ github.event.inputs.tag }}"
    if [ -z "$TAG" ]; then
      git clone --depth 1 \
        https://admin:${{ secrets.DEPLOY_TOKEN }}@gitea.xssh.org/homelab/homelab.git \
        /tmp/homelab
      TAG=$(grep 'newTag:' /tmp/homelab/.../staging/kustomization.yaml \
        | sed 's/.*newTag: *"\(.*\)"/\1/')
      echo "Resolved from staging: $TAG"
    fi
    echo "DEPLOY_TAG=$TAG" >> $GITHUB_ENV

homelab 레포를 shallow clone해서 staging의 kustomization.yaml에서 현재 이미지 태그를 grep으로 뽑아온다. 태그를 직접 입력한 경우에는 그 값을 그대로 쓰고.

Staging에서 Production으로 프로모션

이 패턴의 장점은 Staging과 Production이 완전히 동일한 이미지를 쓴다는 점이다. Staging에서 "잘 돌아간다"고 확인한 바로 그 이미지가 Production에 올라간다. 새로 빌드하지 않는다. 이미지 태그(커밋 SHA)가 같으니 바이너리도 동일하다.

이미지가 정말 레지스트리에 있는지도 검증한다. 오타로 없는 태그를 넣으면 docker pull 단계에서 워크플로우가 실패하니까 실수로 잘못된 태그가 Production에 반영되는 걸 막을 수 있다.

멱등성 보장

같은 태그로 두 번 배포하면 어떻게 될까? kustomization.yaml의 태그 값이 동일하니 실제로 파일이 바뀌지 않는다. 이때 빈 커밋을 만들면 불필요한 ArgoCD sync가 발생한다.

if git diff --staged --quiet; then
  echo "No changes to commit (tag already set)"
else
  git commit -m "deploy: museck production $DEPLOY_TAG"
  git push
fi

git diff --staged --quiet로 변경 사항이 있는지 확인하고, 없으면 커밋을 스킵한다. 단순하지만 없으면 불편한 방어 코드다.

워크플로우 디버깅이라는 고역

CI/CD 워크플로우 디버깅은 로컬에서 돌려볼 수가 없다. 커밋하고 push하고 실행 결과를 보는 수밖에. Production 워크플로우 초기 버전에는 각 단계마다 cat kustomization.yaml을 넣어서 파일이 제대로 수정되는지 확인했다. 디버깅 로그를 남기는 커밋이 여러 개 쌓인 건 좀 민망하지만, 이게 유일한 디버깅 수단이었다.

태그 자동 해석 기능도 처음부터 만든 건 아니다. 몇 번 수동으로 태그를 복사해서 붙여넣다가 "이거 왜 자동이 아니지?"란 생각이 들어서 추가했다. 자동화 도구도 결국 사람이 쓰는 건데, 그 사람의 경험까지 신경 써야 한다.

배운 것

GitOps의 핵심은 "인프라를 코드로 관리한다"가 아니다. 그건 IaC(Infrastructure as Code)의 영역이고, GitOps는 거기에 "앱 코드와 분리해서 독립적으로"라는 조건이 붙는다. 앱 레포에 K8s 매니페스트를 같이 넣으면 IaC는 되지만 GitOps는 아닌 셈이다.

그리고 자동화 파이프라인도 UX가 있다. 실행하는 사람이 편하게 쓸 수 있어야 한다. 태그 자동 해석이 좋은 예시인데, 기술적으로는 사소한 기능이지만 사용 경험을 크게 바꿔준다. 멱등성 보장도 마찬가지다. 같은 버전을 실수로 두 번 배포해도 아무 일도 일어나지 않는 안전장치.

결국 좋은 CI/CD 파이프라인은 "실수해도 괜찮은" 시스템이다. 사람은 실수하니까.

자주 묻는 질문

GitOps에서 앱 코드와 K8s 매니페스트를 분리하는 이유는?
앱 레포와 인프라 레포를 분리하면 각각 독립적으로 변경하고 배포할 수 있다. 앱 코드 변경이 인프라에 영향을 주지 않고, 인프라 설정만 바꿔도 앱 레포에 커밋할 필요가 없다.
Staging에서 Production으로 이미지를 프로모션하는 방법은?
workflow_dispatch로 수동 트리거하면 Staging에서 현재 돌아가는 이미지 태그를 자동으로 읽어와 Production kustomization.yaml에 반영한다. 새로 빌드하지 않고 동일한 이미지를 재사용한다.
같은 태그로 두 번 배포하면 어떻게 되나?
git diff --staged --quiet로 변경 여부를 확인하고, 태그가 동일하면 커밋을 스킵한다. 불필요한 ArgoCD sync를 방지하는 멱등성 보장 방식이다.
museck 만들기(5/10)
Prev

Docker 빌드에서 터진 7가지 호환성 지뢰: ESM + React 19 + PayloadCMS

Next

Tailwind 토큰 바꿨을 뿐인데 홈페이지가 브루탈리즘이 됐다