GitHub ActionsAWSOIDCCI/CDSecurityIAMTerraformSAM

OIDC 인증으로 GitHub Actions에서 AWS에 배포하기

Sloth255
Sloth255
·1 min read·140 words

소개

GitHub Actions에서 AWS 리소스에 배포할 때, 기존 방법은 IAM 사용자 액세스 키를 GitHub Secrets에 저장하는 것이었습니다. 그러나 이 방법에는 몇 가지 과제가 있습니다:

  • 액세스 키 유출 위험
  • 정기적인 교체 작업 필요
  • 관리해야 할 시크릿 증가

OIDC(OpenID Connect) 인증은 이러한 과제를 해결합니다. 이 글에서는 GitHub Actions와 AWS 간 OIDC 연동 설정 방법을 자세히 설명합니다.

OIDC란?

OIDC는 OAuth 2.0을 기반으로 한 인증 프로토콜입니다. GitHub Actions가 "이 저장소의 워크플로우입니다"라고 증명하는 토큰을 AWS에 발행하면, AWS가 이를 검증하고 임시 자격 증명을 제공합니다.

OIDC의 장점

  • 시크릿 불필요: GitHub에 액세스 키를 저장할 필요 없음
  • 보안 강화: 임시 자격 증명만 사용
  • 세밀한 권한 제어: 저장소나 브랜치별로 권한 제한 가능
  • 관리 비용 감소: 키 교체 작업 불필요

AWS 설정

1. IAM Identity Provider 생성

먼저 AWS Management Console에서 IAM Identity Provider를 생성합니다.

  1. IAM 콘솔 열기
  2. "Identity providers" → "Add provider" 클릭
  3. 다음 정보 입력:
Provider type: OpenID Connect
Provider URL: https://token.actions.githubusercontent.com
Audience: sts.amazonaws.com

Terraform 예시

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

  client_id_list = [
    "sts.amazonaws.com",
  ]

  thumbprint_list = [
    "6938fd4d98bab03faadb97b34396831e3780aea1"
  ]
}

AWS SAM 예시

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: GitHub Actions OIDC Provider

Resources:
  GitHubOIDCProvider:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: https://token.actions.githubusercontent.com
      ClientIdList:
        - sts.amazonaws.com
      ThumbprintList:
        - 6938fd4d98bab03faadb97b34396831e3780aea1

2. IAM 역할 생성

다음으로 GitHub Actions가 수임할 IAM 역할을 생성합니다.

신뢰 정책 설정

{
  "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:your-org/your-repo:*"
        }
      }
    }
  ]
}

조건 세부사항

token.actions.githubusercontent.com:sub 값으로 접근을 제한할 수 있습니다.

# 특정 저장소 전체
repo:your-org/your-repo:*

# 특정 브랜치만
repo:your-org/your-repo:ref:refs/heads/main

# 특정 환경만
repo:your-org/your-repo:environment:production

# 풀 리퀘스트만
repo:your-org/your-repo:pull_request

Terraform 예시

data "aws_iam_policy_document" "github_actions_assume_role" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github_actions.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values   = ["repo:your-org/your-repo:*"]
    }
  }
}

resource "aws_iam_role" "github_actions" {
  name               = "github-actions-deploy-role"
  assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json
}

# 필요한 권한 정책 연결
resource "aws_iam_role_policy_attachment" "deploy_policy" {
  role       = aws_iam_role.github_actions.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonECS_FullAccess"
}

AWS SAM 예시

template.yaml
Resources:
  GitHubActionsRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: github-actions-deploy-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Federated: !GetAtt GitHubOIDCProvider.Arn
            Action: sts:AssumeRoleWithWebIdentity
            Condition:
              StringEquals:
                token.actions.githubusercontent.com:aud: sts.amazonaws.com
              StringLike:
                token.actions.githubusercontent.com:sub: repo:your-org/your-repo:*
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonECS_FullAccess

Outputs:
  GitHubActionsRoleArn:
    Description: ARN of the GitHub Actions IAM Role
    Value: !GetAtt GitHubActionsRole.Arn
    Export:
      Name: GitHubActionsRoleArn

SAM으로 배포하려면:

sam build
sam deploy --guided

초기 배포 후 출력된 역할 ARN을 GitHub Secrets에 AWS_ROLE_ARN으로 등록합니다.

3. 권한 정책 연결

역할에 배포에 필요한 권한을 연결합니다. 예를 들어:

  • S3 배포: AmazonS3FullAccess
  • ECS 배포: AmazonECS_FullAccess
  • Lambda: AWSLambda_FullAccess

프로덕션 환경에서는 최소 권한 원칙에 따라 커스텀 정책을 만들 것을 권장합니다.

GitHub Actions 설정

워크플로우 파일 생성

.github/workflows/deploy.yml을 생성합니다.

name: Deploy to AWS

on:
  push:
    branches:
      - main

# OIDC 토큰 취득을 위한 권한 설정 (중요!)
permissions:
  id-token: write
  contents: read

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

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

      - name: Deploy to S3
        run: |
          aws s3 sync ./dist s3://your-bucket-name --delete

      - name: Verify deployment
        run: |
          aws s3 ls s3://your-bucket-name

중요 포인트

1. permissions 설정

permissions:
  id-token: write  # OIDC 토큰 취득에 필요
  contents: read   # 저장소 체크아웃에 필요

이 설정이 없으면 OIDC 토큰을 취득할 수 없어 오류가 발생합니다.

2. configure-aws-credentials 액션 버전

v4 이상을 사용합니다. 이전 버전은 OIDC를 지원하지 않을 수 있습니다.

uses: aws-actions/configure-aws-credentials@v4

실용 예시: SAM 애플리케이션 배포

AWS SAM을 사용한 서버리스 애플리케이션 배포 예시입니다.

name: Deploy SAM Application

on:
  push:
    branches:
      - main

permissions:
  id-token: write
  contents: read

env:
  AWS_REGION: ap-northeast-1
  SAM_STACK_NAME: my-sam-app

jobs:
  deploy:
    runs-on: ubuntu-latest

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

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Setup SAM CLI
        uses: aws-actions/setup-sam@v2
        with:
          use-installer: true

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: SAM Build
        run: sam build --use-container

      - name: SAM Deploy
        run: |
          sam deploy \
            --stack-name ${{ env.SAM_STACK_NAME }} \
            --capabilities CAPABILITY_IAM \
            --resolve-s3 \
            --no-fail-on-empty-changeset \
            --no-confirm-changeset

      - name: Get Stack Outputs
        run: |
          aws cloudformation describe-stacks \
            --stack-name ${{ env.SAM_STACK_NAME }} \
            --query 'Stacks[0].Outputs' \
            --output table

실용 예시: ECS 배포

더 실용적인 예시로 ECS 배포 워크플로우입니다.

name: Deploy to ECS

on:
  push:
    branches:
      - main

permissions:
  id-token: write
  contents: read

env:
  AWS_REGION: ap-northeast-1
  ECR_REPOSITORY: my-app
  ECS_SERVICE: my-service
  ECS_CLUSTER: my-cluster
  CONTAINER_NAME: my-container

jobs:
  deploy:
    runs-on: ubuntu-latest

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

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

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

      - name: Build and push image to ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

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

      - name: Update task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}

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

트러블슈팅

오류: "Not authorized to perform sts:AssumeRoleWithWebIdentity"

원인: 신뢰 정책 조건이 일치하지 않음

해결책:

  1. IAM 역할의 신뢰 정책에서 token.actions.githubusercontent.com:sub 값 확인
  2. 저장소 이름과 브랜치 이름이 올바른지 확인
  3. GitHub Actions 로그에서 실제 sub 클레임 값 확인
- name: Debug OIDC token
  run: |
    curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
      "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | \
      jq -R 'split(".") | .[1] | @base64d | fromjson'

오류: "Error: Credentials could not be loaded"

원인: permissions 설정이 누락됨

해결책: 워크플로우 파일에 다음을 추가

permissions:
  id-token: write
  contents: read

인증은 성공했지만 권한 오류 발생

원인: IAM 역할에 필요한 권한 정책이 연결되어 있지 않음

해결책: IAM 역할에 적절한 정책 연결

aws iam attach-role-policy \
  --role-name github-actions-deploy-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess

보안 모범 사례

1. 최소 권한 원칙

최소한의 필요한 권한만 부여합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::your-specific-bucket",
        "arn:aws:s3:::your-specific-bucket/*"
      ]
    }
  ]
}

2. 브랜치 또는 태그로 제한

프로덕션 배포를 특정 브랜치에만 제한합니다.

{
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
    }
  }
}

3. 환경별 역할 분리

개발, 스테이징, 프로덕션에서 서로 다른 역할을 사용합니다.

- name: Configure AWS credentials (Production)
  if: github.ref == 'refs/heads/main'
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: ${{ secrets.AWS_ROLE_ARN_PROD }}
    aws-region: ap-northeast-1

- name: Configure AWS credentials (Development)
  if: github.ref == 'refs/heads/develop'
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: ${{ secrets.AWS_ROLE_ARN_DEV }}
    aws-region: ap-northeast-1

4. CloudTrail로 감사

모든 API 호출을 기록하고 정기적으로 검토합니다.

# CloudTrail용 S3 버킷
resource "aws_s3_bucket" "cloudtrail" {
  bucket = "my-cloudtrail-logs-bucket"
}

resource "aws_s3_bucket_policy" "cloudtrail" {
  bucket = aws_s3_bucket.cloudtrail.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AWSCloudTrailAclCheck"
        Effect = "Allow"
        Principal = {
          Service = "cloudtrail.amazonaws.com"
        }
        Action   = "s3:GetBucketAcl"
        Resource = aws_s3_bucket.cloudtrail.arn
      },
      {
        Sid    = "AWSCloudTrailWrite"
        Effect = "Allow"
        Principal = {
          Service = "cloudtrail.amazonaws.com"
        }
        Action   = "s3:PutObject"
        Resource = "${aws_s3_bucket.cloudtrail.arn}/*"
        Condition = {
          StringEquals = {
            "s3:x-amz-acl" = "bucket-owner-full-control"
          }
        }
      }
    ]
  })
}

# CloudTrail
resource "aws_cloudtrail" "github_actions_audit" {
  name                          = "github-actions-audit"
  s3_bucket_name               = aws_s3_bucket.cloudtrail.id
  include_global_service_events = true
  is_multi_region_trail        = true
  enable_logging               = true

  depends_on = [aws_s3_bucket_policy.cloudtrail]
}

결론

GitHub Actions와 AWS 간의 OIDC 연동은 다음과 같은 장점을 제공합니다:

  • 액세스 키 없는 안전한 배포
  • 임시 자격 증명만 사용
  • 저장소나 브랜치별 세밀한 권한 제어
  • 관리 비용 감소

초기 설정이 다소 복잡하지만, 일단 구성하면 장기적으로 보안과 운영성이 향상됩니다. 적극적인 도입을 권장합니다.

참고 자료