You are currently viewing GitHub Action에서 AWS OIDC 연동하기

GitHub Action에서 AWS OIDC 연동하기

TL DR;

  • 장기자격증명인 SSH Key 대신 GitHub OIDC로 임시 자격증명을 발급받아 보안 강화
  • AWS IAM에 OIDC Provider와 Role을 설정하고, GitHub Actions에서 AWS STS로 임시 토큰 발급받아 사용
  • 배포·빌드 시 별도 secret 세팅 절차 없이 바로 사용가능해서 사용성 향상

1. Intro


2. 설정과정

2.1. Adding the identity provider to AWS

  • AWS IAM의 Identity Providers에 사용할 identity provider(ldp)를 등록하여야 한다.
  • OpenID Connect 유형의 provider, Provider URL에 https://token.actions.githubusercontent.com, Audience에 sts.amazonaws.com 를 입력한다
    • Provider URL > 등록할 github provider의 URL
    • Audience > 토큰 생성시 사용 용도 검증용 (sts 토큰 발급 외 사용 불가)


2.2. Configuring the role and trust policy

  • AWS Role을 생성하고 해당 role이 github OIDC 토큰을 통해 sts 토큰을 발급 받을 수 있도록 설정한다
  • 사전에 AWS Role이 생성되어있어야하며, 권한(Permissions)이 아닌 신뢰관계(Trust relations)에 허용할 OIDC 공급자(github)와 요청자(github org/user/repo 등)을 설정 할 수 있다
    • Statement.Principal.Federated > "Federated": "arn:aws:iam::<AWS계정ID>:oidc-provider/token.actions.githubusercontent.com
    • Statement.Condition.StringEquals > OIDC 등록시 사용한 대상 sts.amazonaws.com
    • Statement.Condition.StringLike.”token.actions.githubusercontent.com:sub” > 발급을 허용할 대상 지정, repo:ORG-NAME/REPO-NAME:environment:ENVIRONMENT-NAME 형식
      • 특정레포의 모든 env환경 대상 > repo:nasir17git/korean-tour-data-2025-groot-data:* > nasir17git 이름의 git user/org의 korean-tour-data-2025-groot-data레포의 모든 env에 대해 사용
      • 특정 계정에 대해 사용 > repo:nasir17git/* 식으로 입력하면 workflow가 해당 계정/조직에서 작동하면 토큰을 받아올 수 있도록 설정도 가능하다
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Principal": {
				"Federated": "arn:aws:iam::827604627136: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:blcklamb/korean-tour-data-2025-groot-fe:*",
						"repo:minenam/korean-tour-data-2025-groot-be:*",
						"repo:nasir17git/korean-tour-data-2025-groot-data:*",
						"repo:nasir17git/korean-tour-data-2025-groot-fe:*"
					]
				}
			}
		}
	]
}


2.3. Updating your GitHub Actions workflow

  • github workflow 작동시 github provider에 OIDC 토큰 요청을 허용하도록 설정해야한다 (default none)
  • 이후 aws-actions/configure-aws-credentials 와 같은 marketplace action을 사용하여, role-to-assume에 할당받을 AWS Role 역할을 지정하면 workflow 작동시 Github Provider OIDC 토큰(JWT) > AWS STS 토큰 생성 및 할당 > AWS IAM Role의 Token 의 과정을 거져 sts토큰을 통해 iam 역할이 설정된것처럼 작동할 수 있다
permissions:
  id-token: write   # This is required for requesting the JWT
  contents: read    # This is required for actions/checkout


2.4. 실제 파이프라인에 적용

  • 기존에는 build후 ssh로 EC2에 접근해서 파일을 전송하였음 (scp)
    • 해당방식은 ssh를 allow all 해두었기때문에 key 접근만 활성화하였어도 찝찝함
      • github-hosted runner의 모든 IP의 대역을 등록할 수 없다
    • github secret에 키 등록이 필요하며, 추후 github secret에 등록된 SSH 키이름만 보고 어떤키인지 확인할 수 있을까..
  • sts토큰을 발급받아 작동중인 runner에 적절한 AWS IAM Role 부여
    • EC2 조회, S3 조회 및 업로드, SSM Commend 전송
  • 빌드 후 빌드파일을 S3 버킷으로 전송 후, SSM을 통해 EC2로 배포 명령어 전송 및 실행
    • Trust Relationship 이 설정된 IAM Role에 연결된 권한
    • 개별 s3 bucket, ec2에 이름 또는 태그도 지정할까 하다가.. 사프인데 굳이 그정도까지야..
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowS3Access",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:ListBucket"
            ],
            "Resource": "*"
        },
        {
            "Sid": "AllowSSMSendCommand",
            "Effect": "Allow",
            "Action": [
                "ssm:SendCommand",
                "ssm:ListCommandInvocations",
                "ssm:ListCommands"
            ],
            "Resource": "*"
        },
        {
            "Sid": "AllowDescribeInstances",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances"
            ],
            "Resource": "*"
        }
    ]
}
  • SSM으로 EC2 접근해 ec2-user 권한으로 tar.gz 다운로드 후 deploy.sh 실행
    # SSM으로 EC2 접근해 ec2-user 권한으로 tar.gz 다운로드 후 deploy.sh 실행
    - name: Trigger deploy.sh on EC2 via SSM
      run: |
        aws ssm send-command \
          --targets "Key=InstanceIds,Values=${{env.EC2_INSTANCE_ID}}" \
          --document-name "AWS-RunShellScript" \
          --comment "Deploy new frontend version" \
          --parameters 'commands=[
            "sudo -i -u ec2-user bash -c \
              \"aws s3 cp s3://groot-fe-preview/frontend/deploy.tar.gz /home/ec2-user/ && \
              cd /home/ec2-user && \
            ./deploy.sh\""
          ]'

전체 deploy.yaml

# https://raw.githubusercontent.com/blcklamb/korean-tour-data-2025-groot-fe/refs/heads/main/.github/workflows/deploy.yaml
name: deploy frontend

# 실행 이름: PR이벤트면 PR번호/제목 + SHA, 수동이면 브랜치+입력SHA
run-name: |
  ${{ github.event_name == 'pull_request' &&
      format('Deploy PR #{0} "{1}" / {2}',
        github.event.pull_request.number,
        github.event.pull_request.title,
        github.event.pull_request.head.sha
      )
      || format('Deploy BR {0} / {1}',
        github.ref_name,
        github.event.inputs.commit || 'latest HEAD'
      )
  }}

on:
  workflow_dispatch:
    inputs:
      commit:
        description: "8자리 short SHA (비워두면 브랜치의 최신 커밋)"
        required: false
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: write
  id-token: write

env:
  EC2_INSTANCE_ID: i-0ed0405fc7ced8fe4

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
    # ✅ 공통 checkout: 이벤트별로 SHA 결정
    - name: Checkout code
      uses: actions/checkout@v4
      with:
        fetch-depth: 0
        ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}

    # ✅ SHA 계산 (PR이면 head.sha, 수동이면 입력값 or HEAD)
    - name: Resolve commit SHA
      id: resolve_sha
      run: |
        if [ "${{ github.event_name }}" = "pull_request" ]; then
          FULL_SHA="${{ github.event.pull_request.head.sha }}"
        else
          if [ -n "${{ github.event.inputs.commit }}" ]; then
            FULL_SHA=$(git rev-parse ${{ github.event.inputs.commit }})
          else
            FULL_SHA=$(git rev-parse HEAD)
          fi
        fi
        SHORT_SHA=$(echo $FULL_SHA | cut -c1-8)
        echo "Using commit: $FULL_SHA ($SHORT_SHA)"
        echo "full_sha=$FULL_SHA" >> $GITHUB_OUTPUT
        echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT

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

    - name: Install dependencies
      run: yarn install --frozen-lockfile

    - name: Build application
      run: yarn build

    # ✅ 배포용 tar.gz 생성
    - name: Create deployment archive
      run: |
        tar -czf deploy.tar.gz \
          .next \
          public \
          package.json \
          yarn.lock \
          next.config.ts

    # AWS OIDC 기반 임시토큰 발급
    - name: Configure AWS credentials (OIDC)
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::827604627136:role/groot-git
        aws-region: ap-northeast-2

    - name: Upload deploy.tar.gz to S3
      run: aws s3 cp deploy.tar.gz s3://groot-fe-preview/frontend/deploy.tar.gz

    # SSM으로 EC2 접근해 ec2-user 권한으로 tar.gz 다운로드 후 deploy.sh 실행
    - name: Trigger deploy.sh on EC2 via SSM
      run: |
        aws ssm send-command \
          --targets "Key=InstanceIds,Values=${{env.EC2_INSTANCE_ID}}" \
          --document-name "AWS-RunShellScript" \
          --comment "Deploy new frontend version" \
          --parameters 'commands=[
            "sudo -i -u ec2-user bash -c \
              \"aws s3 cp s3://groot-fe-preview/frontend/deploy.tar.gz /home/ec2-user/ && \
              cd /home/ec2-user && \
            ./deploy.sh\""
          ]'

    # ✅ 배포된 URL 정상 응답 확인 (5분 동안 10초 간격 재시도)
    - name: Verify deployed URL
      run: |
        URL="https://preview.logonme.click"

        echo "🔍 배포 검증 시작: ${URL}"
        ATTEMPTS=30   # 30회 * 10초 = 5분
        COUNT=0

        while [ $COUNT -lt $ATTEMPTS ]; do
          STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${URL}" || echo "000")
          echo "Attempt $((COUNT+1)) → HTTP $STATUS_CODE"

          if [ "$STATUS_CODE" = "200" ]; then
            echo "✅ 검증 성공! HTTP 200"
            exit 0
          fi

          COUNT=$((COUNT+1))
          echo "⏳ 10초 후 재시도..."
          sleep 10
        done

        echo "❌ 검증 실패! 5분 동안 HTTP 200 응답을 받지 못했습니다."
        exit 1

    # ✅ Summary 출력 (PR이벤트와 수동 둘 다 지원)
    - name: Write deployment summary
      run: |
        SHORT_SHA="${{ steps.resolve_sha.outputs.short_sha }}"
        URL="https://preview.logonme.click"

        echo "## 🚀 프론트엔드 배포 완료" >> $GITHUB_STEP_SUMMARY
        echo "" >> $GITHUB_STEP_SUMMARY

        if [ "${{ github.event_name }}" = "pull_request" ]; then
          echo "| 항목 | 값 |" >> $GITHUB_STEP_SUMMARY
          echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
          echo "| PR 번호 | #${{ github.event.pull_request.number }} |" >> $GITHUB_STEP_SUMMARY
          echo "| 제목 | ${{ github.event.pull_request.title }} |" >> $GITHUB_STEP_SUMMARY
          echo "| 브랜치 | ${{ github.head_ref }} |" >> $GITHUB_STEP_SUMMARY
        else
          echo "| 항목 | 값 |" >> $GITHUB_STEP_SUMMARY
          echo "|------|-----|" >> $GITHUB_STEP_SUMMARY
          echo "| 브랜치 | ${{ github.ref_name }} |" >> $GITHUB_STEP_SUMMARY
        fi

        echo "| 커밋 SHA | \`${SHORT_SHA}\` |" >> $GITHUB_STEP_SUMMARY
        echo "| 접속 URL | [${URL}](${URL}) |" >> $GITHUB_STEP_SUMMARY

    # ✅ PR 이벤트면 코멘트 작성
    - name: Comment on PR with deployed commit
      if: github.event_name == 'pull_request'
      uses: actions/github-script@v7
      with:
        script: |
          const shortSha = "${{ steps.resolve_sha.outputs.short_sha }}";
          const url = `https://preview.logonme.click`;
          const pr_number = Number("${{ github.event.pull_request.number }}");

          github.rest.issues.createComment({
            issue_number: pr_number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: `🚀 PR **#${pr_number} (${context.payload.pull_request.head.ref})**\n커밋 \`${shortSha}\` 이(가) **preview 환경**에 배포되었습니다!\n👉 [${url}](${url})`
          });

3.Outro

  • DevOps로 근무하며 보안적인 이야기들을 무시할 수 없으며, 장기자격증명은 배제해야한다는 이야기는 자주듣는 이야기였다.
  • 그럼에도 편하니까.. 레퍼런스많아서 구축 간단하니까.. 임시자격증명으로 바꾸려면 더 설정할거 많고 귀찮으니까..라며 눈돌리고 있었음
  • 마음의 짐을 덜었다
  • 사용자 입장에서도 별도로 설정할것 없이 해당 yaml파일만 레포에 추가해 작동하면 잘 돌기 때문에 더욱 간편해짐
    • 이거 안전한거에요?(네 특정레포에서만 사용가능/특정 서비스에만 접근가능)에 대한 부분은 보이지않게 관리자가 AWS IAM에서 관리 가능

Leave a Reply