[V1] Frontend CD - 100-hours-a-week/KTB4-10th-Cloud GitHub Wiki

Frontend CD

main 브랜치에 Push되면 Build → S3 업로드 → CloudFront 캐시 무효화 → 배포 버전 검증 수행

Workflow

name: Frontend-CD

on:
  push:
    branches:
      - main
    paths-ignore:
      - "README.md"
      - "docs/**"

jobs:
  frontend-cd:
    runs-on: ubuntu-latest

    env:
      AWS_REGION: ap-northeast-2
      S3_BUCKET_NAME: kgb-frontend
      CLOUDFRONT_DISTRIBUTION_ID: E3Q7ETZYCFX2WY
      SERVICE_URL: https://kguidebook.site

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

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "22"

      - name: Install dependencies
        run: npm ci

      - name: Build
        env:
          VITE_API_BASE_URL: ""
          VITE_KAKAO_MAP_APP_KEY: ${{ secrets.VITE_KAKAO_MAP_APP_KEY }}
        run: npm run build

      # 현재 배포 Commit을 확인하기 위한 파일 생성
      - name: Create version.json
        run: |
          echo "{\"commitSha\":\"${GITHUB_SHA}\"}" > dist/version.json

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_S3_ACCESS_KEY }}
          aws-secret-access-key: ${{ secrets.AWS_S3_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      # 이전 정적 Asset을 별도로 보존하지 않음
      - name: Upload to S3
        run: |
          aws s3 sync dist/ \
            "s3://${S3_BUCKET_NAME}/" \
            --delete

      # 캐시 무효화
      - name: Invalidate CloudFront Cache
        id: invalidate
        run: |
          INVALIDATION_ID=$(aws cloudfront create-invalidation \
            --distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \
            --paths "/*" \
            --query 'Invalidation.Id' \
            --output text)

          echo "invalidation_id=${INVALIDATION_ID}" >> "$GITHUB_OUTPUT"

      # 캐시 무효화 완료 대기
      - name: Wait for CloudFront Invalidation
        run: |
          aws cloudfront wait invalidation-completed \
            --distribution-id "${CLOUDFRONT_DISTRIBUTION_ID}" \
            --id "${{ steps.invalidate.outputs.invalidation_id }}"

      # 실제 배포된 Commit 확인
      - name: Verify deployed version
        run: |
          RESPONSE=$(curl -fsS "${SERVICE_URL}/version.json")
          DEPLOYED_SHA=$(echo "$RESPONSE" | jq -r '.commitSha')

          if [ "${DEPLOYED_SHA}" != "${GITHUB_SHA}" ]; then
            echo "배포 실패: 제공 중인 버전이 배포 대상과 일치하지 않음"
            exit 1
          fi

          echo "배포 버전 확인 성공"

      # Service URL이 정상적으로 응답하는지 확인
      - name: 서비스 응답 확인
        run: |
          if curl -fsS "${SERVICE_URL}/" > /dev/null; then
            echo "서비스 응답 확인 성공"
          else
            echo "서비스 응답 확인 실패"
            exit 1
          fi

CloudFront의 캐시 무효화 API가 성공을 반환하더라도, 이는 "무효화 요청이 접수됐다"는 뜻이지 "사용자에게 새 버전이 실제로 서빙되고 있다"는 보장은 아님. 따라서 빌드 시점에 현재 커밋 SHA를 담은 version.json을 함께 배포하고, 배포 완료 후 이 파일을 직접 요청해 실제 서빙 중인 버전이 방금 배포한 커밋과 일치하는지 확인함. 일치하지 않으면 배포 실패로 간주하고 파이프라인을 중단함.


S3 Frontend Bucket 설정

Frontend 정적 파일을 저장하기 위한 S3 Bucket을 생성함. S3를 직접 외부에 공개하지 않고 CloudFront + OAC를 통해서만 조회 가능하도록 구성함.

Bucket 생성

Public Access 차단

S3 Bucket 자체를 인터넷에 공개하지 않음. 사용자는 S3로 직접 접근하지 않고 Cloudfront를 통해 OAC를 통해서 Private S3로 접근함.

따라서 모든 Public access를 차단함.

  • Block all public access = ON
    • Block public access to buckets and objects
    • granted through new ACLs = ON
    • Block public access to buckets and objects
    • granted through any ACLs = ON
    • Block public access through new
    • public bucket/access point policies = ON
    • Block public and cross-account access
    • through public bucket policies = ON

Amazon S3의 최신 사용 사례 대부분은 더 이상 ACL을 사용할 필요가 없습니다. 각 객체에 대해 액세스를 개별적으로 제어할 필요가 있는 상황을 제외하고는 ACL을 비활성화한 채로 두는 것이 좋습니다. ACL을 비활성화하면 누가 객체를 버킷에 업로드했는지에 관계없이 정책을 사용하여 버킷의 모든 객체에 대한 액세스를 제어할 수 있습니다. 자세한 내용은 객체 소유권 제어 및 버킷에 대해 ACL 사용 중지 섹션을 참조하세요.

객체별로 정책을 제한할 필요가 없으므로 Bucket Policy를 사용함.

Static Website Hosting

Static website hosting = Disable

S3 Website Endpoint를 직접 공개하는 구조가 아님. CloudFront의 Origin으로 일반 S3 Bucket Origin을 사용하고 OAC를 적용함.

CloudFront OAC 연결

CloudFront Distribution 생성 시 해당 S3 Bucket을 Origin으로 지정함.

Origin → S3 Bucket 선택 Origin access → Origin Access Control settings (recommended) OAC → 새 OAC 생성 또는 기존 OAC 선택

OAC란?

S3는 외부 접근을 막아두고 CloudFront만 S3 파일을 가져갈 수 있게 만드는 인증 방식. OAC는 CloudFront가 Private S3의 객체를 조회할 수 있도록 요청을 인증하는 역할을 함.

Bucket Policy

CloudFront Distribution 생성 및 OAC 연결 후 해당 Distribution이 S3 객체를 읽을 수 있도록 Bucket Policy를 적용함.

권한 구조는 다음과 같음.

CloudFront Console에서 OAC 설정 과정에 제시되는 Bucket Policy를 기준으로 적용함.

Frontend 배포 흐름

Frontend Build 결과물인 dist/만 Bucket에 업로드함. GitHub Actions에서는 IAM 권한으로 S3에 직접 업로드


프론트 배포용 IAM 권한

CI/CD에서 S3 업로드와 CloudFront 캐시 무효화를 수행할 전용 IAM 사용자를 생성함.

  • User name: kgb-frontend-deployer
  • Console access: 없음 (Access Key만 발급)

정책

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListFrontendBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::kgb-frontend"
    },
    {
      "Sid": "DeployFrontendObjects",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::kgb-frontend/*"
    },
    {
      "Sid": "CloudFrontInvalidation",
      "Effect": "Allow",
      "Action": [
        "cloudfront:CreateInvalidation",
        "cloudfront:GetInvalidation"
      ],
      "Resource": "arn:aws:cloudfront::742875367096:distribution/E3Q7ETZYCFX2WY"
    }
  ]
}

배포 자동화에 필요한 최소 권한만 부여함. S3 버킷 읽기(GetObject)는 포함하지 않으며, 이는 CloudFront의 OAC가 별도로 담당함.

  • ListFrontendBucket: aws s3 sync --delete 실행 시 기존 파일 목록을 조회하기 위한 권한. 버킷 자체(kgb-frontend)에 대한 권한이라 객체 경로(/*)가 붙지 않음ㄴㅋ
  • DeployFrontendObjects: 빌드 결과물을 업로드(PutObject)하고 이전 파일을 삭제(DeleteObject)하는 권한. 버킷 안의 객체 전체(kgb-frontend/*)가 대상
  • CloudFrontInvalidation: 배포 후 캐시를 무효화(CreateInvalidation)하고 처리 상태를 조회(GetInvalidation)하는 권한. 특정 Distribution으로 범위를 제한