“이제와서 시작하는 Docker 마스터하기” CI/CD(지속적 통합/지속적 배포)는 현대 소프트웨어 개발의 핵심입니다. Docker를 CI/CD 파이프라인에 통합하면 일관된 빌드와 배포가 가능해집니다. 이번 편에서는 주요 CI/CD 플랫폼에서 Docker를 활용하는 방법을 알아보겠습니다.
CI/CD와 Docker의 시너지
Docker가 CI/CD에 주는 이점
- 환경 일관성: 개발, 테스트, 프로덕션 환경 통일
- 재현 가능한 빌드: 동일한 이미지로 어디서나 같은 결과
- 빠른 배포: 이미지 기반 배포로 속도 향상
- 롤백 용이: 이전 버전 이미지로 즉시 롤백
CI/CD 파이프라인 플로우
flowchart LR
subgraph 개발
Dev[개발자] --> Code[Code Push]
Code --> SCM[Git Repository]
end
subgraph CI[지속적 통합]
SCM --> Build[Docker Build]
Build --> Test[테스트 실행]
Test --> Scan[보안 스캔]
Scan --> Push[Registry Push]
end
subgraph CD[지속적 배포]
Push --> Stage[Staging 배포]
Stage --> Verify[검증]
Verify --> Prod[Production 배포]
Prod --> Monitor[모니터링]
end
Test -->|실패| Dev
Scan -->|취약점| Dev
Verify -->|문제| Stage
Monitor -->|알림| Dev
GitHub Actions
1. 기본 워크플로우
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
| # .github/workflows/docker.yml
name: Docker CI/CD
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: $
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build test image
uses: docker/build-push-action@v4
with:
context: .
target: test
load: true
tags: $:test
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run tests
run: |
docker run --rm $:test npm test
build-and-push:
needs: test
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: $
username: $
password: $
- name: Extract metadata
id: meta
uses: docker/metadata-action@v4
with:
images: $/$
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern=
type=semver,pattern=.
type=sha,prefix=-
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: $
labels: $
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
deploy:
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v0.1.5
with:
host: $
username: $
key: $
script: |
docker pull $/$:main
docker stop myapp || true
docker rm myapp || true
docker run -d \
--name myapp \
--restart unless-stopped \
-p 80:3000 \
$/$:main
|
2. 멀티 스테이지 Dockerfile (CI/CD용)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| # 테스트 스테이지
FROM node:18-alpine AS test
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run lint
CMD ["npm", "test"]
# 빌드 스테이지
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 프로덕션 스테이지
FROM node:18-alpine AS production
RUN apk add --no-cache dumb-init
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --chown=nodejs:nodejs package*.json ./
USER nodejs
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]
|
GitLab CI/CD
1. .gitlab-ci.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
| stages:
- test
- build
- deploy
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
before_script:
- docker info
test:
stage: test
image: docker:latest
services:
- docker:dind
script:
- docker build --target test -t test-image .
- docker run --rm test-image
only:
- merge_requests
- branches
build:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $IMAGE_TAG .
- docker tag $IMAGE_TAG $CI_REGISTRY_IMAGE:latest
- docker push $IMAGE_TAG
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
- develop
deploy-staging:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache curl
script:
- |
curl -X POST \
-H "Authorization: Bearer $DEPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"image\": \"$IMAGE_TAG\"}" \
https://staging.example.com/deploy
environment:
name: staging
url: https://staging.example.com
only:
- develop
deploy-production:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts
script:
- |
ssh $DEPLOY_USER@$DEPLOY_HOST << EOF
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
docker pull $IMAGE_TAG
docker stop myapp || true
docker rm myapp || true
docker run -d \
--name myapp \
--restart unless-stopped \
-p 80:3000 \
-e NODE_ENV=production \
$IMAGE_TAG
EOF
environment:
name: production
url: https://example.com
when: manual
only:
- main
|
Jenkins Pipeline
1. Jenkinsfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
| pipeline {
agent any
environment {
DOCKER_REGISTRY = 'registry.example.com'
DOCKER_IMAGE = 'myapp'
DOCKER_CREDENTIALS = 'docker-registry-credentials'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Test') {
steps {
script {
docker.build("${DOCKER_IMAGE}:test", "--target test .")
docker.image("${DOCKER_IMAGE}:test").inside {
sh 'npm test'
}
}
}
}
stage('Build') {
steps {
script {
dockerImage = docker.build("${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${env.BUILD_NUMBER}")
}
}
}
stage('Push') {
when {
branch 'main'
}
steps {
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", DOCKER_CREDENTIALS) {
dockerImage.push()
dockerImage.push('latest')
}
}
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
sshagent(['staging-ssh-key']) {
sh '''
ssh user@staging.example.com << EOF
docker pull ${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${BUILD_NUMBER}
docker stop myapp || true
docker rm myapp || true
docker run -d \
--name myapp \
--restart unless-stopped \
-p 80:3000 \
${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${BUILD_NUMBER}
EOF
'''
}
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
input {
message "Deploy to production?"
ok "Deploy"
}
steps {
sshagent(['production-ssh-key']) {
sh '''
ssh user@production.example.com << EOF
docker pull ${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${BUILD_NUMBER}
docker stop myapp || true
docker rm myapp || true
docker run -d \
--name myapp \
--restart unless-stopped \
-p 80:3000 \
-e NODE_ENV=production \
${DOCKER_REGISTRY}/${DOCKER_IMAGE}:${BUILD_NUMBER}
EOF
'''
}
}
}
}
post {
always {
cleanWs()
sh 'docker system prune -f'
}
}
}
|
2. Jenkins Docker 설정
1
2
3
4
5
6
7
8
9
10
| // Jenkins 설정 스크립트
pipeline {
agent {
docker {
image 'docker:dind'
args '--privileged -v /var/run/docker.sock:/var/run/docker.sock'
}
}
// ...
}
|
CircleCI
1. .circleci/config.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
| version: 2.1
orbs:
docker: circleci/docker@2.2.0
jobs:
test:
executor: docker/docker
steps:
- setup_remote_docker
- checkout
- docker/check
- run:
name: Build test image
command: |
docker build --target test -t myapp:test .
- run:
name: Run tests
command: |
docker run --rm myapp:test
build-and-push:
executor: docker/docker
steps:
- setup_remote_docker
- checkout
- docker/check
- docker/build:
image: $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME
tag: ${CIRCLE_SHA1:0:7},latest
- docker/push:
image: $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME
tag: ${CIRCLE_SHA1:0:7},latest
deploy:
docker:
- image: cimg/base:stable
steps:
- add_ssh_keys:
fingerprints:
- "SO:ME:FI:NG:ER:PR:IN:T"
- run:
name: Deploy to server
command: |
ssh -o StrictHostKeyChecking=no user@server.com << EOF
docker pull $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME:latest
docker stop myapp || true
docker rm myapp || true
docker run -d \
--name myapp \
--restart unless-stopped \
-p 80:3000 \
$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME:latest
EOF
workflows:
version: 2
test-build-deploy:
jobs:
- test
- build-and-push:
requires:
- test
filters:
branches:
only:
- main
- develop
- deploy:
requires:
- build-and-push
filters:
branches:
only: main
|
ArgoCD와 GitOps
1. Kubernetes 배포 매니페스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| # k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: ghcr.io/username/myapp:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
|
2. ArgoCD Application
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| # argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/username/myapp-k8s-config
targetRevision: HEAD
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
|
보안을 위한 CI/CD
1. 이미지 스캔 통합
1
2
3
4
5
6
7
8
9
10
11
12
13
| # GitHub Actions 예시
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: $/$:$
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
|
2. 시크릿 관리
1
2
3
4
5
6
7
8
9
10
11
12
| # GitHub Actions에서 시크릿 사용
- name: Build with secrets
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: $
secrets: |
"npm_token=$"
"api_key=$"
build-args: |
BUILDKIT_INLINE_CACHE=1
|
배포 전략
배포 전략 비교
| 전략 | 특징 | 장점 | 단점 | 적합한 경우 |
| Blue-Green | 두 환경 교체 | 빠른 롤백 | 리소스 2배 필요 | 빠른 롤백이 필수인 경우 |
| 카나리 | 점진적 배포 | 위험 최소화 | 복잡한 설정 | 새 기능 테스트 |
| 롤링 업데이트 | 순차적 교체 | 간단한 구현 | 일시적 비일관성 | 일반적인 업데이트 |
| A/B 테스팅 | 트래픽 분할 | 실험 가능 | 복잡한 모니터링 | 기능 비교 테스트 |
1. Blue-Green 배포
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| #!/bin/bash
# blue-green-deploy.sh
NEW_COLOR="green"
OLD_COLOR="blue"
if docker ps | grep -q "myapp-green"; then
NEW_COLOR="blue"
OLD_COLOR="green"
fi
echo "Deploying to $NEW_COLOR environment"
# 새 버전 배포
docker run -d \
--name myapp-$NEW_COLOR \
--network myapp-network \
-e COLOR=$NEW_COLOR \
$IMAGE_TAG
# 헬스체크
sleep 10
if ! curl -f http://localhost:8080/health; then
echo "Health check failed"
docker stop myapp-$NEW_COLOR
docker rm myapp-$NEW_COLOR
exit 1
fi
# 트래픽 전환
docker exec nginx sed -i "s/myapp-$OLD_COLOR/myapp-$NEW_COLOR/g" /etc/nginx/nginx.conf
docker exec nginx nginx -s reload
# 이전 버전 제거
sleep 30
docker stop myapp-$OLD_COLOR
docker rm myapp-$OLD_COLOR
|
2. 카나리 배포
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| # Kubernetes에서 Flagger 사용
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: myapp
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
progressDeadlineSeconds: 60
service:
port: 80
targetPort: 3000
analysis:
interval: 1m
threshold: 5
maxWeight: 50
stepWeight: 10
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 30s
|
모니터링과 알림
1. 배포 알림
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Slack 알림 예시
- name: Notify deployment
uses: 8398a7/action-slack@v3
with:
status: $
text: |
Deployment $
Repository: $
Branch: $
Commit: $
Author: $
env:
SLACK_WEBHOOK_URL: $
if: always()
|
2. 배포 대시보드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # Grafana 대시보드 설정
apiVersion: v1
kind: ConfigMap
metadata:
name: deployment-dashboard
data:
dashboard.json: |
{
"dashboard": {
"title": "CI/CD Metrics",
"panels": [
{
"title": "Deployment Frequency",
"targets": [{
"expr": "rate(deployments_total[5m])"
}]
},
{
"title": "Build Success Rate",
"targets": [{
"expr": "sum(rate(builds_total{status=\"success\"}[5m])) / sum(rate(builds_total[5m]))"
}]
}
]
}
}
|
베스트 프랙티스
CI/CD 톴 비교
| 톴 | 특징 | Docker 지원 | 설정 복잡도 | 가격 |
| GitHub Actions | GitHub 통합 | ★★★★★ | 낮음 | 무료/종량제 |
| GitLab CI | GitLab 통합 | ★★★★★ | 중간 | 무료/기업 |
| Jenkins | 오픈소스 | ★★★★☆ | 높음 | 무료 |
| CircleCI | 클라우드 | ★★★★★ | 낮음 | 무료/종량제 |
| Azure DevOps | MS 통합 | ★★★★☆ | 중간 | 무료/기업 |
| Travis CI | 간단한 설정 | ★★★★☆ | 낮음 | 종량제 |
1. 태그 전략
1
2
3
4
5
6
7
8
| # 시맨틱 버저닝
git tag v1.2.3
git push origin v1.2.3
# 자동 태깅
VERSION=$(cat VERSION)
git tag -a v$VERSION -m "Release version $VERSION"
git push origin v$VERSION
|
2. 캐시 최적화
1
2
3
4
5
6
7
8
9
10
11
12
13
| # BuildKit 캐시
- name: Build with cache
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: $
cache-from: |
type=gha
type=registry,ref=$/$:buildcache
cache-to: |
type=gha,mode=max
type=registry,ref=$/$:buildcache,mode=max
|
빌드 성능 최적화 결과
gantt
title Docker 빌드 시간 비교
dateFormat mm:ss
axisFormat %M:%S
section 기본 빌드
레이어 다운로드 :02:30, 150s
소스 복사 :00:20, 20s
빌드 실행 :01:00, 60s
section 캐시 사용
캐시 확인 :00:05, 5s
변경된 레이어만 :00:30, 30s
빌드 실행 :00:15, 15s
section 멀티스테이지 + 캐시
병렬 빌드 :00:05, 5s
변경 감지 :00:02, 2s
최종 조립 :00:08, 8s
마무리
Docker와 CI/CD의 결합은 현대적인 소프트웨어 배포의 핵심입니다. 자동화된 테스트, 빌드, 배포를 통해 더 빠르고 안정적인 릴리스가 가능해집니다. 다음 편에서는 Docker Swarm을 통한 오케스트레이션을 알아보겠습니다.
다음 편 예고
- Docker Swarm 아키텍처
- 서비스와 스택 관리
- 롤링 업데이트
- 고가용성 구성
컨테이너 오케스트레이션의 세계로! 🎯
📚 Docker 마스터하기 시리즈
🐳 기초편 (입문자용 - 5편)
- Docker란 무엇인가?
- Docker 설치 및 환경 설정
- 첫 번째 컨테이너 실행하기
- Docker 이미지 이해하기
- Dockerfile 작성하기
💼 실전편 (중급자용 - 6편)
- Docker 네트워크 기초
- Docker 볼륨과 데이터 관리
- Docker Compose 입문
- 멀티 컨테이너 애플리케이션
- Docker Hub 활용하기
- Docker 보안 베스트 프랙티스
🚀 고급편 (전문가용 - 9편)
- Docker 로그와 모니터링
- Docker로 Node.js 애플리케이션 배포
- Docker로 Python 애플리케이션 배포
- Docker로 데이터베이스 운영
- Docker 이미지 최적화
- Docker와 CI/CD ← 현재 글
- Docker Swarm 기초
- 문제 해결과 트러블슈팅
- Docker 생태계와 미래