본문으로 건너뛰기
샐링잇 Sailing-it

GitHub Actions로 S3·CloudFront 배포 자동화하기

main 브랜치에 push하면 S3 동기화와 CloudFront 캐시 무효화까지 자동으로 처리되는 배포 파이프라인을 OIDC 인증 기반으로 단계별 정리했습니다. 실제로 겪은 에러 2가지와 해결 과정도 함께 다룹니다.

약 8분 분량
GitHub Actions로 S3·CloudFront 배포 자동화하기

정적 사이트를 S3에 올리고 CloudFront·Route 53까지 연결해 두신 개발자 분이라면, 다음 단계는 대부분 같습니다. 코드를 고칠 때마다 콘솔에 들어가 파일을 다시 올리고, 캐시를 수동으로 무효화하는 작업이 반복됩니다.

저희 샐링잇도 랜딩페이지·홍보 사이트 프로젝트마다 이 반복 작업을 자동화해 둡니다. GitHub Actions에서 main 브랜치로 push가 들어오면 S3 동기화와 CloudFront 캐시 무효화까지 한 번에 처리하는 구조입니다. 오늘은 그 파이프라인을 IAM 액세스 키 없이, OIDC 인증만으로 구성하는 방법을 정리합니다.

왜 액세스 키 대신 OIDC인가

가장 빠른 방법은 IAM 사용자를 만들고 액세스 키를 GitHub Secrets에 넣는 것입니다. 다만 이 방식은 키가 유출되면 만료 전까지 계속 유효하다는 위험이 남습니다.

OIDC 방식은 GitHub Actions가 실행될 때마다 AWS에 임시 자격 증명을 요청합니다. 리포지토리에 저장되는 값은 역할(Role) ARN뿐이고, 실제 키는 어디에도 저장되지 않습니다. 설정 단계가 하나 더 늘어나는 대신, 장기 키 관리 부담이 사라집니다.

사전 준비

S3 정적 웹사이트 호스팅과 CloudFront 배포가 이미 연결되어 있다고 가정합니다. 아래 값만 미리 확인해 두시면 됩니다.

  • S3 버킷 이름
  • CloudFront 배포 ID
  • AWS 계정 ID
  • GitHub 리포지토리 이름 ({organization}/{repo} 형식)

1단계 — IAM OIDC 자격 증명 공급자 만들기

IAM 콘솔의 자격 증명 공급자(Identity providers) 메뉴에서 공급자를 하나 추가합니다.

  • 공급자 유형: OpenID Connect
  • 공급자 URL: https://token.actions.githubusercontent.com
  • 대상(Audience): sts.amazonaws.com

한 AWS 계정에서 여러 리포지토리가 GitHub Actions를 쓴다면, 이 공급자는 계정당 한 번만 만들면 충분합니다.

2단계 — IAM 역할과 신뢰 정책

새 역할을 만들고, 신뢰 정책을 해당 리포지토리·브랜치로 제한합니다. 이 조건이 없으면 같은 AWS 계정의 다른 리포지토리도 이 역할을 가져다 쓸 수 있습니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::{ACCOUNT_ID}: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:{ORG}/{REPO}:ref:refs/heads/main"
        }
      }
    }
  ]
}

sub 조건의 ref:refs/heads/main 부분이 핵심입니다. 브랜치를 명시하지 않으면 이 리포지토리의 어떤 브랜치, 어떤 PR에서 실행되는 워크플로든 같은 역할을 가져다 쓸 수 있습니다.

3단계 — 최소 권한 정책 부여

역할에는 배포에 필요한 권한만 인라인 정책으로 붙입니다. 해당 버킷과 해당 배포로만 리소스를 한정합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::{BUCKET_NAME}",
        "arn:aws:s3:::{BUCKET_NAME}/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "cloudfront:CreateInvalidation",
      "Resource": "arn:aws:cloudfront::{ACCOUNT_ID}:distribution/{DISTRIBUTION_ID}"
    }
  ]
}

s3:ListBucket을 빼놓기 쉬운데, aws s3 sync 명령이 내부적으로 버킷 목록 조회부터 시작하기 때문에 이 권한이 없으면 동기화 자체가 실패합니다.

4단계 — GitHub Secrets 등록

리포지토리의 Settings → Secrets and variables → Actions에서 시크릿을 하나 추가합니다.

  • 이름: AWS_DEPLOY_ROLE_ARN
  • 값: 2단계에서 만든 역할의 ARN

키가 아니라 역할 ARN만 저장한다는 점이 액세스 키 방식과 가장 큰 차이입니다.

5단계 — 워크플로 작성

.github/workflows/deploy.yml 파일을 아래처럼 구성합니다. permissions.id-token: write가 없으면 OIDC 토큰 자체가 발급되지 않으니 빠뜨리지 않아야 합니다.

name: Deploy to S3 and invalidate CloudFront

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

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

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
          aws-region: ap-northeast-2

      - run: |
          aws s3 sync . s3://{BUCKET_NAME} --delete \
            --exclude ".git/*" --exclude ".github/*"

      - run: |
          aws cloudfront create-invalidation \
            --distribution-id {DISTRIBUTION_ID} --paths "/*"

실전에서 만난 에러 2가지

저희도 이 파이프라인을 처음 구성할 때 두 가지 에러를 그대로 마주쳤습니다.

AWS 콘솔이 계속 로딩만 도는 경우

신뢰 정책 편집 화면이 멈추거나 “Oops, something went wrong”이 뜨는 건 콘솔 자체의 일시적인 버그일 때가 많습니다. 새로고침이나 시크릿 창으로도 안 풀리면, 콘솔 우측 상단의 CloudShell을 열어 CLI로 우회하는 편이 빠릅니다.

aws iam update-assume-role-policy \
  --role-name {ROLE_NAME} \
  --policy-document file://trust-policy.json

AccessDenied: s3:ListBucket

역할을 가져오는(assume) 데는 성공했는데 동기화 단계에서 AccessDenied가 난다면, 신뢰 정책이 아니라 3단계의 권한 정책이 아직 역할에 붙지 않았다는 신호입니다. 인증(누가 역할을 쓸 수 있는가)과 권한(그 역할로 무엇을 할 수 있는가)은 서로 다른 정책이라, 하나만 설정하고 넘어가면 이런 에러가 납니다.

마지막으로

정리하면 순서는 OIDC 공급자 생성, 신뢰 정책으로 리포지토리 제한, 최소 권한 정책 부여, 시크릿 등록, 워크플로 작성 다섯 단계입니다. 한 번 구성해 두면 이후로는 push 한 번으로 배포가 끝납니다.

저희 샐링잇에서도 프로젝트를 런칭하고 나면 이런 배포 자동화부터 손봅니다. 런칭은 끝이 아니라 운영의 시작이기 때문입니다.

자주 묻는 질문

Q. IAM 액세스 키 방식이 더 간단한데, 꼭 OIDC로 해야 하나요?
개인 프로젝트나 임시 테스트라면 액세스 키도 무방합니다. 다만 여러 명이 함께 쓰는 리포지토리이거나 실서비스 배포라면, 키 유출 시 만료 시점까지 위험이 남는 액세스 키보다 매 실행마다 임시 자격 증명을 발급받는 OIDC가 안전합니다.
Q. OIDC 공급자를 이미 다른 프로젝트에서 만들었다면 또 만들어야 하나요?
아닙니다. OIDC 공급자는 AWS 계정 단위로 한 번만 만들면 됩니다. 이후 새 리포지토리를 배포에 연결할 때는 2단계(역할 생성)부터 반복하시면 됩니다.
Q. CloudFront 무효화(invalidation)는 매번 전체 경로로 해도 되나요?
경로 수가 적은 정적 사이트라면 `/*`로 전체 무효화해도 큰 무리는 없습니다. 다만 무효화 요청은 월 1,000건까지 무료이고 이후 건당 과금되므로, 배포 빈도가 매우 높은 프로젝트라면 변경된 경로만 지정하는 편이 비용 관리에 유리합니다.