Published on

GitHub Actions OIDC로 AWS 배포 실패 해결 가이드

Authors

서버리스든 ECS/EKS든, GitHub Actions에서 AWS로 배포할 때 OIDC를 붙이면 장기 액세스 키를 없앨 수 있어 보안과 운영이 좋아집니다. 하지만 실제로 적용해보면 배포 단계에서 AssumeRoleWithWebIdentity가 실패하거나, 로컬에서는 되는데 CI에서만 권한이 막히는 식의 문제가 자주 발생합니다.

이 글은 GitHub Actions OIDC 기반으로 AWS 배포가 실패할 때, 로그에 나타나는 에러 메시지를 단서로 원인을 빠르게 좁히고, IAM 신뢰 정책(trust policy)과 권한 정책(permission policy), 워크플로 설정을 “정답 형태”로 고정하는 것을 목표로 합니다.

배포 파이프라인이 간헐적으로만 깨진다면 캐시/환경 차이도 의심해야 합니다. 비슷한 결의 CI 디버깅 접근은 GitHub Actions 캐시 충돌로 CI 간헐 실패 디버깅도 참고해두면 좋습니다.

OIDC 흐름을 한 장으로 이해하기

GitHub Actions OIDC는 다음 순서로 동작합니다.

  1. 워크플로가 id-token: write 권한을 가진 상태에서 OIDC 토큰을 발급받음
  2. aws-actions/configure-aws-credentials가 그 토큰으로 AWS STS AssumeRoleWithWebIdentity 호출
  3. STS가 IAM Role의 신뢰 정책을 검사해 통과하면 임시 자격증명 발급
  4. 이후 AWS CLI/SDK는 임시 자격증명으로 ECR, S3, CloudFormation, ECS, EKS 등을 호출

즉 실패 지점은 크게 두 군데입니다.

  • STS AssumeRole 단계에서 실패: 신뢰 정책, 클레임 조건, OIDC Provider 설정 문제
  • AssumeRole은 성공했지만 배포 액션에서 실패: 권한 정책(permissions) 문제

가장 흔한 실패 1: Not authorized to perform sts:AssumeRoleWithWebIdentity

증상

로그에 대개 아래처럼 나옵니다.

  • AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity
  • No OpenIDConnect provider found in your account for https://token.actions.githubusercontent.com

원인 A: OIDC Provider 미생성 또는 URL/Thumbprint 불일치

AWS 계정에 GitHub OIDC Provider가 없거나, Provider URL이 정확히 https://token.actions.githubusercontent.com가 아니면 STS가 바로 거절합니다.

CLI로 확인합니다.

aws iam list-open-id-connect-providers

Provider ARN을 찾았으면 세부 정보를 확인합니다.

aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com

Url이 정확해야 하고, ClientIDList에 최소 sts.amazonaws.com이 포함돼야 합니다.

원인 B: GitHub Actions에서 OIDC 토큰 발급 권한 누락

워크플로에 permissions를 명시하지 않으면, 조직/레포 설정에 따라 id-token이 기본적으로 막혀 있을 수 있습니다.

다음이 최소 구성입니다.

name: deploy
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: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: ap-northeast-2
      - run: aws sts get-caller-identity

여기서 aws sts get-caller-identity가 성공하면 “신뢰 정책 + OIDC 토큰 발급”은 통과한 것입니다.

가장 흔한 실패 2: The security token included in the request is invalid

증상

  • The security token included in the request is invalid
  • InvalidIdentityToken: No matching audience

원인: aud 클레임 조건 불일치

GitHub OIDC 토큰의 aud는 기본적으로 sts.amazonaws.com을 쓰는 것이 일반적입니다. IAM Role 신뢰 정책에서 token.actions.githubusercontent.com:aud 조건을 걸어놨는데 값이 다르면 실패합니다.

신뢰 정책 예시는 아래처럼 구성합니다.

{
  "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:OWNER/REPO:*"
        }
      }
    }
  ]
}

만약 configure-aws-credentials에서 audience를 별도로 설정했다면, 위 aud 조건도 동일하게 맞춰야 합니다.

- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
    aws-region: ap-northeast-2
    audience: sts.amazonaws.com

가장 흔한 실패 3: sub 조건 불일치 (브랜치/환경 제한)

증상

  • AccessDenied인데 OIDC Provider도 있고 aud도 맞는 것 같은 상황
  • 특정 브랜치에서만 실패하거나, PR에서는 실패하고 main push에서는 성공

원인: token.actions.githubusercontent.com:sub 매칭이 너무 빡빡함

GitHub OIDC의 sub는 실행 컨텍스트에 따라 달라집니다.

  • push: repo:OWNER/REPO:ref:refs/heads/main
  • tag: repo:OWNER/REPO:ref:refs/tags/v1.2.3
  • environment 사용 시: repo:OWNER/REPO:environment:prod
  • pull_request는 또 다른 형태가 될 수 있음

따라서 신뢰 정책에서 sub를 “정확히 main 브랜치만”으로 잠그고 싶다면 아래처럼 명시합니다.

"StringEquals": {
  "token.actions.githubusercontent.com:sub": "repo:OWNER/REPO:ref:refs/heads/main",
  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}

반대로, 브랜치/태그/환경 등 여러 케이스를 허용해야 한다면 StringLike로 완화합니다.

"StringLike": {
  "token.actions.githubusercontent.com:sub": [
    "repo:OWNER/REPO:ref:refs/heads/*",
    "repo:OWNER/REPO:ref:refs/tags/*"
  ]
}

환경 보호 규칙을 쓰는 팀이라면 environment:prod 형태를 신뢰 정책에 포함시키는 것을 자주 놓칩니다.

"StringLike": {
  "token.actions.githubusercontent.com:sub": "repo:OWNER/REPO:environment:prod"
}

가장 흔한 실패 4: AssumeRole은 성공했는데 ECR/S3/CFN에서 AccessDenied

증상

  • aws sts get-caller-identity는 성공
  • 이후 단계에서 다음과 같은 오류
    • ECR: ecr:GetAuthorizationToken 또는 ecr:PutImage 거부
    • S3: s3:PutObject 거부
    • CloudFormation: cloudformation:CreateChangeSet 거부
    • ECS: ecs:UpdateService 거부

원인: Role 권한 정책이 배포 작업을 커버하지 못함

OIDC는 “누가 Role을 맡을 수 있는가”만 해결합니다. 실제 배포 권한은 Role에 붙은 permission policy로 해결해야 합니다.

예를 들어 ECR에 이미지를 푸시하려면 최소 아래 권한이 필요합니다.

{
  "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"
    }
  ]
}

S3 업로드까지 포함한다면 버킷 ARN과 오브젝트 ARN을 구분해 넣습니다.

{
  "Effect": "Allow",
  "Action": ["s3:ListBucket"],
  "Resource": "arn:aws:s3:::my-deploy-bucket"
}
{
  "Effect": "Allow",
  "Action": ["s3:PutObject", "s3:GetObject"],
  "Resource": "arn:aws:s3:::my-deploy-bucket/*"
}

권한 범위를 빠르게 찾는 방법은 CloudTrail에서 이벤트를 보고, 거부된 API와 리소스 ARN을 그대로 정책에 반영하는 것입니다. 특히 EKS/ECS 배포는 호출 API가 생각보다 많아 “최소 권한”을 한 번에 맞추기 어렵습니다.

EKS 쪽에서 시크릿/외부 연동까지 얽혀 있다면 배포 실패가 OIDC가 아니라 클러스터 리소스 문제일 수도 있습니다. 그 경우 EKS에서 ExternalSecret이 0개만 생성될 때처럼 레이어를 나눠 점검하는 게 좋습니다.

디버깅 체크리스트: 5분 안에 원인 좁히기

1) 워크플로에서 OIDC 토큰 권한 확인

  • permissionsid-token: write가 있는지
  • 조직 정책에서 GitHub Actions 권한이 제한되어 있지 않은지

2) STS 호출이 되는지 최소 재현

아래 한 줄이 성공하면 “OIDC 연결 자체”는 된 것입니다.

aws sts get-caller-identity

실패한다면 신뢰 정책 또는 OIDC Provider 문제입니다.

3) 신뢰 정책의 sub를 실행 컨텍스트에 맞게

  • main push만 허용할지
  • tag 배포도 허용할지
  • GitHub Environments를 쓰는지

이 3가지에 따라 sub 패턴이 달라집니다.

4) AssumeRole 이후 권한은 CloudTrail로 역추적

  • AccessDenied가 난 API 액션 이름을 확인
  • 리소스 ARN이 *여야 하는 액션인지(예: ecr:GetAuthorizationToken)
  • 리전/계정/리포지토리 ARN이 맞는지

5) 배포가 간헐적으로만 실패한다면 환경 재현

OIDC 자체는 대체로 결정론적으로 실패하지만, 배포 단계는 캐시/레이스/동시성 이슈로 흔들릴 수 있습니다. 특히 같은 워크플로에서 여러 잡이 동시에 ECR 태그를 밀거나, 캐시가 꼬이면 “권한 문제처럼 보이는” 실패가 납니다. 이때는 GitHub Actions 캐시 충돌로 CI 간헐 실패 디버깅의 접근처럼 로그/키/동시성부터 정리하세요.

실전 예제: 안전한 OIDC 신뢰 정책 + 배포 워크플로

IAM Role 신뢰 정책(예: main 브랜치만 허용)

{
  "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",
          "token.actions.githubusercontent.com:sub": "repo:OWNER/REPO:ref:refs/heads/main"
        }
      }
    }
  ]
}

GitHub Actions 워크플로(ECR 로그인 + 푸시 예시)

name: build-and-push
on:
  push:
    branches: [ main ]

permissions:
  id-token: write
  contents: read

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

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: ap-northeast-2

      - name: Verify identity
        run: aws sts get-caller-identity

      - name: Login to ECR
        run: |
          aws ecr get-login-password --region ap-northeast-2 \
            | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-2.amazonaws.com

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

이 구성이 통과하면, 이후 ECS 업데이트나 EKS 배포는 “추가 권한”만 확장하면 됩니다. 즉 OIDC 문제를 먼저 완전히 고정하고, 그 다음 권한 정책을 서비스별로 쪼개는 순서가 가장 빠릅니다.

마무리: OIDC 실패는 대부분 trust policy에서 끝난다

GitHub Actions OIDC로 AWS 배포가 실패할 때 핵심은 다음 두 문장으로 요약됩니다.

  • STS AssumeRole이 실패하면, 거의 항상 OIDC Provider 또는 Role 신뢰 정책의 aud/sub 조건 문제다.
  • AssumeRole이 성공하면, 그 다음은 순수하게 IAM 권한 정책의 범위 문제다.

먼저 aws sts get-caller-identity를 파이프라인에 넣어 경계를 명확히 하고, 신뢰 정책의 sub를 실행 컨텍스트(브랜치, 태그, environment)에 정확히 맞춘 뒤, CloudTrail로 실제 거부된 액션을 기반으로 권한을 보강하면 “될 때도 있고 안 될 때도 있는” 상태에서 빠르게 벗어날 수 있습니다.