Theme:

로컬에서 docker build하고 docker push하는 것을 매번 수동으로 하고 계신가요? 코드를 push하면 자동으로 이미지가 빌드되고, 테스트를 통과하면 배포까지 되는 파이프라인을 만들어봅시다.

CI/CD 파이프라인 구조

PLAINTEXT
코드 Push / PR


┌── GitHub Actions ──────────────────────┐
│  1. 코드 체크아웃                        │
│  2. Docker Buildx 설정                  │
│  3. 레지스트리 로그인                     │
│  4. 이미지 빌드 (캐시 활용)              │
│  5. 보안 스캐닝                          │
│  6. 테스트 실행                          │
│  7. 이미지 Push                          │
│  8. 배포 트리거                          │
└─────────────────────────────────────────┘

기본 빌드 워크플로

YAML
# .github/workflows/docker-build.yml
name: Docker Build & Push

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      # 1. 코드 체크아웃
      - name: Checkout
        uses: actions/checkout@v4

      # 2. Docker Buildx 설정
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      # 3. 레지스트리 로그인 (PR에서는 스킵)
      - name: Login to GHCR
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      # 4. 메타데이터 (태그/라벨 자동 생성)
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha

      # 5. 빌드 & Push
      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

태그 전략 자동화

docker/metadata-action은 Git 이벤트에 따라 자동으로 태그를 생성합니다.

태그 생성 규칙

Git 이벤트생성되는 태그
push to mainmain, sha-a1b2c3d
PR #42pr-42
tag v1.2.31.2.3, 1.2, sha-a1b2c3d
tag v2.0.02.0.0, 2.0, sha-a1b2c3d
YAML
# 상세 태그 설정
- name: Extract metadata
  id: meta
  uses: docker/metadata-action@v5
  with:
    images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
    tags: |
      # main 브랜치 → latest
      type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
      # SemVer 태그
      type=semver,pattern={{version}}
      type=semver,pattern={{major}}.{{minor}}
      type=semver,pattern={{major}}
      # Git SHA (짧은 형태)
      type=sha,prefix=
      # 브랜치 이름
      type=ref,event=branch

BuildKit 캐시 전략

GitHub Actions Cache (type=gha)

YAML
- name: Build and push
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    cache-from: type=gha
    cache-to: type=gha,mode=max
  • GitHub Actions의 캐시 스토리지(10GB 제한)를 활용
  • 설정이 가장 간단
  • mode=max: 모든 레이어를 캐시에 저장 (중간 스테이지 포함)

Registry Cache

YAML
- name: Build and push
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
    cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
  • 레지스트리에 캐시 이미지를 별도로 저장
  • 캐시 크기 제한이 레지스트리 용량에 의존
  • 여러 CI 러너 간에 캐시 공유 가능

보안 스캐닝 통합

YAML
jobs:
  build:
    # ... (빌드 단계)

  scan:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH
          exit-code: 1  # CRITICAL/HIGH 발견 시 실패

      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: trivy-results.sarif

GitHub의 Security 탭에서 취약점 결과를 확인할 수 있습니다.

테스트 통합

이미지 기반 테스트

YAML
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      db:
        image: postgres:16-alpine
        env:
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Build test image
        run: docker build --target test -t myapp:test .

      - name: Run tests
        run: |
          docker run --rm \
            --network host \
            -e DB_HOST=localhost \
            -e DB_PORT=5432 \
            -e DB_PASSWORD=testpass \
            myapp:test

Docker Compose를 사용한 통합 테스트

YAML
- name: Run integration tests
  run: |
    docker compose -f compose.test.yaml up -d
    docker compose -f compose.test.yaml run --rm test
    docker compose -f compose.test.yaml down -v

멀티 플랫폼 빌드

Apple Silicon(ARM)과 일반 서버(AMD64)를 모두 지원하는 이미지를 빌드합니다.

YAML
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # QEMU 설정 (ARM 에뮬레이션)
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

배포 자동화

SSH를 통한 서버 배포

YAML
  deploy:
    needs: [build, scan]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /opt/myapp
            docker compose pull
            docker compose up -d
            docker image prune -f

Webhook 기반 배포

YAML
  deploy:
    needs: [build, scan]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Trigger deployment
        run: |
          curl -X POST \
            -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d '{"image": "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"}' \
            https://deploy.example.com/api/deploy

Kubernetes 배포

YAML
  deploy:
    needs: [build, scan]
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')

    steps:
      - uses: actions/checkout@v4

      - name: Set up kubectl
        uses: azure/setup-kubectl@v4

      - name: Configure kubeconfig
        run: |
          echo "${{ secrets.KUBECONFIG }}" > kubeconfig.yaml

      - name: Deploy to Kubernetes
        run: |
          kubectl --kubeconfig=kubeconfig.yaml \
            set image deployment/myapp \
            myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}

      - name: Wait for rollout
        run: |
          kubectl --kubeconfig=kubeconfig.yaml \
            rollout status deployment/myapp --timeout=300s

전체 파이프라인 예시

YAML
name: CI/CD Pipeline

on:
  push:
    branches: [main]
    tags: ["v*"]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  # 1단계: 빌드 & 테스트
  build-and-test:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      image-tag: ${{ steps.meta.outputs.version }}

    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
            type=semver,pattern={{version}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # 2단계: 보안 스캐닝
  security-scan:
    needs: build-and-test
    runs-on: ubuntu-latest
    if: github.event_name != 'pull_request'
    steps:
      - name: Run Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
          severity: CRITICAL,HIGH
          exit-code: 1

  # 3단계: 배포 (태그 push 시에만)
  deploy:
    needs: [build-and-test, security-scan]
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')
    environment: production

    steps:
      - name: Deploy
        run: echo "배포 로직 실행"

유용한 팁

빌드 시간 단축

YAML
# 병렬 빌드 활용
- name: Build
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    cache-from: type=gha
    cache-to: type=gha,mode=max
    # BuildKit 병렬 빌드 활성화
    build-args: |
      BUILDKIT_INLINE_CACHE=1

시크릿 전달

YAML
- name: Build with secrets
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    secrets: |
      "npmrc=${{ secrets.NPM_TOKEN }}"

빌드 매트릭스

YAML
strategy:
  matrix:
    include:
      - service: api
        context: ./api
      - service: worker
        context: ./worker
      - service: web
        context: ./web

steps:
  - name: Build ${{ matrix.service }}
    uses: docker/build-push-action@v6
    with:
      context: ${{ matrix.context }}
      tags: ghcr.io/myorg/${{ matrix.service }}:${{ github.sha }}

정리

  • GitHub Actions에서 docker/build-push-action으로 이미지 빌드와 push를 자동화합니다.
  • docker/metadata-action으로 Git 이벤트에 따른 태그를 자동 생성합니다. SemVer 태그, SHA, latest 등을 한 번에 관리합니다.
  • BuildKit 캐시(type=gha)로 CI 환경에서도 레이어 캐시를 활용하여 빌드 시간을 줄입니다.
  • 빌드 후 Trivy로 보안 스캐닝을 하고, CRITICAL 취약점이 발견되면 파이프라인을 실패시킵니다.
  • 배포는 SSH, Webhook, Kubernetes 등 인프라에 맞는 방식을 선택합니다.
  • 전체 파이프라인은 빌드 → 스캐닝 → 배포 순서로 구성하고, 각 단계가 이전 단계의 성공에 의존하도록 합니다.
댓글 로딩 중...