
2주 동안 홈랩 인프라부터 회사 홈페이지, n8n 워크플로우, Claude Code 스킬까지 9개 프로젝트를 만들었다. 돌이켜보면 모든 프로젝트에 공통으로 관통하는 패턴이 하나 있었다. 설정을 코드처럼 관리한다는 것.
수동으로 서버에 SSH 접속해서 설정 파일을 고치던 시절이 있었다. 뭘 바꿨는지 기억이 안 나고 다른 서버에 같은 설정을 적용하려면 처음부터 다시 해야 했다. 설정을 코드로 관리하면 이게 달라진다.
버전 관리가 된다. 누가 언제 뭘 바꿨는지 git log로 추적할 수 있고 문제가 생기면 git revert 한 방이면 이전 상태로 돌아간다.
재현이 가능해진다. 새 환경을 구성할 때 "저번에 어떻게 했더라?"가 아니라 git clone으로 시작하면 된다. 홈랩 K8s 클러스터가 날아가도 매니페스트 레포만 있으면 전체 인프라를 복원할 수 있다.
환경별 분기도 깔끔해진다. staging과 production의 차이를 코드로 명시하니까 "staging에서 됐는데 production에서 안 돼요"류의 사고가 줄어든다.
이 글에서는 실제 프로젝트에서 사용한 도구별 Config as Code 접근법을 비교하고 삽질에서 얻은 교훈을 정리해본다.
K8s 매니페스트를 관리할 때 Kustomize의 base/overlay 패턴을 가장 많이 썼다. museck 홈페이지 배포가 대표적이다.
base 디렉토리에 Deployment, Service, ConfigMap 같은 공통 리소스를 넣고 staging과 production overlay에서 이미지 태그와 도메인만 덮어쓰는 구조다.
# base/kustomization.yaml
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
# overlays/staging/kustomization.yaml
bases:
- ../../base
images:
- name: gitea.xssh.org/admin/museck
newTag: "abc123" # CI가 자동으로 업데이트
# overlays/production/kustomization.yaml
bases:
- ../../base
images:
- name: gitea.xssh.org/admin/museck
newTag: "def456" # 수동 workflow_dispatch로 업데이트staging은 master push마다 CI가 태그를 자동 갱신하고 production은 workflow_dispatch로 명시적으로 승격한다. 같은 base를 공유하니까 staging에서 테스트한 구조가 그대로 production에 들어간다.
cert-manager나 ArgoCD 같은 외부 도구는 직접 매니페스트를 작성하기보다 Helm 차트를 쓴다. 이때 설정 커스터마이징은 values.yaml 파일 하나로 해결한다.
Postiz 배포가 가장 복잡했다. ArgoCD의 multi-source 기능으로 OCI Helm 차트와 Git values를 조합했는데 구조는 이렇다.
# ArgoCD Application - multi-source
spec:
sources:
- repoURL: ghcr.io/gitroomhq/postiz-helmchart/charts/postiz-app
chart: postiz-app
targetRevision: 1.0.5
helm:
valueFiles:
- $values/postiz-values.yaml
- repoURL: https://gitea.xssh.org/homelab/homelab.git
ref: values외부 Helm 차트의 기본값을 우리 환경에 맞게 override하는 values 파일을 Git에서 관리한다. 차트 버전 업그레이드할 때 뭐가 바뀌었는지 diff로 바로 확인할 수 있어서 좋다.
여기서 삽질을 많이 했다. Bitnami subchart의 서비스 이름이 fullnameOverride가 아닌 릴리스 이름 기반이라 DATABASE_URL이 틀렸고, Temporal의 ConfigMap을 /etc/temporal에 마운트하면 entrypoint.sh를 덮어써서 컨테이너가 시작도 안 됐다. values.yaml 한 줄의 실수가 연쇄 장애로 이어지는 걸 13번의 커밋으로 체감했다.
TLS 인증서를 코드로 관리하는 건 처음엔 좀 과하다 싶었다. 한번 발급하면 자동 갱신되니까. 근데 실제로 해보니 이게 없으면 안 된다.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-tokenClusterIssuer와 Certificate 리소스를 YAML로 정의해두면 클러스터를 재구축해도 인증서 발급이 자동으로 된다. home.lab 도메인과 mkcert 자체 서명 인증서를 쓰다가 xssh.org로 전환할 때도 YAML 파일 몇 개만 수정하면 끝이었다.
와일드카드 인증서 한 장(*.xssh.org)을 Traefik TLSStore default에 등록해두니 새 서비스를 추가할 때 TLS 설정은 tls: {} 한 줄로 충분했다.
n8n 같은 노코드 도구에서 Config as Code를 하려면 좀 다른 접근이 필요하다. 워크플로우는 GUI에서 노드를 연결해서 만드는데 이걸 어떻게 코드로 관리할까?
n8n API로 워크플로우 JSON을 export해서 Git에 저장하고 deploy.sh 스크립트로 다시 import하는 방식을 택했다.
# deploy.sh - read-only 필드 제거 후 배포
BODY=$(python3 -c "
import json, sys
d = json.load(open('$WORKFLOW_FILE'))
for k in ('id', 'tags', 'active', 'createdAt', 'updatedAt', 'versionId',
'staticData', 'meta', 'pinData'):
d.pop(k, None)
json.dump(d, sys.stdout, ensure_ascii=False)
")
curl -X PUT "$N8N_URL/api/v1/workflows/$WF_ID" \
-H "X-N8N-API-KEY: $API_KEY" \
-d "$BODY"까다로운 점이 하나 있다. n8n API가 export한 JSON에 포함된 read-only 필드(id, tags, active 등)를 import 시 거부한다. Python 원라이너로 이 필드를 제거하는 로직을 넣어야 했다. 노코드 도구라고 코드가 필요 없는 게 아니라 "다른 종류의 코드"가 필요한 셈이다.
코드 품질 도구의 설정도 Config as Code에 해당한다. husky + lint-staged 설정이 package.json에 들어있으면 팀원 누구나 git clone 후 바로 같은 품질 검증 파이프라인을 쓸 수 있다.
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --max-warnings 0", "prettier --write"],
"*.{js,cjs,mjs,json,css,md}": ["prettier --write"]
}
}pre-commit에서 변경 파일만 린트하고 pre-push에서 전체 타입 체크를 돌리는 이중 구조다. "Shift-Left 검증"이라 부르는데 문제를 CI 빌드 실패가 아닌 커밋 시점에서 잡겠다는 발상이다. 설정 파일 하나로 팀 전체의 코드 품질 기준을 강제할 수 있다.
가장 흥미로웠던 Config as Code 사례는 Claude Code의 스킬 파일이다. AI 코딩 어시스턴트의 동작을 마크다운 파일로 정의하고 Git으로 관리한다니. 꽤 신선했다.
skill-version-manager를 만들면서 배운 건 AI 도구의 설정에도 소프트웨어와 같은 원칙이 적용된다는 점이다. 처음엔 설치 경로를 하드코딩해뒀는데(~/Desktop/vibe/workspace-skill/) WSL2 환경이 바뀌자 바로 깨졌다. CWD 기반 자동 감지로 리팩터링해서 환경 독립성을 확보했다.
스킬 파일은 ~/.claude/skills/ 또는 {project}/.claude/skills/에 마크다운으로 저장된다. 버전 관리와 변경 이력 추적, 환경별 분기(user-level vs project-level)까지 전부 Config as Code 원칙과 맞아떨어진다.
9개 프로젝트를 관통하는 환경 분기 패턴을 정리하면 크게 세 가지로 나뉜다.
파일 분기형이 있다. Kustomize overlay처럼 staging/production 디렉토리를 분리하고 각각 다른 설정 파일을 두는 방식이다. 뭐가 다른지 명확히 보이지만 파일이 늘어나는 단점이 있다.
값 주입형도 있다. Helm values나 환경변수처럼 하나의 템플릿에 환경별 값을 주입하는 패턴이다. museck의 ConfigMap이 이 방식인데 K8s Secret이나 Kustomize overlays에서 환경변수를 override한다.
프로모션형도 흔하다. staging에서 검증된 설정(이미지 태그)을 production으로 승격하는 방식으로 museck의 배포 워크플로우가 여기에 해당한다. 빈 태그 입력 시 staging의 현재 태그를 자동으로 가져오는 기능까지 붙였다.
# deploy-production.yaml - 태그 자동 해석
- name: Resolve tag
run: |
TAG="${{ github.event.inputs.tag }}"
if [ -z "$TAG" ]; then
TAG=$(grep 'newTag:' staging/kustomization.yaml | sed 's/.*newTag: *"\(.*\)"/\1/')
fiConfig as Code를 실천하면서 가장 많이 겪은 문제는 "사소한 설정 하나가 연쇄 장애를 일으킨다"는 거다.
Postiz 배포에서 Bitnami subchart의 서비스 이름 하나 틀린 게 DATABASE_URL 오류로 이어졌고 Temporal ConfigMap 마운트 경로 하나가 entrypoint.sh를 덮어써서 전체 서비스가 안 떴다. n8n API는 read-only 필드가 body에 있으면 400 에러를 던졌다. 설정 파일의 한 줄이 서비스 전체를 먹통으로 만든다.
이런 경험에서 몇 가지 원칙을 세웠다.
설정 변경은 반드시 커밋한다. "잠깐 테스트"로 kubectl edit 하는 순간 drift가 시작된다. ArgoCD의 auto-sync가 이걸 강제해준다.
CI/CD 파이프라인에 검증 단계를 넣는다. production 배포 전 이미지 존재 확인이나 변경 없으면 커밋을 스킵하는 안전장치가 실수를 막아준다.
실패한 설정도 기록으로 남긴다. Postiz의 13번 커밋처럼 삽질 과정이 git history에 남으면 나중에 비슷한 문제를 만났을 때 큰 도움이 된다.
Kustomize overlay든 Helm values든 n8n JSON이든 Claude Code 스킬이든 결국 Config as Code의 본질은 같다. 설정의 현재 상태를 코드로 선언하고 변경 이력을 추적하고 환경별 차이를 명시적으로 관리하는 것.
도구마다 형식은 다르지만 원칙은 같았다. 이걸 지키면 어떤 도구를 쓰든 "지난번에 어떻게 했더라?"에서 해방된다.