Published on

GitHub Actions OIDC로 AWS 키 없이 배포하기

Authors

서로 다른 리포지토리/환경에서 배포 파이프라인을 운영하다 보면 결국 한 번은 이런 요구가 나옵니다.

  • GitHub Actions에 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY를 저장하지 말자
  • 키 로테이션/유출 대응 비용을 줄이자
  • PR/브랜치/환경별로 “누가 무엇을 배포할 수 있는지”를 더 강하게 통제하자

이때 가장 깔끔한 해법이 GitHub Actions OIDC(OpenID Connect) + AWS STS AssumeRoleWithWebIdentity 조합입니다. 요약하면, GitHub가 워크플로 실행 시 OIDC 토큰(JWT)을 발급하고, AWS는 그 토큰을 검증한 뒤 짧은 만료 시간의 임시 자격증명을 내려줍니다. 결과적으로 장기 키(Access Key) 없이 배포가 가능합니다.

아래에서는 OIDC의 동작 원리부터 AWS/GitHub 설정, Terraform 예시, 실제 배포 워크플로, 그리고 운영 중 자주 만나는 에러 포인트까지 한 번에 정리합니다.

왜 OIDC인가: 장기 키의 운영 리스크 제거

기존 방식(장기 키 기반)은 다음 문제가 반복됩니다.

  • GitHub Secrets에 키를 저장해야 함(유출/오남용 리스크)
  • 키 로테이션이 번거로움(사람/프로세스 의존)
  • 키 권한이 과대해지기 쉬움(“일단 되게 하자”)

OIDC 방식은 다음 장점이 있습니다.

  • 키 저장 불필요: GitHub Secrets에 AWS 키를 넣지 않음
  • 임시 자격증명: STS 토큰은 일반적으로 15~60분 만료
  • 조건 기반 신뢰 정책: 특정 리포지토리/브랜치/환경만 AssumeRole 허용
  • 감사 추적 용이: CloudTrail에 AssumeRoleWithWebIdentity 기록이 남음

전체 아키텍처 한 장 요약

  1. GitHub Actions 워크플로가 실행됨
  2. 워크플로가 GitHub OIDC Provider에서 ID Token(JWT) 요청
  3. aws-actions/configure-aws-credentials가 STS에 AssumeRoleWithWebIdentity 호출
  4. AWS가 토큰의 iss/aud/sub 등 클레임을 검증
  5. 검증 성공 시 임시 자격증명(AccessKey/SecretKey/SessionToken)을 워크플로 런타임에만 제공
  6. 이후 AWS CLI/SDK로 ECR/ECS/EKS/S3/CloudFront 등 배포 수행

1) AWS: GitHub OIDC Identity Provider 생성

AWS IAM에 GitHub OIDC Provider를 등록합니다.

  • Provider URL: https://token.actions.githubusercontent.com
  • Audience(aud): 보통 sts.amazonaws.com

Terraform 예시:

resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = [
    "sts.amazonaws.com"
  ]

  # GitHub OIDC의 TLS 인증서 지문(thumbprint)은 AWS 문서/검증 방식에 따라 달라질 수 있습니다.
  # 최신 Terraform/AWS Provider에서는 thumbprint를 요구하거나 자동 갱신을 지원하는 경우가 있으니
  # 사용하는 provider 버전에 맞춰 확인하세요.
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

> thumbprint는 환경/시점에 따라 변경될 수 있으니, 운영에서는 AWS 공식 가이드에 따라 최신 값을 확인하는 편이 안전합니다.

2) AWS: 배포용 IAM Role 만들기(신뢰 정책이 핵심)

OIDC에서 가장 중요한 건 Role의 Trust Policy(신뢰 정책) 입니다. 여기서 “어떤 워크플로가 이 Role을 Assume할 수 있는지”를 결정합니다.

다음 예시는 my-org/my-repo 리포지토리의 main 브랜치에서만 AssumeRole을 허용합니다.

{
  "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-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

sub 조건을 어떻게 잡을까?

sub는 GitHub가 토큰에 넣는 “주체(subject)”로, 보통 아래 패턴을 씁니다.

  • 브랜치 제한: repo:ORG/REPO:ref:refs/heads/main
  • 태그 제한: repo:ORG/REPO:ref:refs/tags/v*
  • 환경(Environment) 제한: repo:ORG/REPO:environment:prod

운영에서는 브랜치 + 환경까지 묶어 두면 사고 확률이 크게 줄어듭니다(예: prod 배포는 GitHub Environment 승인 필요).

3) AWS: 최소 권한 정책 붙이기(예: ECR push + ECS deploy)

Role에 붙일 IAM Policy는 “배포에 필요한 만큼만” 주는 게 정석입니다.

예를 들어 ECR에 이미지를 push하고 ECS 서비스를 업데이트한다면 대략 이런 권한이 필요합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:CompleteLayerUpload",
        "ecr:InitiateLayerUpload",
        "ecr:PutImage",
        "ecr:UploadLayerPart"
      ],
      "Resource": "arn:aws:ecr:ap-northeast-2:123456789012:repository/my-app"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecs:RegisterTaskDefinition",
        "ecs:UpdateService",
        "ecs:DescribeServices",
        "iam:PassRole"
      ],
      "Resource": "*"
    }
  ]
}
  • ecr:GetAuthorizationToken은 Resource가 *여야 하는 경우가 많습니다.
  • iam:PassRole은 ECS Task Execution Role/Task Role을 넘겨야 해서 필요합니다. 대신 PassRole 대상 Role ARN을 조건으로 제한하는 게 좋습니다.

4) GitHub Actions: OIDC 권한 선언 및 AWS 자격증명 구성

워크플로에서 반드시 아래 권한이 필요합니다.

  • permissions: id-token: write (OIDC 토큰 발급)
  • permissions: contents: read (checkout)

그리고 aws-actions/configure-aws-credentials 액션으로 Role을 Assume합니다.

예시: ECR 빌드/푸시 + ECS 배포

name: deploy

on:
  push:
    branches: ["main"]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest

    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-deploy-role
          aws-region: ap-northeast-2

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

      - name: Build and push
        env:
          ECR_REGISTRY: 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com
          ECR_REPO: my-app
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t "$ECR_REGISTRY/$ECR_REPO:$IMAGE_TAG" .
          docker push "$ECR_REGISTRY/$ECR_REPO:$IMAGE_TAG"

      - name: Deploy to ECS
        env:
          CLUSTER: my-cluster
          SERVICE: my-service
        run: |
          aws ecs update-service \
            --cluster "$CLUSTER" \
            --service "$SERVICE" \
            --force-new-deployment

이 구성의 포인트는 다음입니다.

  • AWS 키를 Secrets에 저장하지 않습니다.
  • Role 신뢰 정책이 허용한 워크플로만 배포 가능합니다.
  • 자격증명은 job 런타임에만 존재하고 만료됩니다.

5) (선택) GitHub Environment로 prod 배포를 더 강하게 잠그기

main 브랜치 push만으로 prod가 배포되면 위험합니다. GitHub Environment를 만들고 승인자(Reviewers) 를 지정하면, 워크플로가 environment: prod를 사용할 때 승인 없이는 진행되지 않습니다.

워크플로 예시:

jobs:
  deploy:
    environment: prod
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      # ...

그리고 AWS Role Trust Policy에서 sub를 환경 기반으로 제한합니다.

"StringLike": {
  "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:environment:prod"
}

이렇게 하면 승인된 prod 환경 배포만 Role Assume이 가능해집니다.

6) 자주 터지는 문제와 빠른 진단 포인트

6.1 InvalidIdentityToken / Not authorized to perform sts:AssumeRoleWithWebIdentity

대부분 Trust Policy의 조건이 실제 토큰 클레임과 불일치해서 발생합니다.

  • audsts.amazonaws.com인지 확인
  • sub 패턴이 정확한지 확인(브랜치/태그/환경)
  • OIDC Provider ARN이 맞는지 확인

이 케이스는 원인별로 체크리스트가 길어지기 쉬워서, 아래 글에 정리된 진단 흐름을 같이 보면 시간을 많이 줄일 수 있습니다.

6.2 EKS까지 배포하는데 Pod가 IMDS 401을 뱉는다?

OIDC 자체는 GitHub→AWS 인증이고, EKS Pod 내부에서 AWS API를 호출할 때는 IRSA(IAM Roles for Service Accounts)로 이어집니다. 배포는 성공했는데 런타임에서 IMDS 401이 보이면, 노드/파드의 메타데이터 접근 정책이나 HopLimit, IRSA 설정을 의심해야 합니다.

6.3 ECR Pull/Push 권한 문제(403, 토큰 만료)

OIDC로 AssumeRole은 됐는데 ECR에서 403이 나면, 거의 항상 ECR 권한 범위 또는 로그인 토큰 재발급 흐름 문제입니다.

7) 운영 팁: 보안/감사/유지보수 관점에서의 권장사항

  • Role 분리: dev/stage/prod를 같은 Role로 쓰지 말고 Role을 분리하세요.
  • Trust Policy를 좁게: subrepo:*처럼 넓게 열지 말고, 브랜치/환경/태그로 구체화하세요.
  • 권한 최소화: ECR/ECS/EKS/S3 등 필요한 액션만, 가능한 리소스 ARN까지 제한하세요.
  • 세션 이름/태그 활용: CloudTrail에서 누가 어떤 배포를 했는지 추적하기 위해 세션 태그(예: repo, workflow, actor)를 남기는 구성을 고려하세요.
  • 만료 시간 관리: 장시간 배포가 필요하면 STS 세션 시간을 늘릴 수 있지만, 보안상 꼭 필요한 만큼만 늘리세요.

마무리

GitHub Actions OIDC는 “키 없이 배포”라는 목표를 가장 현실적으로 달성하는 표준 패턴입니다. 핵심은 단순히 OIDC를 붙이는 게 아니라, Trust Policy의 조건(sub/aud)으로 배포 주체를 정확히 제한하고, Role 권한을 최소화하는 것입니다.

이미 AWS 키 기반으로 돌아가는 파이프라인이 있다면, 먼저 configure-aws-credentials@v4를 OIDC로 바꾸고(Secrets 제거), 다음 단계로 환경별 Role 분리와 정책 최소화를 적용하는 순서로 마이그레이션하면 리스크 없이 전환할 수 있습니다.