들어가며
사내 프로젝트를 진행하다 보면 "배포"라는 단순한 행위가 생각보다 큰 장벽이 되는 경우가 있다. 기존에 제공되는 CI/CD 파이프라인의 복잡도가 높고, 내부망과 Public Cloud가 분리된 환경이라면 더욱 그렇다. 개발팀이 기능 개발보다 배포 파이프라인을 이해하는 데 더 많은 시간을 쓰게 되는 상황, 나는 이 문제를 직접 겪었고 직접 해결했다.
이 글은 CONE-Chain Portal(CCP) 프로젝트에서 Helm Chart 기반의 독립 CI/CD 구조를 설계한 배경, 의도, 그리고 실제 구현 내용을 정리한 것이다.
1. 문제 인식 — 왜 기존 파이프라인을 쓰지 않았는가
개발 환경의 구조적 제약
CCP는 CONE-Chain 솔루션 위에 구축된 포탈 프로젝트다. 현재 개발환경용 CONE-Chain은 Public Cloud의 멀티 클러스터 Kubernetes 환경 위에서 동작하고, FE/BE 소스코드는 NHN 내부망 GitHub Enterprise에 각각 모노레포로 관리된다.
핵심 제약은 망 분리다. 내부망의 소스코드가 Public Cloud의 K8s 클러스터에 배포되어야 하는데, 두 망을 연결할 수 있는 자원은 Self-hosted GitHub Actions Runner 서버 하나뿐이었다. 이 Runner가 내부망과 외부망 양쪽에 통신이 가능하다는 점이 전체 설계의 출발점이 되었다.
기존 CONE-Chain CI/CD의 문제
CONE-Chain이 제공하는 CI/CD 파이프라인은 Gitea + Tekton + ArgoCD 기반으로 구성되어 있었다. 하지만 CCP 입장에서는 다음과 같은 문제가 있었다.
- 과도한 복잡도: 개별 Tekton Task마다 Nexus, Harbor, SonarQube, Trivy 등 다양한 도구가 엮여 있었다. CCP 규모에서는 이 모든 도구 체인이 필요하지 않았다.
- 높은 진입 장벽: FE, BE 개발팀이 Tekton 리소스 구조(Pipeline, Task, PipelineRun, Workspace 등)를 이해하고 운영하기엔 학습 비용이 너무 컸다.
- 오버엔지니어링: 각 Task가 범용 솔루션을 지향하다 보니, 실제로 CCP에 필요한 것 이상의 리소스와 설정을 요구했다.
- 솔루션 배포 시 부담: 나중에 Real 환경에 CCP를 설치할 때, Tekton 기반 파이프라인을 그대로 가져가려면 사전에 준비해야 할 인프라 의존성(Gitea, Tekton Controller, Nexus 등)이 너무 많았다.
Gitea를 쓰지 않는 이유 — 이미 GitHub으로 충분했다
CONE-Chain의 CI/CD 흐름에서 Gitea는 소스코드를 Tekton과 연동하기 위한 Git 서버 역할을 한다. 하지만 우리 개발팀은 이미 GitHub Enterprise를 중심으로 개발 문화를 구축하고 있었다. PR 기반의 코드 리뷰, Branch Protection Rule, 그리고 GitHub과 Dooray 협업 툴의 연동을 통한 이슈 트래킹까지 개발 워크플로의 전 과정이 GitHub 위에서 돌아가고 있었다.
이 상황에서 Gitea를 추가로 도입한다는 것은 소스코드를 이중으로 관리해야 한다는 의미였다. Gitea가 제공하는 기능도 GitHub에 비해 한정적이었고, 무엇보다 이미 잘 돌아가고 있는 개발 워크플로를 CI/CD 때문에 바꿀 이유가 없었다. CONE-Chain이 제공하는 도구 체인 중 필요한 것(Harbor, ArgoCD)만 취하고, 불필요한 것(Gitea, Tekton, Nexus, SonarQube)은 과감히 걷어내는 것이 합리적인 선택이었다.
한마디로, 개발팀이 배포를 위해 알아야 할 것이 너무 많았고, 이미 사용 중인 도구로 충분히 해결할 수 있었다.
2. 설계 원칙 — 어떤 방향으로 풀 것인가
위 문제를 해결하기 위해 네 가지 원칙을 세웠다.
원칙 1: CONE-Chain과의 의존성을 최소화한다
CCP는 CONE-Chain 위에서 동작하지만, 포탈 자체의 배포는 CONE-Chain의 CI/CD에 의존하지 않도록 했다. CONE-Chain 인프라에 장애가 발생하더라도 CCP의 배포/운영 안정성을 유지할 수 있고, Real 환경 설치 시에도 CONE-Chain CI/CD 인프라 없이 독립적으로 배포가 가능하다.
원칙 2: 이미 사용 중인 도구를 최대한 활용한다
개발팀은 이미 GitHub Enterprise에서 PR, 코드 리뷰, Dooray 연동까지 완성된 개발 워크플로를 운영하고 있었다. CI/CD도 이 흐름 위에 자연스럽게 얹는 것이 맞았다. FE/BE 개발자가 알아야 하는 것은 Dockerfile 작성과 GitHub Actions 워크플로 정도로 한정했다. 새로운 도구(Tekton, Gitea)를 학습하거나, 기존 개발 워크플로를 바꿀 필요가 없도록 했다.
원칙 3: 개발 환경 그대로 Real 환경에 배포 가능하게 한다
Docker 이미지 + Helm Chart 조합은 환경에 독립적이다. 개발 환경에서 검증된 이미지와 차트 구성 그대로 Real 환경에 적용할 수 있으므로, 솔루션 배포/설치 시 별도의 파이프라인 재구성이 필요 없다.
원칙 4: GitOps 원칙을 따른다
Git 저장소를 Single Source of Truth로 삼아, 배포 상태를 선언적으로 관리한다. ArgoCD가 Git 저장소를 폴링하여 변경을 감지하고 자동 Sync하는 구조를 통해, 수동 kubectl 명령 없이 Git commit만으로 배포가 완료되도록 했다.
3. 구현 — 실제 어떻게 구성했는가
전체 CI/CD 아키텍처

전체 흐름은 CI(이미지 빌드) → CD(차트 태그 업데이트) → GitOps 배포의 3단계로 구성된다. Self-hosted Runner가 내부망과 외부망의 브릿지 역할을 하며, CI에서는 Harbor로 이미지를 push하고, CD에서는 Mirror Repository로 Chart 변경사항을 동기화한다.
레포지토리 구조와 역할 분리

소스코드(be-ccp, fe-ccp)와 배포 설정(ccp-chart)을 분리한 것이 핵심이다. 이 분리를 통해
- CI는 소스 레포에서: 빌드 → 이미지 Push만 담당
- CD는 Chart 레포에서: 이미지 태그 업데이트 → 미러 동기화만 담당
- 배포는 ArgoCD가: Mirror Repo 폴링 → K8s 자동 Sync
각 단계가 독립적이므로 CI 실패가 CD에 영향을 주지 않고, CD 실패가 운영 중인 서비스에 영향을 주지 않는다.
3-1. CI — 이미지 빌드 파이프라인
CI 워크플로는 각 소스 레포(be-ccp, fe-ccp)에 서비스별로 구성되어 있다.
| 워크플로 | 모듈 | 레포 |
|---|---|---|
ci-member-harbor-image.yml |
Member | be-ccp |
ci-portalManagement-harbor-image.yml |
PortalManagement | be-ccp |
ci-kubeManagement-harbor-image.yml |
KubeManagement | be-ccp |
ci-frontend-harbor-image.yml |
Frontend | fe-ccp |
ci-bff-harbor-image.yml |
BFF | fe-ccp |
CI 흐름

CI 입력은 간결하다. 배포 대상 Git ref와 Spring profile 두 가지만 지정하면 된다.
Docker 이미지 태그 규칙
이미지 태그는 추적 가능성을 위해 타임스탬프 + 실행 횟수 + 커밋 해시 조합으로 생성한다.
{TIMESTAMP}-r{RUN_ATTEMPT}-{SHORT_SHA}
# 예시: 20260304120000-r1-abc1234
이 규칙 덕분에 태그만 보고도 "언제, 몇 번째 시도에서, 어떤 커밋으로 빌드되었는지"를 즉시 파악할 수 있다.
Dockerfile — 모듈별 빌드를 하나의 Dockerfile로
BE 모노레포의 여러 모듈을 하나의 Dockerfile로 처리하기 위해 ARG를 활용했다.
FROM eclipse-temurin:21-jdk-jammy
ARG MODULE_NAME # 빌드 대상 모듈 (member, portalManagement, kubeManagement)
ARG APP_PROFILE # Spring profile (alpha-net)
COPY ${MODULE_NAME}/build/libs/app.jar ./app.jar
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]
CI 워크플로에서 --build-arg MODULE_NAME=member처럼 모듈명만 바꿔주면 동일한 Dockerfile로 모든 BE 서비스의 이미지를 빌드할 수 있다.
3-2. Helm Chart — 템플릿 모듈화와 환경별 독립 Values
Chart 디렉터리 구조
ccp-chart/
├── ccp-be-kubemanagement/ # BE - K8s Management 서비스
├── ccp-be-member/ # BE - Member 서비스
├── ccp-be-portalmanagement/ # BE - Portal Management 서비스
├── ccp-fe/ # FE - React + Nginx
├── ccp-fe-bff/ # FE - Node.js BFF
├── ccp-fe-storybook/ # FE - Storybook 문서 사이트
└── .github/workflows/ # 서비스별 CD 워크플로
각 서비스 차트의 내부 구조는 다음과 같다.
ccp-be-member/
├── Chart.yaml # 차트 메타데이터
├── templates/
│ ├── _helpers.tpl # 이름/네임스페이스/라벨 헬퍼 함수
│ ├── deployment.yaml # Deployment (이미지, 프로브, 리소스, ConfigMap 마운트)
│ ├── service.yaml # ClusterIP Service
│ ├── ingress.yaml # Nginx Ingress (호스트/경로/TLS)
│ ├── hpa.yaml # HorizontalPodAutoscaler
│ ├── configmap.yaml # ConfigMap (files 맵 기반)
│ └── serviceaccount.yaml # ServiceAccount
├── values-alpha-net.yaml # alpha-net 환경 (Active)
└── values-alpha.yaml # alpha 환경 (Fade-out, 템플릿 참고용)
핵심 설계: _helpers.tpl로 이름 규칙 통일
모든 리소스의 이름과 네임스페이스는 _helpers.tpl의 헬퍼 함수를 통해 global 블록에서 파생된다.
# _helpers.tpl
{{- define "ccp-be-member.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else if .Values.global.name -}}
{{- .Values.global.name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- define "ccp-be-member.namespace" -}}
{{- if .Values.global.namespace -}}
{{- .Values.global.namespace -}}
{{- else -}}
{{- default .Release.Namespace .Values.namespace -}}
{{- end -}}
{{- end -}}
이렇게 하면 values 파일의 global.name과 global.namespace만 지정하면 Deployment, Service, Ingress, HPA 등 모든 리소스의 네이밍이 자동으로 일관성 있게 생성된다.
핵심 설계: 이미지 해상도 — Global/Local 우선순위
Deployment 템플릿에서 이미지 정보는 global → local 순으로 해석된다.
# deployment.yaml (상단 변수 선언부)
{{- $global := default (dict) .Values.global -}}
{{- $globalImage := default (dict) (index $global "image") -}}
{{- $localImage := default (dict) .Values.image -}}
{{- $imageRepository := default (default "" (index $localImage "repository")) (index $globalImage "repository") -}}
{{- $imageTag := default (default .Chart.AppVersion (index $localImage "tag")) (index $globalImage "tag") -}}
{{- $imagePullPolicy := default (default "IfNotPresent" (index $localImage "pullPolicy")) (index $globalImage "pullPolicy") -}}
이 구조 덕분에 평상시에는 global.image만 관리하면 되고, 특수한 경우에만 로컬 image 블록으로 오버라이드할 수 있다. CD 워크플로가 global.image.tag만 치환하면 되는 이유이기도 하다.
핵심 설계: 조건부 리소스 생성
모든 부가 리소스는 values에서 명시적으로 활성화해야 생성된다.
# hpa.yaml — autoscaling.enabled가 true일 때만 생성
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "ccp-be-member.fullname" . }}-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "ccp-be-member.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
# configmap.yaml — config.enabled + files/properties가 있을 때만 생성
{{- if and $config.enabled (or $config.files $config.properties) }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "ccp-be-member.fullname" . }}-cm
data:
{{- range $name, $content := $files }}
{{ $name | quote }}: |
{{ $content | indent 4 }}
{{- end }}
{{- end }}
ConfigMap이 활성화되면 Deployment에서 자동으로 Volume Mount와 SPRING_CONFIG_ADDITIONAL_LOCATION 환경변수를 주입한다.
# deployment.yaml (ConfigMap 연동 부분)
{{- $configEnabled := and .Values.config .Values.config.enabled }}
# ...
env:
{{- if $configEnabled }}
- name: {{ $configEnvVar }}
value: {{ $configEnvValue | quote }}
{{- end }}
{{- if $configEnabled }}
volumeMounts:
- name: application-config
mountPath: {{ $configMountPath }}
readOnly: true
{{- end }}
이렇게 values 파일의 플래그 하나로 관련 리소스가 연쇄적으로 활성화/비활성화되도록 설계했다.
환경별 독립 Values 파일 — 새 환경은 파일 하나만 추가
오버라이드 체인(base → overlay) 없이 환경별 단일 파일로 모든 설정을 완결시켰다.
# values-alpha-net.yaml — 이 파일 하나가 alpha-net 환경의 전부다
global:
name: ccp-be-member
namespace: cone-chain-portal
image:
repository: harbor.cone-chain.net/cone-chain-portal/ccp-be-member
tag: "20260305103605-r1-3ff849f"
pullPolicy: IfNotPresent
replicaCount: 1
revisionHistoryLimit: 3
imagePullSecrets:
- name: harbor-pull
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]
readOnlyRootFilesystem: true
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: 250m
memory: 2Gi
limits:
cpu: 500m
memory: 3Gi
livenessProbe:
httpGet:
path: /member/api/actuator/health/liveness
port: http
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /member/api/actuator/health/readiness
port: http
initialDelaySeconds: 30
periodSeconds: 15
ingress:
enabled: true
className: "nginx"
hosts:
- host: alpha-kr1-ccp-be.cone-chain.net
paths:
- path: /member/api
pathType: Prefix
tls:
- secretName: ssl-common
hosts:
- alpha-kr1-ccp-be.cone-chain.net
env:
SPRING_PROFILES_ACTIVE: alpha-net
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- common-default-worker-node-0
- common-default-worker-node-1
- common-default-worker-node-2
새 환경(예: prod)이 추가된다면? values-prod.yaml을 작성하고 ArgoCD에 등록하면 끝이다. 도메인, 레지스트리, 리소스 스펙, TLS 인증서만 환경에 맞게 바꿔주면 동일한 템플릿이 그대로 동작한다.
3-3. CD — GitOps 기반 이미지 태그 업데이트
CD 워크플로 핵심 로직
CD 워크플로의 입력은 이미지 태그와 대상 values 파일 두 가지뿐이다.
# .github/workflows/ccp-be-member-chart-cd.yml
on:
workflow_dispatch:
inputs:
image-tag:
description: '새 Docker 이미지 태그'
required: true
values-file:
description: '태그를 업데이트할 values 파일'
required: true
type: choice
options:
- values-alpha-net.yaml
repository_dispatch:
types: [update-image-tag]
workflow_dispatch(GitHub UI 수동 실행)와 repository_dispatch(API 호출) 두 가지 트리거를 지원한다. CI 완료 후 자동 연결도 가능하지만, 현재는 의도적으로 수동 트리거 방식을 유지하고 있다. 배포 시점을 개발자가 직접 결정하는 것이 더 안전하다고 판단했기 때문이다.
Values 파일 업데이트 — Python 스크립트로 정밀 치환
values YAML에서 global.image.tag 위치만 정확히 찾아 치환하는 Python 스크립트를 워크플로에 내장했다.
# 워크플로 내 Python 스크립트 (핵심 로직 발췌)
for idx, line in enumerate(lines):
stripped = line.strip()
indent = len(line) - len(line.lstrip(' '))
if stripped.startswith('global:'):
in_global = True
global_indent = indent
if in_global and stripped.startswith('image:'):
in_image = True
image_indent = indent
if in_image and stripped.startswith('tag:'):
# 현재 태그와 동일하면 변경 없음 처리
if current == image_tag:
print(f"이미 최신 태그입니다. 변경 없음.")
break
lines[idx] = f"{prefix}tag: \"{image_tag}\""
updated = True
break
YAML 파서 대신 라인 단위 파싱을 사용한 이유는, YAML 파서가 주석이나 포매팅을 변경하는 것을 방지하기 위해서다. global.image.tag 줄만 정확히 치환하고 나머지는 그대로 유지한다.
미러 동기화 — 망 분리를 우회하는 브릿지
ArgoCD는 Public Cloud에서 운영되므로 내부망 GitHub Enterprise에 직접 접근할 수 없다. 이 간극을 Self-hosted Runner가 메운다.
# 워크플로 내 미러 동기화 핵심 로직
# 소스 파일 동기화 (.git, .github 제외)
rsync -a --delete \
--exclude '.git' \
--exclude '.github' \
"$SOURCE_DIR/" "$MIRROR_DIR/"
cd "$MIRROR_DIR"
git add -A
if git diff --cached --quiet; then
echo "미러 저장소에 변경 사항이 없습니다. 푸시를 건너뜁니다."
exit 0
fi
git commit -m "mirror: sync $GITHUB_SHA"
git push origin "$CURRENT_BRANCH"
.github디렉터리는 미러에서 제외 (워크플로는 내부망에서만 실행되므로)- 변경 사항이 없으면 불필요한 push를 스킵
- 시크릿이 미설정이면 미러 단계 자체를 건너뜀
이 구조 덕분에 내부망 보안 정책을 유지하면서도 Public Cloud의 ArgoCD가 GitOps 방식으로 배포를 수행할 수 있다.
GitOps 배포 사이클
전체 배포 사이클을 정리하면 다음과 같다.

Git 저장소의 values 파일이 곧 배포 상태의 선언(Desired State)이다. ArgoCD가 이 선언과 실제 클러스터 상태를 지속적으로 비교하고, 차이가 있으면 자동으로 Sync한다. 수동 kubectl apply나 helm upgrade가 필요 없다.
알림 — 실패 원인까지 자동 전송

성공/실패 시 Dooray Messenger로 알림을 보낸다. 특히 실패 시에는 어떤 Step에서 실패했는지와 1차 원인 가이드까지 함께 전송한다.
[CD | Member] Chart Image Tag Update FAILED
----------------------------------------
Failed Step: Update values files
Cause: values 파일 파싱 실패. global.image.tag 항목 확인 필요
Image Tag: 20260304120000-r1-abc1234
Actor: hakHyeonSong
Run: https://github.nhnent.com/.../actions/runs/123
별도로 GitHub Actions 로그를 뒤지지 않아도 1차 원인 파악이 가능하다.
4. 결과 — 무엇이 달라졌는가
개발팀 관점
| 항목 | 기존 (Gitea + Tekton) | 변경 후 (GitHub Actions + Helm) |
|---|---|---|
| 소스코드 관리 | Gitea에 별도 미러링 필요 | GitHub Enterprise 그대로 사용 |
| 개발 워크플로 연속성 | PR/코드리뷰 흐름과 분리 | PR → CI → CD가 하나의 플랫폼에서 연결 |
| 배포 시 알아야 할 도구 | Tekton, Gitea, Nexus, Harbor, SonarQube, Trivy 등 | Dockerfile, GitHub Actions |
| 배포 트리거 | Tekton PipelineRun 생성 | GitHub UI에서 버튼 클릭 또는 API 호출 |
| 배포 결과 확인 | Tekton Dashboard 접속 | Dooray 메신저 알림 수신 |
| 새 서비스 추가 | Pipeline/Task 리소스 다수 생성 | Chart 디렉터리 복사 + values 파일 작성 |
| 새 환경 추가 | 전체 파이프라인 재구성 | values-{환경}.yaml 파일 1개 추가 |
운영 관점
- 독립성: CONE-Chain CI/CD 인프라 장애가 CCP 배포에 영향을 주지 않는다.
- 이식성: Docker 이미지 + Helm Chart 조합이므로, Real 환경에 그대로 설치 가능하다. Tekton Controller, Gitea, Nexus 등의 인프라를 사전 구축할 필요가 없다.
- 단순성: 환경별 values 파일 하나로 모든 설정이 완결된다. 오버라이드 체인이 없으므로 "이 값이 어디서 온 건지" 추적할 필요가 없다.
- 추적성: 이미지 태그에 타임스탬프와 커밋 해시가 포함되어 있어, 현재 배포된 버전이 어떤 코드 기반인지 즉시 확인 가능하다.
- 롤백 용이: 이전 이미지 태그로 CD 워크플로를 다시 실행하거나, ArgoCD에서 이전 리비전으로 Sync하면 끝이다.
마치며
이 설계의 핵심은 "개발팀이 배포를 위해 알아야 할 것을 최소화하자"였다.
범용 CI/CD 솔루션이 제공하는 기능은 강력하지만, 프로젝트의 규모와 팀의 상황에 맞지 않으면 오히려 생산성을 떨어뜨린다. CCP는 Tekton 파이프라인의 모든 기능이 필요한 프로젝트가 아니었고, 개발팀은 Tekton 전문가가 아니었다.
적정 기술(Appropriate Technology)이라는 말이 있다. 가장 뛰어난 기술이 아니라, 현재 상황에 가장 적합한 기술을 선택하는 것. GitHub Actions로 CI를 구성하고, Helm Chart로 배포 단위를 관리하고, ArgoCD로 GitOps 배포를 수행하는 이 조합은 특별히 새로운 것이 없다. 하지만 망 분리 환경에서 Runner를 브릿지로 활용하는 미러링 구조, 환경별 단일 values 파일로 완결되는 Chart 설계, 그리고 CI/CD 분리를 통한 독립적인 배포 체계는 우리 팀의 제약 조건에서 나온 실용적인 선택이었다.
결국 좋은 설계란, 화려한 아키텍처가 아니라 팀이 실제로 사용할 수 있는 구조를 만드는 것이라 생각한다. Docker 이미지와 Helm Chart만 있으면 어떤 환경에든 배포할 수 있다는 단순함이, 이 설계가 지향하는 최종 목표다.
혼자 잘하기보다 함께 자라기
솔직하게 말하면, 이 설계에는 기술적 판단만큼이나 개인적인 경험이 크게 작용했다.
오버엔지니어링은 늘 경계해야 한다. 그리고 그보다 더 중요한 것은, 팀 구성원 모두가 기술적으로 이해하고 운영할 수 있어야 한다는 것이다.
내가 BE 개발 챕터에서 기술적 리드로서 이루고 싶은 것은 결국 "혼자 잘하기"가 아니라 "함께 자라기"다. 한 사람만 이해하는 복잡한 시스템보다, 팀 전원이 이해하고 운영할 수 있는 단순한 시스템이 더 강하다. 누군가 휴가를 가도, 팀에 새로운 사람이 합류해도, 배포가 막히지 않는 구조. 그것이 이 CI/CD 설계가 지향하는 최종 목표다.
결국 좋은 설계란, 화려한 아키텍처가 아니라 팀이 실제로 사용할 수 있는 구조를 만드는 것이라 생각한다. Docker 이미지와 Helm Chart만 있으면 어떤 환경에든 배포할 수 있다는 단순함 — 그 단순함이 팀 전체의 역량이 되는 것, 그것이 내가 생각하는 좋은 설계다.
'아키텍처' 카테고리의 다른 글
| Transactional Outbox Pattern과 적용 회고 (from. BaaS) (0) | 2025.08.01 |
|---|