본문 바로가기

DevOps

GitHub Actions로 Docker 이미지를 빌드하고 배포하기: CI/CD 파이프라인 실습

반응형

코드를 Push하면 Docker 이미지가 자동으로 빌드되고, 테스트를 통과한 뒤 프로덕션에 배포됩니다. 이 흐름을 GitHub Actions로 구성하는 방법을 단계별로 설명합니다. 단순히 "돌아가는" 파이프라인이 아니라, 캐싱으로 빌드 시간을 줄이고, OIDC로 보안을 강화하고, 이미지 스캔으로 취약점을 차단하는 운영 수준의 워크플로우를 만듭니다.

핵심 요약

  • GitHub Actions에서 Docker 이미지를 빌드하려면 docker/build-push-action을 사용합니다. BuildKit 기반으로 Multi-platform 빌드와 레이어 캐싱을 지원합니다.
  • AWS ECR에 Push할 때는 OIDC 인증을 사용하면 장기 Access Key 없이 임시 자격 증명으로 접근할 수 있습니다.
  • 레이어 캐싱(cache-from/cache-to)을 적용하면 빌드 시간을 50~80% 단축할 수 있습니다.
  • 이미지 빌드 후 취약점 스캔을 추가하면 알려진 CVE가 포함된 이미지가 배포되는 것을 차단할 수 있습니다.
  • 전체 흐름: 코드 Push → 이미지 빌드 → 취약점 스캔 → ECR Push → ECS 배포

1. 전체 파이프라인 구조

3명이 백엔드 API를 개발하는 팀을 가정합니다. 코드는 GitHub에, 컨테이너 이미지는 AWS ECR에, 서비스는 ECS Fargate에서 운영합니다.

이 팀이 원하는 CI/CD 흐름은 다음과 같습니다.

  1. 개발자가 feature 브랜치에서 PR을 생성합니다.
  2. PR 이벤트로 이미지 빌드 + 테스트가 자동 실행됩니다.
  3. PR이 merge되면 main 브랜치에서 프로덕션 이미지를 빌드합니다.
  4. 빌드된 이미지를 ECR에 Push합니다.
  5. ECS 서비스를 새 이미지로 업데이트합니다.
GitHub Actions Docker 배포 파이프라인 구조
GitHub Actions Docker 배포 파이프라인 구조

2. 사전 준비

2.1 필요한 AWS 리소스

리소스 용도 비고
ECR Repository Docker 이미지 저장소 리전당 1개 생성
ECS Cluster + Service 컨테이너 실행 환경 Fargate 또는 EC2
IAM OIDC Provider GitHub Actions 인증용 GitHub의 OIDC 엔드포인트 등록
IAM Role ECR Push + ECS Deploy 권한 OIDC 신뢰 정책 연결

2.2 ECR Repository 생성

aws ecr create-repository \
  --repository-name my-api \
  --region ap-northeast-2 \
  --image-scanning-configuration scanOnPush=true \
  --encryption-configuration encryptionType=KMS

scanOnPush=true를 설정하면 이미지가 Push될 때 자동으로 취약점 스캔이 실행됩니다. CI 단계에서 별도 스캔을 추가하는 것과 중복처럼 보이지만, 두 레이어를 모두 적용하는 것이 방어 깊이(Defense in Depth) 관점에서 안전합니다.

2.3 OIDC Provider 구성

GitHub Actions에서 AWS 리소스에 접근할 때 Access Key를 Secret에 저장하는 대신 OIDC를 사용합니다.

# GitHub OIDC Provider 등록
aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

OIDC를 사용하면 장기 자격 증명이 존재하지 않으므로 키 유출 위험이 구조적으로 제거됩니다. GitHub Actions가 실행될 때마다 임시 토큰을 발급받고, 작업이 끝나면 토큰이 만료됩니다.

2.4 IAM Role 생성

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:my-org/my-api:ref:refs/heads/main"
        }
      }
    }
  ]
}

Conditionsub 필드가 중요합니다. repo:my-org/my-api:ref:refs/heads/main으로 설정하면 main 브랜치에서 실행되는 워크플로우만 이 Role을 사용할 수 있습니다. PR이나 다른 브랜치에서는 접근이 차단됩니다.

Security Note
sub 조건을 repo:my-org/my-api:*로 설정하면 해당 저장소의 모든 브랜치, 모든 이벤트에서 Role을 사용할 수 있습니다. fork한 외부 기여자의 PR에서도 접근 가능해지므로 반드시 범위를 제한해야 합니다.

IAM Role에 연결할 정책:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecr:BatchGetImage",
        "ecr:BatchCheckLayerAvailability",
        "ecr:CompleteLayerUpload",
        "ecr:GetDownloadUrlForLayer",
        "ecr:InitiateLayerUpload",
        "ecr:PutImage",
        "ecr:UploadLayerPart"
      ],
      "Resource": "arn:aws:ecr:ap-northeast-2:123456789012:repository/my-api"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecs:UpdateService",
        "ecs:DescribeServices",
        "ecs:DescribeTaskDefinition",
        "ecs:RegisterTaskDefinition"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole"
    }
  ]
}

3. Dockerfile 작성 (Multi-stage Build)

운영 환경에서는 Multi-stage Build를 사용하여 최종 이미지 크기를 줄이고, 빌드 도구가 프로덕션 이미지에 포함되지 않도록 합니다.

# Stage 1: 의존성 설치 및 빌드
FROM node:20-alpine AS builder

WORKDIR /app

# 의존성 파일만 먼저 복사 (레이어 캐싱 활용)
COPY package.json package-lock.json ./
RUN npm ci --only=production && cp -R node_modules prod_modules
RUN npm ci

# 소스 복사 및 빌드
COPY . .
RUN npm run build

# Stage 2: 프로덕션 이미지
FROM node:20-alpine AS production

# 보안: root가 아닌 사용자로 실행
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001 -G appgroup

WORKDIR /app

# 프로덕션 의존성만 복사
COPY --from=builder /app/prod_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

# 불필요한 권한 제거
RUN chown -R appuser:appgroup /app
USER appuser

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "dist/main.js"]

왜 Multi-stage Build인가

항목 단일 Stage Multi-stage
이미지 크기 ~900MB (개발 도구 포함) ~150MB (런타임만)
공격 표면 빌드 도구, 소스 코드 노출 실행에 필요한 것만 포함
빌드 캐싱 소스 변경 시 전체 재빌드 의존성 레이어 캐싱 가능

package.json을 먼저 복사하고 npm ci를 실행하는 이유는 레이어 캐싱 때문입니다. 소스 코드가 변경되어도 package.json이 같으면 의존성 설치 레이어를 재사용합니다.

4. 기본 워크플로우 작성

GitHub Actions Docker 빌드 워크플로우 흐름
GitHub Actions Docker 빌드 워크플로우 흐름

4.1 전체 워크플로우 파일

# .github/workflows/docker-deploy.yml
name: Build and Deploy

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

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: my-api
  ECS_CLUSTER: prod-cluster
  ECS_SERVICE: api-service

jobs:
  build:
    name: Build & Push Docker Image
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # OIDC 인증
      contents: read    # 코드 체크아웃

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecr
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: ecr-login
        uses: aws-actions/amazon-ecr-login@v2

      - name: Docker meta (태그 생성)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ steps.ecr-login.outputs.registry }}/${{ env.ECR_REPOSITORY }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch
            type=ref,event=pr

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

      - 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
          platforms: linux/amd64

  scan:
    name: Vulnerability Scan
    needs: build
    if: github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecr
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        uses: aws-actions/amazon-ecr-login@v2

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ needs.build.outputs.image-tag }}
          format: table
          exit-code: '1'
          severity: CRITICAL,HIGH

  deploy:
    name: Deploy to ECS
    needs: [build, scan]
    if: github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    environment: production

    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecr
          aws-region: ${{ env.AWS_REGION }}

      - name: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster ${{ env.ECS_CLUSTER }} \
            --service ${{ env.ECS_SERVICE }} \
            --force-new-deployment

4.2 각 단계 설명

단계 Action 역할
Checkout actions/checkout@v4 소스 코드를 Runner에 복사
AWS 인증 aws-actions/configure-aws-credentials@v4 OIDC로 임시 자격 증명 발급
ECR 로그인 aws-actions/amazon-ecr-login@v2 Docker가 ECR에 Push할 수 있도록 인증
태그 생성 docker/metadata-action@v5 commit SHA, 브랜치명 기반 태그 자동 생성
Buildx 설정 docker/setup-buildx-action@v3 BuildKit 활성화 (캐싱, Multi-platform 지원)
빌드 & Push docker/build-push-action@v6 이미지 빌드 + ECR Push
취약점 스캔 aquasecurity/trivy-action CRITICAL/HIGH 취약점 발견 시 파이프라인 중단
배포 aws ecs update-service ECS 서비스가 새 이미지를 Pull하도록 트리거

5. 이미지 태깅 전략

이미지 태그를 어떻게 지정하느냐에 따라 배포 추적과 롤백 용이성이 달라집니다.

5.1 태깅 방식 비교

방식 예시 장점 단점
latest my-api:latest 단순함 어떤 커밋인지 추적 불가, 롤백 어려움
Git SHA my-api:a1b2c3d 정확한 커밋 추적 사람이 읽기 어려움
시맨틱 버전 my-api:1.2.3 명확한 릴리스 구분 태깅 프로세스 필요
SHA + Branch my-api:a1b2c3d, my-api:main 추적 + 최신 참조 모두 가능 태그 2개 관리

운영 환경에서는 Git SHA 기반 태그를 권장합니다. 배포된 이미지가 어떤 커밋에서 빌드되었는지 즉시 확인할 수 있고, 롤백 시 이전 SHA 태그를 지정하면 됩니다.

5.2 latest 태그를 피해야 하는 이유

# ❌ 이렇게 하면 안 됩니다
tags: my-api:latest

latest는 "가장 최근에 Push된 이미지"를 가리키는 mutable 태그입니다. 문제는:

  • ECS Task Definition에 latest를 지정하면 언제 어떤 버전이 배포되는지 예측할 수 없습니다.
  • 롤백 시 "이전 latest"가 무엇이었는지 확인할 방법이 없습니다.
  • 여러 브랜치에서 빌드하면 latest가 덮어써집니다.

docker/metadata-action을 사용하면 commit SHA, 브랜치명, PR 번호 기반의 태그를 자동으로 생성할 수 있습니다.

6. 빌드 캐싱 전략

Docker 이미지 빌드에서 가장 시간이 많이 걸리는 부분은 의존성 설치입니다. 캐싱을 적용하면 변경되지 않은 레이어를 재사용하여 빌드 시간을 크게 줄일 수 있습니다.

GitHub Actions Docker 캐싱 전략 비교
GitHub Actions Docker 캐싱 전략 비교

6.1 캐싱 방식 비교

방식 설정 속도 비용 적합 상황
GitHub Actions Cache (gha) cache-from: type=gha 빠름 GitHub 캐시 용량 사용 (10GB 제한) 대부분의 프로젝트
Registry Cache cache-from: type=registry 중간 ECR 저장 비용 여러 Runner에서 공유 필요 시
캐싱 없음 - 느림 분당 과금 증가 -

6.2 GitHub Actions Cache (권장)

- 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

mode=max는 빌드의 모든 레이어를 캐싱합니다. Multi-stage Build에서 중간 단계의 레이어도 캐싱되므로 다음 빌드에서 최대한 재사용할 수 있습니다.

6.3 Registry Cache (대안)

GitHub Cache 용량(10GB)이 부족하거나, Self-hosted Runner에서 캐시 공유가 필요하면 ECR에 캐시 이미지를 저장하는 방식을 사용합니다.

- name: Build and Push
  uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    cache-from: type=registry,ref=${{ steps.ecr-login.outputs.registry }}/my-api:buildcache
    cache-to: type=registry,ref=${{ steps.ecr-login.outputs.registry }}/my-api:buildcache,mode=max

6.4 캐싱 효과 측정

Node.js 프로젝트 기준 일반적인 빌드 시간 비교:

상황 캐싱 없음 GHA Cache 적용
의존성 변경 없음, 소스만 변경 3~5분 30초~1분
의존성 추가 (package.json 변경) 3~5분 2~3분
Dockerfile 구조 변경 3~5분 3~5분 (캐시 무효화)
Tip
캐시가 기대만큼 동작하지 않으면 Dockerfile의 레이어 순서를 확인합니다. 자주 변경되는 파일(COPY . .)을 가능한 뒤쪽에 배치해야 이전 레이어의 캐시가 유지됩니다.

7. 보안 강화

7.1 이미지 취약점 스캔

빌드된 이미지에 알려진 취약점(CVE)이 포함되어 있으면 배포를 차단해야 합니다. Trivy를 사용하면 이미지의 OS 패키지와 애플리케이션 의존성을 스캔합니다.

- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: ${{ steps.ecr-login.outputs.registry }}/my-api:${{ github.sha }}
    format: table
    exit-code: '1'           # 취약점 발견 시 파이프라인 실패
    severity: CRITICAL,HIGH  # CRITICAL, HIGH만 차단
    ignore-unfixed: true     # 아직 패치가 없는 취약점은 무시

exit-code: '1'은 취약점 발견 시 해당 Step을 실패로 만듭니다. deploy Job이 scan Job에 의존(needs: [build, scan])하므로 스캔 실패 시 배포가 자동으로 차단됩니다.

7.2 Action 버전 고정

# ❌ 위험: 태그는 mutable — 공급망 공격에 취약
- uses: actions/checkout@v4

# ✅ 안전: SHA 고정 — 코드가 변경되면 SHA가 달라짐
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1

Marketplace Action의 태그(v4)는 관리자가 언제든지 다른 커밋을 가리키도록 변경할 수 있습니다. SHA를 고정하면 의도하지 않은 코드 변경이 파이프라인에 유입되는 것을 방지합니다.

다만 모든 Action을 SHA로 고정하면 업데이트 추적이 어렵습니다. Dependabot이나 Renovate를 설정하여 Action 버전 업데이트를 PR로 받으면 보안과 유지보수를 모두 챙길 수 있습니다.

7.3 최소 권한 permissions 설정

jobs:
  build:
    permissions:
      id-token: write   # OIDC에 필요
      contents: read    # 코드 읽기에 필요
      # packages: write  # GHCR Push 시 필요 (ECR 사용 시 불필요)

GitHub Actions는 기본적으로 GITHUB_TOKEN에 넓은 권한을 부여합니다. permissions를 명시적으로 선언하면 해당 Job에서 사용할 수 있는 권한이 선언된 것만으로 제한됩니다.

7.4 Secret 관리

# ❌ 나쁨: 하드코딩
env:
  AWS_ACCOUNT_ID: "123456789012"

# ✅ 좋음: GitHub Secrets 또는 Variables 사용
env:
  AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}

OIDC를 사용하면 AWS Access Key를 Secret에 저장할 필요가 없습니다. 다만 계정 ID, 리전 등 민감하지 않지만 환경마다 다른 값은 GitHub Variables(vars.)에 저장하면 관리가 편합니다.

8. PR 환경에서의 워크플로우

PR에서는 이미지를 Push하지 않고 빌드 성공 여부만 확인합니다. 이는 push 조건으로 제어합니다.

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

PR에서 이미지를 Push하지 않는 이유:

  • 불필요한 이미지가 ECR에 쌓여 저장 비용이 증가합니다.
  • merge되지 않을 수도 있는 코드의 이미지가 레지스트리에 존재하면 혼란을 줍니다.
  • push: false로 설정해도 빌드 자체는 실행되므로 Dockerfile 오류를 잡을 수 있습니다.

9. ECS 배포 상세

9.1 단순 배포 (force-new-deployment)

- name: Deploy to ECS
  run: |
    aws ecs update-service \
      --cluster ${{ env.ECS_CLUSTER }} \
      --service ${{ env.ECS_SERVICE }} \
      --force-new-deployment

이 방식은 현재 Task Definition의 최신 리비전을 사용하여 서비스를 재시작합니다. Task Definition에 latest 태그 대신 ECR 이미지를 사용하고, ECS가 새 태그의 이미지를 Pull하도록 합니다.

9.2 Task Definition을 업데이트하는 배포 (권장)

더 안전한 방식은 Task Definition에 정확한 이미지 태그를 기록하는 것입니다.

- name: Download current task definition
  run: |
    aws ecs describe-task-definition \
      --task-definition my-api \
      --query taskDefinition \
      > task-definition.json

- name: Update image in task definition
  id: task-def
  uses: aws-actions/amazon-ecs-render-task-definition@v1
  with:
    task-definition: task-definition.json
    container-name: api
    image: ${{ steps.ecr-login.outputs.registry }}/my-api:${{ github.sha }}

- name: Deploy to ECS
  uses: aws-actions/amazon-ecs-deploy-task-definition@v2
  with:
    task-definition: ${{ steps.task-def.outputs.task-definition }}
    service: ${{ env.ECS_SERVICE }}
    cluster: ${{ env.ECS_CLUSTER }}
    wait-for-service-stability: true

wait-for-service-stability: true를 설정하면 새 Task가 정상적으로 실행될 때까지 Step이 대기합니다. 배포 후 Health Check 실패로 Task가 반복 재시작되면 Step이 실패합니다.

9.3 두 방식 비교

항목 force-new-deployment Task Definition 업데이트
추적성 어떤 이미지가 배포되었는지 불명확 Task Definition 리비전에 이미지 태그 기록
롤백 이전 이미지 태그를 알아야 함 이전 Task Definition 리비전으로 롤백
안정성 확인 별도 확인 필요 wait-for-service-stability로 자동 확인
설정 복잡도 낮음 중간

운영 환경에서는 Task Definition 업데이트 방식을 권장합니다. 배포 이력이 Task Definition 리비전으로 남으므로 "언제, 어떤 이미지가 배포되었는지" 추적할 수 있습니다.

10. 비용 고려사항

10.1 GitHub Actions 비용

항목 비용
GitHub-hosted Runner (Linux) $0.006/분 (Free 플랜 2,000분 무료)
캐시 저장소 최대 10GB (무료)
빌드 시간 단축 효과 캐싱 적용 시 50~80% 절감 가능

일 10회 빌드, 빌드당 3분(캐싱 적용)이면 월 약 900분입니다. Free 플랜으로 충분합니다.

10.2 ECR 비용

항목 비용
이미지 저장 $0.10/GB/월
데이터 전송 (같은 리전) 무료
데이터 전송 (다른 리전) $0.01/GB~

이미지 크기가 150MB이고 최근 30개 태그를 보존한다면 약 4.5GB × $0.10 = $0.45/월입니다.

10.3 비용 최적화 방법

  • ECR Lifecycle Policy: 오래된 이미지를 자동 삭제합니다.
  • Multi-stage Build: 이미지 크기를 줄여 저장 비용과 전송 시간을 절감합니다.
  • 레이어 캐싱: 빌드 시간 = Runner 과금 시간입니다. 캐싱으로 빌드 시간을 줄이면 비용도 줄어듭니다.
{
  "rules": [
    {
      "rulePriority": 1,
      "description": "최근 10개 이미지만 보존",
      "selection": {
        "tagStatus": "tagged",
        "tagPrefixList": ["main"],
        "countType": "imageCountMoreThan",
        "countNumber": 10
      },
      "action": {
        "type": "expire"
      }
    },
    {
      "rulePriority": 2,
      "description": "태그 없는 이미지 1일 후 삭제",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 1
      },
      "action": {
        "type": "expire"
      }
    }
  ]
}

11. 자주 하는 실수와 해결

실수 결과 해결 방법
permissions 미선언으로 OIDC 실패 id-token: write 없으면 토큰 발급 불가 Job 레벨에서 permissions 명시
Buildx 미설정으로 캐싱 불가 docker/build-push-action은 BuildKit 필요 docker/setup-buildx-action 추가
PR에서 이미지 Push 시도 ECR에 불필요한 이미지 누적 push: ${{ github.event_name != 'pull_request' }}
latest 태그만 사용 롤백 불가, 배포 추적 불가 SHA 기반 태그 사용
ECR Lifecycle Policy 미설정 이미지 무한 누적 → 비용 증가 Lifecycle Policy로 보존 기간 설정
캐시 key 충돌 다른 브랜치의 캐시를 잘못 사용 docker/build-push-action의 GHA 캐시는 브랜치별 격리됨
wait-for-service-stability 미사용 배포 실패를 인지하지 못함 deploy Step에서 안정성 확인 추가

12. 운영 환경 체크리스트

파이프라인을 프로덕션에 적용하기 전에 확인할 항목:

보안 - [ ] OIDC 인증 사용 (장기 Access Key 제거) - [ ] IAM Role의 sub 조건으로 브랜치 제한 - [ ] permissions를 최소한으로 선언 - [ ] Action 버전 SHA 고정 또는 Dependabot 설정 - [ ] Dockerfile에서 non-root 사용자 실행

빌드 - [ ] Multi-stage Build로 이미지 크기 최소화 - [ ] 레이어 캐싱(cache-from/cache-to) 적용 - [ ] Git SHA 기반 이미지 태깅

배포 - [ ] Task Definition에 정확한 이미지 태그 기록 - [ ] wait-for-service-stability로 배포 안정성 확인 - [ ] environment: production으로 수동 승인 게이트 추가

비용/운영 - [ ] ECR Lifecycle Policy 설정 - [ ] 취약점 스캔(Trivy 또는 ECR Basic Scanning) 적용 - [ ] 배포 실패 시 알림 설정 (Slack, Teams 등)

13. 정리

  • GitHub Actions에서 Docker 이미지를 빌드하고 배포하는 파이프라인은 build-push-action + ECR + ECS 조합으로 구성합니다.
  • OIDC 인증으로 장기 자격 증명 없이 AWS에 접근하면 보안과 운영 편의성을 모두 확보할 수 있습니다.
  • 레이어 캐싱(type=gha)을 적용하면 빌드 시간과 CI 비용을 동시에 줄일 수 있습니다.
  • 이미지 태그는 Git SHA를 사용하여 추적성과 롤백 용이성을 확보합니다.
  • 취약점 스캔을 배포 전에 실행하면 알려진 CVE가 프로덕션에 유입되는 것을 차단할 수 있습니다.
  • 단순히 "돌아가는" 파이프라인이 아니라, 보안, 캐싱, 태깅, 스캔, 안정성 확인까지 갖춘 운영 수준의 워크플로우가 필요합니다.

관련 글

참고 문서

반응형