1. CI/CD란? (Continuous Integration / Continuous Delivery,Deployment)
CI/CD는 **지속적 통합(Continuous Integration)**과 **지속적 배포(Continuous Delivery 또는 Continuous Deployment)**를 합친 용어로, 애플리케이션 개발부터 배포까지의 전 과정을 자동화하여 소프트웨어를 더 빠르고, 안정적이며, 빈번하게 사용자에게 제공하는 개발 방법론 또는 문화입니다. DevOps(개발과 운영의 통합)의 핵심적인 실천 방안 중 하나입니다.
* 지속적 통합 (Continuous Integration, CI)
CI는 개발자들이 작성한 코드를 주기적으로 **공유 저장소(예: Git)**에 **통합(Merge)**하는 프로세스와, 통합 후 자동화된 테스트 및 빌드를 수행하는 과정을 의미합니다.
- 목표: 코드 충돌을 최소화하고, 버그를 개발 초기에 신속하게 발견하고 수정하여 통합 문제로 인한 시간 낭비를 줄이는 것입니다.
- 주요 단계:
- Code (코드 작성): 개발자가 코드를 작성하고 Git 등의 버전 관리 시스템에 커밋(Commit)합니다.
- Build (빌드): 커밋된 코드를 가져와 실행 가능한 아티팩트(예: JAR, WAR, Docker Image)로 만듭니다.
- Test (테스트): 유닛 테스트(Unit Test), 통합 테스트(Integration Test) 등을 자동으로 수행하여 코드가 제대로 작동하는지 검증합니다.
* 지속적 전달 (Continuous Delivery, CD) vs. 지속적 배포 (Continuous Deployment, CD)
CI 과정을 거쳐 검증된 소프트웨어를 사용자에게 제공하는 과정을 CD라고 합니다. CD에는 두 가지 개념이 있습니다.
- 지속적 전달 (Continuous Delivery):
- 빌드되고 테스트를 통과한 소프트웨어 변경 사항이 배포 가능한 상태로 준비되어 **저장소(Artifact Repository)**에 보관됩니다.
- **운영 환경(Production)**으로의 최종 배포는 수동적인 결정 (예: 버튼 클릭)에 의해 이루어집니다.
- 장점: 최종 배포 시점을 유연하게 결정할 수 있어, 비즈니스적인 판단(예: 마케팅 일정)에 맞춰 서비스를 출시할 수 있습니다.
- 지속적 배포 (Continuous Deployment):
- 지속적 전달의 개념을 확장하여, 빌드 및 테스트를 통과한 모든 변경 사항이 자동적으로 운영 환경에 배포됩니다. 사람의 개입이 최소화됩니다.
- 장점: 개발 속도가 극대화되고, 고객 피드백을 매우 빠르게 서비스에 반영할 수 있습니다. 높은 수준의 자동화와 신뢰할 수 있는 테스트 환경이 필수입니다.
2. 컨테이너 이미지 빌드 방식 비교: DinD / DooD / Daemonless
CI/CD 파이프라인에서 애플리케이션을 **도커(Docker)**와 같은 컨테이너 이미지로 만드는 것은 필수적인 단계입니다. 이때, CI 환경(대부분 컨테이너) 내에서 또 다른 컨테이너 이미지를 빌드하는 방식에 따라 보안과 효율성에 차이가 발생합니다.
| 방식 | DinD (Docker in Docker) | DooD (Docker out of Docker) | Daemonless (Kaniko, Buildah 등) |
| 개념 | 컨테이너 안에 별도의 도커 데몬을 실행하여 이미지를 빌드합니다. | 호스트의 도커 데몬을 컨테이너와 공유하여 이미지를 빌드합니다. | 도커 데몬 없이 독립적인 프로세스로 컨테이너 이미지를 빌드합니다. |
| 보안 | 매우 취약 (DinD 컨테이너에 --privileged 옵션 필요). 호스트 시스템의 모든 권한을 가질 수 있어, 보안 사고 발생 시 호스트 전체가 위험해질 수 있습니다. | 비교적 취약 (도커 소켓 공유). 호스트의 도커 데몬을 오용할 수 있어 보안 위험이 존재합니다. | 가장 안전. 호스트의 도커 데몬이나 소켓에 접근하지 않으므로, 컨테이너 격리성이 유지되어 보안 위험이 낮습니다. |
| 격리성 | 호스트와 격리된 독립적인 도커 환경 구축이 가능합니다. (하지만 --privileged 옵션 때문에 무의미해질 수 있음) | 호스트의 도커 데몬에 의존하므로 격리성이 낮습니다. | 완벽하게 격리된 환경에서 빌드되어 격리성이 높습니다. |
| 복잡성 | 별도의 도커 데몬 실행/관리 필요, 복잡합니다. | 소켓 공유만 하면 되므로 비교적 간단합니다. | 별도의 도구(Kaniko, Buildah 등)에 대한 학습이 필요하지만, 설정 자체는 깔끔합니다. |
| 성능 | 컨테이너 내의 가상화 오버헤드 때문에 느립니다. | 호스트의 데몬을 사용하므로 가장 빠릅니다. | 오버헤드가 적어 매우 빠르거나 DinD보다 빠릅니다. |
| 주요 도구 | Docker (자체) | Docker (Volume Mount) | Kaniko (Google), Buildah (Red Hat) |

** Daemonless 방식이 선호되는 이유
DinD와 DooD 방식은 도커의 핵심 철학인 격리성(Isolation)을 무너뜨릴 수 있다는 치명적인 보안 문제를 가지고 있습니다. 특히 쿠버네티스(Kubernetes)와 같은 클라우드 네이티브 환경에서는 각 워크로드가 격리되는 것이 매우 중요하며, Daemonless 빌드 방식인 Kaniko는 도커 데몬에 의존하지 않고 이미지 빌드를 수행하여 이러한 보안 문제와 복잡성을 해결합니다. 따라서 보안을 최우선으로 하는 클라우드 네이티브 CI/CD 환경에서는 Kaniko와 같은 Daemonless 방식이 가장 권장됩니다.
3. 실습 아키텍처 이해: Tekton 파이프라인 & Argo CD
최신 클라우드 네이티브 CI/CD 환경에서는 쿠버네티스(Kubernetes) 위에서 CI(빌드/테스트)와 CD(배포)를 처리하는 도구들을 사용하며, 이번 실습 아키텍처는 Tekton과 Argo CD를 결합하여 GitOps 원칙을 따릅니다.
1) Tekton 파이프라인 (CI 담당)
Tekton은 쿠버네티스 위에서 실행되도록 설계된 강력하고 유연한 CI/CD 파이프라인 프레임워크입니다.
* Tekton의 장점
- 클라우드 네이티브: 쿠버네티스의 CRD(Custom Resource Definition)를 사용하여 파이프라인을 정의하고, 모든 작업은 쿠버네티스 파드(Pod) 내에서 실행됩니다. 이는 확장성과 이식성이 뛰어남을 의미합니다.
- 재사용성 및 모듈화:
- Task: 파이프라인을 구성하는 가장 작은 단위 (예: 소스 코드 클론, 이미지 빌드, 테스트 실행). 각 Task는 독립적으로 정의되어 재사용이 가능합니다.
- Pipeline: 여러 Task를 순서 또는 DAG(Directed Acyclic Graph) 형태로 연결하여 전체 CI 워크플로우를 정의합니다.
- 격리성: 각 TaskRun은 새로운 파드에서 실행되므로, 작업 환경이 완전히 격리되어 안전하고 일관된 빌드를 보장합니다.
* Tekton의 동작 방식
- Code Commit: 개발자가 Git 저장소(Private Repo)에 코드를 푸시(Push)합니다.
- Trigger: Tekton의 Trigger 컴포넌트가 이 Git 이벤트를 감지합니다.
- Pipeline 실행: Trigger는 미리 정의된 PipelineRun을 생성하여 Tekton 파이프라인을 시작합니다.
- Task 실행: 파이프라인은 순서대로 정의된 Task(예: git-clone Task, kaniko-build Task)들을 실행합니다. 각 Task는 쿠버네티스 파드로 실행됩니다.
- Artifact 생성:
- git-clone Task가 소스 코드를 가져옵니다.
- kaniko-build Task (Daemonless 방식)가 소스 코드를 사용하여 도커 이미지를 빌드하고 **레지스트리(Registry)**에 푸시합니다.
- GitOps Repo 업데이트: 빌드된 이미지의 태그(Tag) 정보를 **배포 설정 파일(Manifest)**이 있는 GitOps 저장소에 자동으로 업데이트합니다. (이것이 Argo CD에게 배포를 시작하라고 알려주는 신호가 됩니다.)
2) Argo CD (CD 담당)
Argo CD는 GitOps 원칙을 구현하는 **선언적(Declarative)**이고 자동화된 CD 도구입니다.
* GitOps란?
GitOps는 Git을 **진실의 단일 출처(Single Source of Truth)**로 사용하여 인프라 및 애플리케이션의 상태를 관리하는 방법론입니다. 애플리케이션의 최종 상태를 YAML과 같은 선언적인 파일로 Git에 저장하고, Git에 저장된 상태와 실제 쿠버네티스 클러스터의 상태가 일치하도록 도구가 지속적으로 동기화합니다.
* Argo CD의 장점
- GitOps 원칙 준수: 배포의 모든 변경 사항이 Git에 기록되므로 변경 이력 추적이 용이하고, 롤백이 간단하며, **감사(Audit)**에 유리합니다.
- 선언적 관리: 쿠버네티스 Manifest(YAML 파일)에 원하는 상태(Desired State)를 선언하면, Argo CD가 현재 상태(Actual State)를 확인하여 원하는 상태와 일치하도록 자동으로 배포(동기화)합니다.
- 자동 동기화 및 Drift 감지: GitOps 저장소의 변경 사항을 지속적으로 감시하고, 클러스터의 상태가 Git의 상태와 다를 경우(Drift) 자동으로 수정하거나 알림을 보냅니다.
- 다양한 배포 전략 지원: Blue/Green, Canary 등 복잡한 배포 전략을 쉽게 구현할 수 있도록 Argo Rollouts과 같은 부가 컴포넌트와 잘 연동됩니다.
- 직관적인 UI: 배포된 애플리케이션의 상태, 히스토리, 차이점(Diff) 등을 시각적으로 확인할 수 있는 대시보드를 제공합니다.
* Argo CD의 동작 방식
- 감시 (Watch): Argo CD는 GitOps 저장소를 지속적으로 감시합니다.
- 변경 감지: Tekton 파이프라인에 의해 이미지 태그가 업데이트되면(Git Commit 발생), Argo CD는 Git의 Desired State가 변경된 것을 감지합니다.
- Diff 확인: Argo CD는 Git에 명시된 Desired State와 실제 쿠버네티스 클러스터의 Actual State를 비교합니다.
- 동기화 (Sync): 차이점(Diff)이 발견되면, Argo CD는 자동으로 또는 수동 승인을 거쳐 쿠버네티스 클러스터에 변경 사항을 적용합니다. (YAML 파일에 명시된 대로 새로운 이미지 태그를 가진 Pod를 배포합니다.)
- Health Check: 배포된 애플리케이션의 상태(Pod, Service 등)를 모니터링하여 정상적으로 배포되었는지 확인합니다.
4. 실습 진행 (github + Tekton + ArgoCD)
1) kind cluster 배포
$ kind create cluster --name myk8s --image kindest/node:v1.32.8 --config - <<EOF
a> kind: Cluster
> apiVersion: kind.x-k8s.io/v1alpha4
orking:> networking:
> apiServerAddress: "0.0.0.0"
> nodes:
> - role: control-plane
tMappin> extraPortMappings:
> - containerPort: 30000
> hostPort: 30000
> - containerPort: 30001
> hostPort: 30001
ntainerP> - containerPort: 30002
> hostPort: 30002
30003
> - containerPort: 30003
> hostPort: 30003
orker
E> - role: worker
> EOF
Creating cluster "myk8s" ...
✓ Ensuring node image (kindest/node:v1.32.8) 🖼
✓ Preparing nodes 📦 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
✓ Joining worker nodes 🚜
Set kubectl context to "kind-myk8s"
You can now use your cluster with:
kubectl cluster-info --context kind-myk8s
Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂
$ kind get nodes --name myk8s
myk8s-worker
myk8s-control-plane
$ kubens default
✔ Active namespace is "default"
2) github repo 준비
2-1. test code 작성

2-2. github에 작업 서버 SSH 등록


2-3. [dev] github private repo 생성 및 dev app PUSH
git init
git add .
git config --global user.email "<email>"
git config --global user.name "<name>"
git commit -m "cicd test app"
git branch -M main
git remote add origin git@github.com:ria-choi/dev-lab1.git
git push -u origin main

2-4. [devops] github private repo 생성 및 devops app PUSH
git init
git add .
git commit -m "cicd test app"
git branch -M main
git remote add origin git@github.com:ria-choi/devops-lab1.git
git push -u origin main

3) tekton으로 CI 구현
3-1. tekton 설치
kubectl apply -f https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml
kubectl apply -f https://storage.googleapis.com/tekton-releases/triggers/latest/release.yaml

3-2. Secret & ServiceAccount 만들기
- Docker Hub 푸시 권한 Secret 생성
kubectl create secret docker-registry regcred \
--docker-server=index.docker.io \
--docker-username=<YOUR_DOCKERHUB_ID> \
--docker-password=<YOUR_DOCKERHUB_PASSWORD> \
--docker-email=<YOUR_DOCKERHUB_EMAIL>\
-n default
- devops 저장소에 git push 권한 Secret 생성
kubectl create secret generic git-credentials \
--from-literal=username=<YOUR_GITHUB_NAME>\
--from-literal=password=<YOUR_GITHUB_PAT> \
-n default
- Secret 참조 ServiceAccount 생성
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-bot
namespace: default
secrets:
- name: regcred
- name: git-credentials
imagePullSecrets:
- name: regcred
3-3. tekton test 정의
(⎈|kind-myk8s:default) riachoi@DESKTOP-HUU6SC7:~/cicd_study/cicd-labs/tekton$ cat task-pipeline/clone-source.yaml
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: clone-source
namespace: default
spec:
params:
- name: app_repo_url # 예: github.com/ria-choi/dev-lab1.git
type: string
- name: app_revision # 예: main
type: string
default: main
workspaces:
- name: source # /workspace/source 로 마운트됨
results:
- name: commit-sha
description: "short git commit SHA for this build"
steps:
- name: clone
image: alpine/git:latest
workingDir: /workspace/source
env:
- name: GIT_USER
valueFrom:
secretKeyRef:
name: git-credentials
key: username
- name: GIT_PASS
valueFrom:
secretKeyRef:
name: git-credentials
key: password
script: |
#!/bin/sh
set -eu
echo "[clone-source] cleanup workspace"
rm -rf ./* ./.git
echo "[clone-source] git clone"
git clone --branch "$(params.app_revision)" \
"https://${GIT_USER}:${GIT_PASS}@$(params.app_repo_url)" .
echo "[clone-source] extract short sha"
SHORT_SHA="$(git rev-parse --short HEAD)"
echo -n "${SHORT_SHA}" > /tekton/results/commit-sha
(⎈|kind-myk8s:default) riachoi@DESKTOP-HUU6SC7:~/cicd_study/cicd-labs/tekton$ cat task-pipeline/build-and-push-image-buildah.yaml
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: build-and-push-image-buildah
namespace: default
spec:
params:
- name: image_url # 예: docker.io/riachoi/dev-lab1
type: string
- name: image_tag # 예: 커밋 short SHA
type: string
workspaces:
- name: source # /workspace/source
steps:
- name: build-and-push
image: quay.io/buildah/stable:v1.37
workingDir: /workspace/source
securityContext:
privileged: true
env:
- name: REG_USERNAME
valueFrom:
secretKeyRef:
name: regcred-basic
key: username
- name: REG_PASSWORD
valueFrom:
secretKeyRef:
name: regcred-basic
key: password
script: |
#!/bin/sh
set -eux
FULL_IMAGE="$(params.image_url):$(params.image_tag)"
# build
buildah bud -t "$FULL_IMAGE" .
# push
buildah login -u "$REG_USERNAME" -p "$REG_PASSWORD" docker.io
buildah push "$FULL_IMAGE"
(⎈|kind-myk8s:default) riachoi@DESKTOP-HUU6SC7:~/cicd_study/cicd-labs/tekton$ cat task-pipeline/update-manifest.yaml
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: update-manifest
namespace: default
spec:
params:
- name: devops_repo_url # github.com/ria-choi/devops-lab1.git
type: string
- name: image_url # docker.io/riachoi/dev-lab1
type: string
- name: image_tag # 커밋 short SHA
type: string
- name: commit_user
type: string
default: "ria-choi"
- name: commit_email
type: string
default: "maria456852@gmail.com"
steps:
- name: update-and-push
image: alpine/git:latest
workingDir: /workspace
env:
- name: GIT_USER
valueFrom:
secretKeyRef:
name: git-credentials
key: username
- name: GIT_PASS
valueFrom:
secretKeyRef:
name: git-credentials
key: password
- name: DEVOPS_REPO
value: "$(params.devops_repo_url)"
- name: IMAGE_URL
value: "$(params.image_url)"
- name: IMAGE_TAG
value: "$(params.image_tag)"
- name: COMMIT_USER
value: "$(params.commit_user)"
- name: COMMIT_EMAIL
value: "$(params.commit_email)"
script: |
#!/bin/sh
set -eu
echo "[update-manifest] clone devops repo"
rm -rf devops
git clone "https://${GIT_USER}:${GIT_PASS}@${DEVOPS_REPO}" devops
cd devops
echo "[update-manifest] git config"
git config user.name "${COMMIT_USER}"
git config user.email "${COMMIT_EMAIL}"
TARGET_FILE="overlays/dev/patch-image.yaml"
NEW_IMAGE="${IMAGE_URL}:${IMAGE_TAG}"
echo "[update-manifest] patch ${TARGET_FILE} -> ${NEW_IMAGE}"
sed -i "s|^\([[:space:]]*image:[[:space:]]*\).*|\1${NEW_IMAGE}|" "${TARGET_FILE}"
git add "${TARGET_FILE}"
if git diff --cached --quiet; then
echo "no change, skip commit"
exit 0
fi
git commit -m "ci: update image to ${NEW_IMAGE}"
git push origin main
(⎈|kind-myk8s:default) riachoi@DESKTOP-HUU6SC7:~/cicd_study/cicd-labs/tekton$ cat task-pipeline/pipeline.yaml
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: build-and-update-deploy
namespace: default
spec:
workspaces:
- name: shared-workspace
params:
- name: app_repo_url # github.com/ria-choi/dev-lab1.git
type: string
- name: app_revision # main
type: string
default: main
- name: image_url # docker.io/riachoi/dev-lab1
type: string
- name: devops_repo_url # github.com/ria-choi/devops-lab1.git
type: string
tasks:
- name: clone-source
taskRef:
name: clone-source
workspaces:
- name: source
workspace: shared-workspace
params:
- name: app_repo_url
value: $(params.app_repo_url)
- name: app_revision
value: $(params.app_revision)
- name: build-push
runAfter:
- clone-source
taskRef:
name: build-and-push-image-buildah
workspaces:
- name: source
workspace: shared-workspace
params:
- name: image_url
value: $(params.image_url)
- name: image_tag
value: $(tasks.clone-source.results.commit-sha)
- name: update-manifest
runAfter:
- build-push
taskRef:
name: update-manifest
params:
- name: devops_repo_url
value: $(params.devops_repo_url)
- name: image_url
value: $(params.image_url)
- name: image_tag
value: $(tasks.clone-source.results.commit-sha)
- name: commit_user
value: "ria-choi"
- name: commit_email
value: "maria456852@gmail.com"
$ ls
build-and-push-image-buildah.yaml clone-source.yaml pipeline.yaml update-manifest.yaml
$ kubectl apply -f .
4) ArgoCD로 CD 구현
4-1. argocd 설치
$ kubectl create namespace argocd
namespace/argocd created
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
4-2. application 배포
(⎈|kind-myk8s:default) riachoi@DESKTOP-HUU6SC7:~/cicd_study/cicd-labs/argocd$ cat application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: test-dev
namespace: argocd
spec:
project: default
source:
repoURL: https://ria-choi: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@github.com/ria-choi/devops-lab1.git
targetRevision: main
path: overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
$ kubectl create -f argocd.yaml
$ kubectl get application -n argocd
NAME SYNC STATUS HEALTH STATUS
test-dev Unknown Healthy
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
sample-app-f54d9c6c8-frffd 1/1 Running 0 7m13s
5) 동작 테스트
5-1. pipelinerun 배포
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
name: sample-ci-run-sha-2
namespace: default
spec:
pipelineRef:
name: build-and-update-deploy
params:
- name: app_repo_url
value: github.com/ria-choi/dev-lab1.git
- name: app_revision
value: main
- name: image_url
value: docker.io/riachoi/dev-lab1
- name: devops_repo_url
value: github.com/ria-choi/devops-lab1.git
workspaces:
- name: shared-workspace
volumeClaimTemplate:
metadata:
name: pr-workspace-sample-ci-run-sha-2
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
5-2. image tag 자동 변경 후 pod 재시작 확인
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
sample-app-f54d9c6c8-frffd 1/1 Running 0 12m
sample-ci-run-sha-2-build-push-pod 0/1 Completed 0 17m
sample-ci-run-sha-2-clone-source-pod 0/1 Completed 0 17m
sample-ci-run-sha-2-update-manifest-pod 0/1 Completed 0 16m

$ kubectl describe pod sample-app-f54d9c6c8-frffd
Name: sample-app-f54d9c6c8-frffd
Namespace: default
Priority: 0
Service Account: default
Node: myk8s-worker/172.18.0.2
Start Time: Sun, 02 Nov 2025 10:10:22 +0900
Labels: app=sample-app
pod-template-hash=f54d9c6c8
Annotations: <none>
Status: Running
IP: 10.244.1.36
IPs:
IP: 10.244.1.36
Controlled By: ReplicaSet/sample-app-f54d9c6c8
Containers:
sample-app:
Container ID: containerd://6e1e916bab633029731b17a47f893dec7c252f75f6a6940c028a6bc44344b26c
Image: docker.io/riachoi/dev-lab1:628facd
Image ID: docker.io/riachoi/dev-lab1@sha256:63682516f62406e7b9748def33c7e05f2c615684d1515b9324647a835bcd6898
Port: 8080/TCP
Host Port: 0/TCP
State: Running
Started: Sun, 02 Nov 2025 10:10:31 +0900
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-pcglj (ro)
Conditions:
Type Status
PodReadyToStartContainers True
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
kube-api-access-pcglj:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
ConfigMapOptional: <nil>
DownwardAPI: true
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 17m default-scheduler Successfully assigned default/sample-app-f54d9c6c8-frffd to myk8s-worker
Normal Pulling 17m kubelet Pulling image "docker.io/riachoi/dev-lab1:628facd"
Normal Pulled 17m kubelet Successfully pulled image "docker.io/riachoi/dev-lab1:628facd" in 8.302s (8.302s including waiting). Image size: 12906907 bytes.
Normal Created 17m kubelet Created container: sample-app
Normal Started 17m kubelet Started container sample-app
** 추후 dev-lab1 환경에 Trigger 기능 적용 예정
5. 운영 확장 시 고려해야 할 부분 (안전성 확보)
CI/CD를 실제 서비스 운영으로 확장할 때, 보안과 안정성을 위해 수립해야 할 정책들입니다.
1) 보안 / Secret 관리: 권한 분리 원칙
- 문제: 모든 저장소(Git), 레지스트리, 클러스터가 프라이빗이므로 접근을 위한 자격증명(Secret) 관리가 필수입니다.
- 해결책 (권한 분리):
- Tekton (CI): 코드 읽기, 이미지 푸시, GitOps Repo 쓰기 권한만 가집니다.
- Argo CD (CD): GitOps Repo 읽기, 클러스터 배포 권한만 가집니다.
- 철학: CI는 코드/이미지만, CD는 배포만 담당하게 하여, 어느 한쪽이 해킹당해도 전체 시스템에 대한 권한을 갖지 못하게 설계해야 안전합니다.
2) 이미지 태그 전략: 불변성(Immutable) 확보
- 문제: latest처럼 내용이 바뀌는 가변적인 태그를 사용하면, 배포된 버전의 코드를 정확히 추적할 수 없어 장애 발생 시 재현과 분석이 불가능해집니다.
- 해결책: 커밋 SHA (소스 코드의 고유 ID)를 이미지 태그로 사용합니다. (예: my-service:3f1a9c2)
- 장점:
- 1:1 추적: 태그를 보면 정확히 어떤 소스 코드로 빌드되었는지 즉시 알 수 있습니다.
- 쉬운 롤백: 문제 발생 시 이전 커밋 SHA 태그로 안전하게 롤백할 수 있습니다.
3) 환경 분리 및 승인 게이트
- 문제: 개발(Dev)과 운영(Prod) 환경은 설정이 다르고, 운영 환경은 안전을 위해 자동화 외에 추가적인 검증이 필요합니다.
- 해결책 (GitOps 환경 분리):
- GitOps Repo 안에 overlays/dev와 overlays/prod처럼 환경별 디렉터리를 분리합니다.
- Dev: Tekton이 이미지를 빌드하면 overlays/dev에 자동으로 태그를 반영하고, Argo CD가 완전 자동으로 배포합니다. (빠르고 신속하게)
- Prod: 검증된 이미지 태그만 **사람의 승인(Manual Approval)**을 거쳐 overlays/prod에 수동/반자동으로 커밋합니다.
- Argo CD는 overlays/prod의 변경 사항만 확인하여 배포를 진행합니다.
'CI&CD' 카테고리의 다른 글
| [CI/CD] OpenLDAP + KeyCloak + Argo CD + Jenkins 구축 과정 (1) | 2025.11.23 |
|---|---|
| [ArgoCD] Vault 플러그인으로 인증 관리하기 (0) | 2025.11.11 |
| [ArgoCD] HA 구성으로 안정적인 배포 자동화 구현 (0) | 2025.11.06 |
| [Helm] ConfigMap 변경 감지 기반 자동 롤링 업데이트 구현 (0) | 2025.10.22 |
| [Shipwright] Docker 없이 Kubernetes에서 컨테이너 이미지 빌드하기 (0) | 2025.10.16 |