DevToolBox무료
블로그

CI/CD 가이드: GitHub Actions, GitLab CI, Docker, 배포 파이프라인

13분 읽기by DevToolBox
TL;DR
  • CI builds and tests every commit; CD automates deployment to staging or production
  • GitHub Actions uses YAML workflows triggered by events; GitLab CI uses .gitlab-ci.yml with stages
  • Docker multi-stage builds produce small, secure images; layer caching keeps CI fast
  • Blue-green and canary deployments enable zero-downtime releases with safe rollback
  • Store secrets outside code using GitHub Secrets, HashiCorp Vault, or cloud secret managers
  • Speed up pipelines with parallelism, dependency caching, and path-based filtering
Key Takeaways
  • A failing pipeline is not a problem — it is a fast feedback loop that prevents broken code from reaching users
  • Treat your pipeline configuration as production code: review it, test it, version it
  • Start simple (push → test → deploy to staging) and add complexity only when the pain justifies it
  • Invest in caching early; it is the single highest-ROI pipeline optimization
  • OIDC-based authentication to cloud providers eliminates the need for long-lived credentials in CI

CI/CD (Continuous Integration and Continuous Delivery/Deployment) has become the backbone of modern software engineering. Teams that ship fast, ship reliably, and recover quickly from failures all share one thing in common: well-designed automated pipelines. This guide walks through everything you need to build production-grade CI/CD workflows using GitHub Actions, GitLab CI, Docker, and proven deployment patterns.

CI/CD Core Concepts

Continuous Integration (CI)

Continuous Integration means automatically triggering a build and test process every time a developer pushes code to a shared repository. The goal is to catch integration problems early and avoid "integration hell" — the mass of conflicts that emerge when multiple developers work in isolation for long periods before merging.

CI best practices: commit multiple times per day, keep builds under 10 minutes (slow builds breed slow feedback loops), fix a broken main branch immediately, and run CI on all branches.

Continuous Delivery (CD – Delivery)

Continuous Delivery extends CI by automatically packaging every passing build into a deployable artifact (Docker image, JAR, zip) and deploying it to a staging environment. Deployment to production still requires human approval.

Continuous Deployment (CD – Deployment)

Continuous Deployment goes one step further: every commit that passes all tests and checks is automatically deployed to production without human intervention. This is the most mature CI/CD pattern and requires a comprehensive automated test suite, feature flags, and robust monitoring.

AspectCIContinuous DeliveryContinuous Deployment
TriggerEvery pushEvery passing buildEvery passing build
Production deployManualManual approvalAutomatic
Human gateYes (deploy)Yes (prod deploy)None
Test suite requiredBasicComprehensiveVery comprehensive

GitHub Actions Deep Dive

GitHub Actions is GitHub's native CI/CD platform, launched in 2018 and now the default choice for open-source and many enterprise projects. It is event-driven, has thousands of community-maintained Actions, and integrates deeply with GitHub Issues, PRs, Packages, and Releases.

Workflow Syntax Fundamentals

Workflow files live in <code>.github/workflows/</code> and use YAML. Here is a complete annotated example:

# .github/workflows/ci.yml
name: CI Pipeline

# Triggers
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  workflow_dispatch:  # manual trigger

# Environment variables available to all jobs
env:
  NODE_VERSION: "20"

jobs:
  test:
    name: Run Tests
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run unit tests
        run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

Matrix Strategy

The matrix strategy fans out a single job definition across multiple parameter combinations, all running in parallel:

jobs:
  test-matrix:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false  # continue other combos if one fails
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: [18, 20, 22]
        exclude:
          - os: windows-latest
            node: 18  # skip this specific combo
        include:
          - os: ubuntu-latest
            node: 20
            experimental: true  # add extra property
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci && npm test

Job Dependencies and Conditional Execution

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4
      - id: meta
        run: echo "tags=myapp:${{ github.sha }}" >> $GITHUB_OUTPUT
      - run: docker build -t myapp:${{ github.sha }} .

  deploy-staging:
    needs: build
    if: github.ref == "refs/heads/main"
    environment: staging
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying ${{ needs.build.outputs.image-tag }} to staging"

  deploy-prod:
    needs: deploy-staging
    if: github.event_name == "push" && github.ref == "refs/heads/main"
    environment:
      name: production
      url: https://myapp.com
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to production"

Secrets and OIDC Authentication

Avoid storing long-lived credentials. Use GitHub OIDC to request short-lived credentials from AWS, GCP, or Azure directly:

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # required for OIDC
      contents: read
    steps:
      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1
          # No ACCESS_KEY_ID or SECRET_ACCESS_KEY needed!

      - name: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster prod-cluster \
            --service myapp \
            --force-new-deployment

GitLab CI/CD Complete Guide

GitLab CI centralizes pipeline configuration in <code>.gitlab-ci.yml</code> at the repository root. It organizes work into stages — jobs within a stage run in parallel, stages run sequentially.

Complete .gitlab-ci.yml Example

# .gitlab-ci.yml
image: node:20-alpine

stages:
  - install
  - test
  - build
  - deploy

variables:
  npm_config_cache: "$CI_PROJECT_DIR/.npm"

# Reusable cache configuration
.node-cache: &node-cache
  cache:
    key:
      files:
        - package-lock.json
    paths:
      - .npm/
      - node_modules/

install-deps:
  stage: install
  <<: *node-cache
  script:
    - npm ci
  artifacts:
    paths:
      - node_modules/
    expire_in: 1 hour

unit-tests:
  stage: test
  <<: *node-cache
  script:
    - npm test -- --coverage --ci
  coverage: /All files[^|]*|[^|]*s+([d.]+)/
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
    paths:
      - coverage/

build-app:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week
  only:
    - main
    - merge_requests

deploy-staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.myapp.com
  script:
    - echo "Deploying to staging..."
    - ./scripts/deploy.sh staging
  only:
    - main

deploy-production:
  stage: deploy
  environment:
    name: production
    url: https://myapp.com
  script:
    - ./scripts/deploy.sh production
  when: manual  # requires human approval
  only:
    - main

Rules and Path Filtering

The <code>rules</code> keyword gives fine-grained control over when jobs run — especially valuable in monorepos:

build-api:
  stage: build
  script:
    - cd packages/api && npm run build
  rules:
    # Run on merge request if api files changed
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - packages/api/**/*
        - packages/shared/**/*
    # Always run on main branch pushes
    - if: $CI_COMMIT_BRANCH == "main"

build-frontend:
  stage: build
  script:
    - cd packages/frontend && npm run build
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      changes:
        - packages/frontend/**/*
        - packages/shared/**/*
    - if: $CI_COMMIT_BRANCH == "main"

GitLab Runner Types

Runner TypeUse CaseIsolationNotes
ShellLegacy / simple jobsNoneRuns directly on host
DockerMost workloadsContainerClean env each run
KubernetesScale-out / cloud-nativePodAuto-scales runner pods
Instance (SaaS)gitlab.com usersVMFree tier: 400 min/month

Docker in CI/CD Pipelines

Multi-Stage Builds

Multi-stage builds are the standard pattern for production Dockerfiles. They separate the build environment (compilers, test tools) from the runtime image (only runtime dependencies), producing smaller and more secure images.

# Dockerfile

# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: Runtime (smallest possible image)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# Copy only what is needed at runtime
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

# Run as non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 3000
CMD ["node", "dist/server.js"]

Building and Pushing Images in GitHub Actions

jobs:
  build-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write  # for GHCR
    steps:
      - uses: actions/checkout@v4

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

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

      - name: Extract Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=sha
            type=ref,event=branch
            type=semver,pattern={{version}}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha        # GitHub Actions cache
          cache-to: type=gha,mode=max

Docker Layer Caching Best Practices

Docker caches layers top-to-bottom. Once a layer is invalidated, all subsequent layers must rebuild. Layer order in your Dockerfile is critical:

  • Copy package.json and lock files FIRST, then run npm ci — this layer only invalidates when dependencies change
  • Copy source code AFTER installing dependencies — source changes are frequent but fast to copy
  • Use .dockerignore to exclude node_modules, .git, test files, and docs from the build context
  • Use --mount=type=cache in BuildKit for package manager caches that persist across builds

Deployment Strategies Explained

Rolling Deployment

Rolling deployment gradually replaces instances of the old version with the new one. With 10 instances, you might replace 2 at a time until all are updated. Kubernetes Deployments use this strategy by default. Pros: low resource overhead. Cons: old and new versions coexist during rollout, requiring backward-compatible APIs.

# kubernetes-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 10
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2       # max extra pods during rollout
      maxUnavailable: 0 # never reduce below desired count
  template:
    spec:
      containers:
        - name: myapp
          image: ghcr.io/myorg/myapp:v2.1.0
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 5

Blue-Green Deployment

Blue-green maintains two identical production environments. Traffic is always routed to one of them. To deploy, you update the idle environment, run smoke tests, then switch the load balancer. Rollback is instantaneous — just switch the load balancer back.

# blue-green deploy script
#!/bin/bash
set -euo pipefail

CURRENT=$(aws elbv2 describe-target-groups \
  --names myapp-blue myapp-green \
  --query "TargetGroups[?Tags[?Key=='active' && Value=='true']].TargetGroupName" \
  --output text)

if [ "$CURRENT" = "myapp-blue" ]; then
  NEW_TG="myapp-green"
  OLD_TG="myapp-blue"
else
  NEW_TG="myapp-blue"
  OLD_TG="myapp-green"
fi

echo "Deploying to $NEW_TG"

# Update the idle target group
aws ecs update-service --cluster prod --service "myapp-${NEW_TG}" \
  --task-definition "myapp:$NEW_TASK_DEF_REVISION" \
  --force-new-deployment

# Wait for stability
aws ecs wait services-stable --cluster prod --services "myapp-${NEW_TG}"

# Run smoke tests
./scripts/smoke-test.sh "https://staging.myapp.com"

# Switch traffic
aws elbv2 modify-listener \
  --listener-arn $LISTENER_ARN \
  --default-actions "Type=forward,TargetGroupArn=$(aws elbv2 describe-target-groups --names $NEW_TG --query TargetGroups[0].TargetGroupArn --output text)"

echo "Successfully switched traffic to $NEW_TG"

Canary Deployment

Canary deployments route a small percentage of real production traffic to the new version while monitoring metrics. If metrics are healthy, the percentage is gradually increased. If not, traffic is instantly shifted back.

Testing Strategies in Pipelines

The Test Pyramid

An effective CI test suite follows the test pyramid: many fast unit tests (milliseconds), fewer integration tests (seconds), and few end-to-end tests (minutes). Lower layers run faster, cost less to maintain, and give faster feedback.

# Full test pipeline with coverage gate
jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "npm" }
      - run: npm ci
      - run: npm run test:unit -- --coverage
      - name: Check coverage threshold
        run: |
          COVERAGE=$(cat coverage/coverage-summary.json | \
            jq ".total.lines.pct")
          echo "Coverage: $COVERAGE%"
          if (( $(echo "$COVERAGE < 80" | bc -l) )); then
            echo "Coverage $COVERAGE% is below 80% threshold"
            exit 1
          fi

  integration-tests:
    runs-on: ubuntu-latest
    needs: unit-tests
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      redis:
        image: redis:7-alpine
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "npm" }
      - run: npm ci
      - run: npm run test:integration
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379

Environment Management

Typical Multi-Environment Architecture

EnvironmentTriggerApprovalPurpose
PreviewEvery PRNonePR review, feature demo
StagingMerge to mainNone (auto)QA, integration, UAT
ProductionTag / releaseRequiredLive user traffic

Environment Variables and Secrets Management

Different environments require different configurations. Non-sensitive config (feature flags, API endpoints) goes in CI environment variables. Sensitive data (DB passwords, API keys) belongs in a dedicated secrets manager.

# Using HashiCorp Vault in GitHub Actions
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Import Secrets from Vault
        uses: hashicorp/vault-action@v3
        with:
          url: https://vault.mycompany.com
          method: jwt
          role: github-actions
          secrets: |
            secret/data/myapp/prod database_url | DATABASE_URL ;
            secret/data/myapp/prod redis_url | REDIS_URL ;
            secret/data/myapp/prod stripe_key | STRIPE_SECRET_KEY

      - name: Deploy
        run: ./scripts/deploy.sh
        env:
          DATABASE_URL: ${{ env.DATABASE_URL }}
          REDIS_URL: ${{ env.REDIS_URL }}

Monorepo CI/CD

Monorepos containing multiple services or packages present a unique CI challenge: how do you avoid rebuilding everything on every commit? The answer is path filtering combined with incremental build tools.

Monorepo Pipeline with Turborepo

# .github/workflows/monorepo-ci.yml
name: Monorepo CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      api: ${{ steps.filter.outputs.api }}
      frontend: ${{ steps.filter.outputs.frontend }}
      shared: ${{ steps.filter.outputs.shared }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            api:
              - "packages/api/**"
              - "packages/shared/**"
            frontend:
              - "packages/frontend/**"
              - "packages/shared/**"
            shared:
              - "packages/shared/**"

  test-api:
    needs: changes
    if: needs.changes.outputs.api == "true"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "npm" }
      - run: npm ci
      - run: npx turbo run test --filter=@myapp/api...

  test-frontend:
    needs: changes
    if: needs.changes.outputs.frontend == "true"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "npm" }
      - run: npm ci
      - run: npx turbo run test --filter=@myapp/frontend...

Pipeline Performance Optimization

Dependency Caching

# Cache node_modules across runs
- name: Cache node modules
  uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      node_modules
    key: ${{ runner.os }}-node-${{ hashFiles("**/package-lock.json") }}
    restore-keys: |
      ${{ runner.os }}-node-

# For Python projects
- uses: actions/setup-python@v5
  with:
    python-version: "3.12"
    cache: "pip"

# For Rust projects
- uses: Swatinem/rust-cache@v2
  with:
    workspaces: ". -> target"

# For Gradle (Android / Java)
- name: Cache Gradle
  uses: actions/cache@v4
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles("**/*.gradle*", "**/gradle-wrapper.properties") }}

Test Parallelism

# Split test suite across 4 parallel jobs
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: "20", cache: "npm" }
      - run: npm ci
      - name: Run test shard
        run: |
          npx jest \
            --shard=${{ matrix.shard }}/4 \
            --coverage \
            --ci

Notifications and Status Checks

Slack Notification Integration

# Notify Slack on deployment success or failure
- name: Notify Slack on success
  if: success()
  uses: slackapi/slack-github-action@v1
  with:
    channel-id: "deployments"
    payload: |
      {
        "text": ":white_check_mark: Deployed to production",
        "attachments": [{
          "color": "good",
          "fields": [
            { "title": "Version", "value": "${{ github.sha }}", "short": true },
            { "title": "Author", "value": "${{ github.actor }}", "short": true }
          ]
        }]
      }
  env:
    SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

- name: Notify Slack on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    channel-id: "deployments"
    payload: |
      {
        "text": ":x: Production deployment FAILED",
        "attachments": [{
          "color": "danger",
          "fields": [
            { "title": "Run URL", "value": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}", "short": false }
          ]
        }]
      }
  env:
    SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

GitHub Actions vs GitLab CI vs CircleCI Comparison

FeatureGitHub ActionsGitLab CICircleCI
Config file.github/workflows/*.yml.gitlab-ci.yml.circleci/config.yml
Free tier2,000 min/month400 min/month6,000 min/month
Marketplace20,000+ actionsComponent catalogOrbs registry
Self-hostedSelf-hosted runnersGitLab RunnerSelf-hosted runners
Docker supportServices containersServices + DinDNative Docker layer
Cachingactions/cachecache: keywordrestore_cache step
OIDC cloud authYes (AWS/GCP/Azure)Yes (ID tokens)Yes (OIDC contexts)
Best forGitHub-hosted reposSelf-hosted GitLabSpeed-focused teams
ParallelismMatrix + jobsParallel + needsNative parallel jobs

Complete Production-Grade Workflow Example

Here is a complete real-world CI/CD workflow for a Node.js application, from commit to production:

# .github/workflows/production.yml
name: Production Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true  # cancel stale PR runs

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

jobs:
  quality:
    name: Code Quality
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck

  test:
    name: Tests
    runs-on: ubuntu-latest
    needs: quality
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - run: npm test -- --coverage
      - uses: codecov/codecov-action@v4

  build-image:
    name: Build Docker Image
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == "push"
    permissions:
      contents: read
      packages: write
      id-token: write
    outputs:
      digest: ${{ steps.build.outputs.digest }}
      image: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
      - id: build
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: build-image
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - name: Configure AWS
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_STAGING_ROLE }}
          aws-region: us-east-1
      - run: |
          aws ecs update-service \
            --cluster staging \
            --service myapp \
            --image-override ${{ needs.build-image.outputs.image }} \
            --force-new-deployment
      - run: aws ecs wait services-stable --cluster staging --services myapp
      - run: ./scripts/smoke-test.sh https://staging.myapp.com

  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment:
      name: production
      url: https://myapp.com
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_PROD_ROLE }}
          aws-region: us-east-1
      - run: |
          aws ecs update-service \
            --cluster production \
            --service myapp \
            --image-override ${{ needs.build-image.outputs.image }} \
            --force-new-deployment
      - run: aws ecs wait services-stable --cluster production --services myapp
      - name: Notify Slack
        if: always()
        uses: slackapi/slack-github-action@v1
        with:
          channel-id: deployments
          payload: |
            {
              "text": "${{ job.status == 'success' && ':white_check_mark: Production deploy succeeded' || ':x: Production deploy FAILED' }}"
            }
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

Frequently Asked Questions (FAQ)

Q: What is the difference between CI, CD (Delivery), and CD (Deployment)?

Continuous Integration (CI) automatically builds and tests code on every commit. Continuous Delivery (CD) extends CI by automatically preparing a release artifact that is ready to deploy but requires manual approval to go live. Continuous Deployment goes one step further and automatically deploys every passing build to production without human intervention.

Q: How do I securely pass secrets in GitHub Actions?

Store sensitive values in Settings > Secrets and variables > Actions in your repository. Reference them in workflows using the ${{ secrets.YOUR_SECRET_NAME }} syntax. Never hard-code secrets in workflow files or print them in logs. For advanced cases, use GitHub OIDC to assume cloud roles without storing long-lived credentials at all.

Q: What is a matrix strategy in GitHub Actions?

A matrix strategy allows a single job definition to run across multiple combinations of variables (e.g., Node.js versions, operating systems). GitHub Actions fans out the job automatically, running all combinations in parallel. This is useful for cross-platform testing or multi-version compatibility checks without duplicating job definitions.

Q: How does Docker layer caching speed up CI builds?

Docker caches each layer of an image. If a layer and all preceding layers are unchanged, Docker reuses the cached result instead of rebuilding. In CI you can use --cache-from to pull a previously built image and use its layers as cache. Structuring your Dockerfile so dependency installation (slow, rarely changes) comes before source code copying (fast, changes frequently) maximizes cache hits.

Q: What is a blue-green deployment?

Blue-green deployment maintains two identical production environments called "blue" and "green". At any time, one environment serves live traffic. When deploying a new version, you deploy to the idle environment, run smoke tests, then switch the load balancer to route traffic to it. If problems occur, you instantly roll back by switching the load balancer back. This achieves zero-downtime deployments with a simple rollback path.

Q: How do I trigger a GitLab CI pipeline only for changed files in a monorepo?

Use the changes keyword under rules in your .gitlab-ci.yml. For example: rules: [{ if: "$CI_PIPELINE_SOURCE == "push"", changes: ["packages/api/**/*"] }]. This tells GitLab to only run that job when files under packages/api/ are modified. Combine with needs to build a dependency graph between jobs so downstream jobs only run if their upstream counterparts ran.

Q: What is a canary deployment and when should I use it?

A canary deployment routes a small percentage of real production traffic (e.g., 5%) to a new version while the rest continues to run the stable version. You monitor error rates, latency, and business metrics. If metrics look healthy you gradually increase the canary percentage until 100% of traffic runs the new version. Use canary deployments for high-traffic services where even a short outage is costly and you want to validate behavior under real load before full rollout.

Q: How do I optimize slow CI pipelines?

The main levers are: (1) Parallelism — split test suites across multiple runners. (2) Caching — cache dependency directories (node_modules, .gradle, ~/.cargo) between runs. (3) Path filtering — skip jobs for unrelated changes. (4) Fail-fast — cancel remaining matrix jobs when one fails. (5) Incremental builds — use tools like Nx, Turborepo, or Bazel to only rebuild affected packages. (6) Use faster runners — GitHub larger runners or self-hosted runners with SSDs can dramatically cut I/O-bound steps.

Summary and Recommended Path Forward

The best strategy for building CI/CD pipelines is to start simple and add complexity only as pain points emerge. For most teams, the recommended path is:

  1. Week 1: Set up basic CI (push → test). Keep main always deployable.
  2. Week 2: Add Docker build and push to a registry. Add auto-deploy to staging.
  3. Week 3: Add caching and matrix tests. Optimize for build speed.
  4. Week 4: Migrate to OIDC auth. Add notifications and production approval gates.
  5. Beyond: Explore blue-green/canary deployments, monorepo path filtering, and advanced security scanning as needed.

Core principle: A failing pipeline is not a problem — it is a fast feedback mechanism that stopped broken code from reaching users. A pipeline that never fails is either not testing anything meaningful or not protecting anything. Invest in making failures fast, obvious, and easy to fix rather than trying to make pipelines never fail.

𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

{ }JSON Formatter

Related Articles

Kubernetes 개발자 완전 가이드: Pod, Helm, RBAC, CI/CD

Kubernetes를 마스터하세요. Pod, Deployment, Service, Ingress, Helm, PVC, 헬스체크, HPA, RBAC, GitHub Actions CI/CD 통합 완전 가이드.

Docker 명령어: 기초부터 프로덕션까지 완전 가이드

Docker를 마스터하세요. docker run/build/push, Dockerfile, 멀티스테이지 빌드, 볼륨, 네트워킹, Docker Compose, 보안, 레지스트리, 프로덕션 배포 완전 가이드.

Terraform 완전 가이드: 기초부터 CI/CD까지의 Infrastructure as Code

Terraform IaC를 마스터하세요. HCL 구문, 모듈, 상태 관리, AWS 프로바이더, 워크스페이스, Terraform Cloud, tfsec/checkov/Terratest 테스트 완전 가이드.