AWS S3로 GitHub Actions CI/CD 구축 여정: 환경 변수 누락과 SignatureDoesNotMatch 오류 해결기

이영섭·2025년 6월 5일

bovo 프로젝트

목록 보기
4/10

문제의 시작

초기 AWS S3 bucket에 업로드되는 과정까지는 잘 진행되었으나, 코드의 수정사항이 발생될 때마다 Github에서 PR시 Github Action에서 계속 실패했다. awact/s3-action@master 작업 과정 중에 실패가 지속적으로 발생됐으며 발생되는 지점이
Secret and Variable로 설정한 환경 변수가 비워져서 ""로 나타나거나 AWS_S3_BUCKET is not set. Quitting. 과 같은 변수가 설정되지 않았다는 에러문구가 나타났다.

초기에는 흔히들 yml 파일에서 설정한 코드의 오타라던지 Github repo의 settings에서 설정한 secret 변수에 대해 오타가 있어서 흔히 발생되는 문제라는 글을 많이 읽었기에 확인해 보았지만 오타는 발견하지 못하였다.

이 문제를 해결하기 위해 근 1~2일 정도 장시간 투자했고, 그간 했던 수십번의 시도들 중 유의미한 결과를 낸 시도 또는 새롭게 알게 된 것들을 정리해보고자 한다.

  • 기존 node.js.yml
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs

name: Node.js CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [22.x]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}
        cache: 'npm'
        cache-dependency-path: './bovo_client/package-lock.json'
    - name: Install dependencies
      working-directory: ./bovo_client # npm i 명령을 실행할 디렉토리 지정
      run: npm i

    - name: Build
      working-directory: ./bovo_client # npm run build 명령을 실행할 디렉토리 지정
      run: npm run build --if-present

    # - name: Test
    #   working-directory: ./bovo_client # npm test 명령을 실행할 디렉토리 지정
    #   run: npm test

    - uses: awact/s3-action@master
      with:
        args: --acl public-read --delete
      env:
          SOURCE_DIR: './bovo_client/dist'
          AWS_REGION: 'ap-northeast-2'
          AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

1단계: 환경 변수 누락 문제와 workflow 스코프

처음 생각은 당연히 Github repository의 setting에 secret and variale에 입력한 secret 변수가 누락되었으니 입력 방식에 오류가 있는지부터 점검하였다.
처음에는 secret 변수 입력시에 value 입력방식이 다른 것인가 였다.

이 Secret에 예를 들어 고양이라는 값을 입력하려면 "고양이"라고 입력해야 되나 싶었지만 그냥 값 그대로 고양이를 입력하면 되었다.

해결 과정

이후 GitHub 레포지토리의 Settings > Actions > General 섹션에서 아래쪽에 위치한 Workflow permissions 옵션을
workflow permission 메뉴 1단계
workflow permission 메뉴 2단계
Read and write permissions로 설정하고 Allow GitHub Actions to create and approve pull requests 옵션까지 체크했다.
worflow permission 체크
이 설정 후 환경 변수 누락 문제가 완화되는 듯 보였지만, 곧이어 SignatureDoesNotMatch라는 새로운 오류에 직면했다.

  • 에러 화면
    Github Action 에러 화면

왜 이렇게 설정했는가?

사실 이 방법은 우연의 산물이다. 지속적인 환경변수 누락으로 yml 파일을 수정하였고 PR이 실패되는 경우도 많았다. 이에 따라 repository 설정을 변경하다 action에 환경변수가 어찌됐든 "" 비어있지 않고 ***로 입력은 되게 되었다.
Workflow permissions 설정은 GitHub Actions workflow가 repository에 접근할 수 있는 기본 권한을 정의한다. 이에 관한 설명은 관련 공식 문서의 "Preventing GitHub Actions from creating or approving pull requests"에서 더 자세히 볼 수 있다.

Github Action_Github 공식문서

Read and write permissions는 workflow가 repository에 읽기 및 쓰기 작업을 수행할 수 있도록 허용한다. 특히 node.js.yml과 같은 워크플로우 파일 자체를 수정하거나 워크플로우를 통해 repository의 상태를 변경하는 작업(git push와 같은)을 수행할 때 이 권한이 필요하다.

공식 문서 상 환경변수와 관련된 설정이라기 보다는 Github Action의 권한을 어디까지 허용할 것인지에 대한 설정으로 보인다. 다만, 현상적으로 환경변수가 비워져 있다가 이 옵션을 체크한 이후 입력이 되기는 했다는 점이다. 따라서 결과론적으로 직접적인 연관은 없지만 repository의 setting에 등록된 secret and variable에서 가져올 때 간접적인 영향이 있을 것으로 추정된다.

2단계: SignatureDoesNotMatch 오류와 IAM 정책 권한

문제 발생

환경 변수 누락 문제가 어느 정도 해소된 후, fatal error: An error occurred (SignatureDoesNotMatch) when calling the ListObjectsV2 operation: The request signature we calculated does not match the signature you provided. Check your key and signing method. 오류가 발생했다. 이 오류는 AWS API 요청 시 제공된 자격 증명으로 생성된 서명과 AWS가 계산한 서명이 일치하지 않는다는 의미이다.

1차 시도: access key 재발급

SignatureDoesNotMath에 대한 오류 검색 중 흥미로운 블로그 글을 발견하게 되었다.

참조 블로그
accessKey 적용 관련 문제 발생

해당 블로그 글의 주요 요지는 AWS의 I AM 서비스의 유저를 생성하고 secret accessKey를 발급받았을때, '/'나 '%'와 같은 특수문자가 들어있을 경우에 해당 오류가 발생한다는 것이다.

사실 이 문제에 대해서는 고민하지 않았던 것이 AWS 서비스로 발급하는 key인데 그 문자자체에 에러가 있을 것이라는 고민은 해본적이 없었다.

이에 따라 I AM 유저를 새로 생성한 후 Secret Access Key에 특수문자가 없을 때까지 반복해서 재발급하였다. (물론 이전 key들은 비활성화 후 삭제해주었다.) 다만 이 시도는 문제 해결에 직접적인 도움이 되지는 않았지만, 이러한 경우도 있을 수 있겠다는 생각을 넓혀주었다는 점에서 시도해볼만 하다고 생각했다.

2차 시도: I AM 유저의 권한(정책) 추가

이후, IAM 사용자에게 AmazonS3FullAccess 정책 외에 더 구체적인 S3 권한을 추가해야 한다고 생각하여 다음과 같은 사용자 지정 IAM 정책을 추가했다.

① I AM 서비스 이동

② 정책을 custom하기 위해 액세스 관리 > 정책 > 정책 생성을 클릭한다.
정책 생성 메뉴

③ JSON 형식으로 변환하고 다음 구문을 추가해주었다. (주석은 제거)
정책 편집

  • JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",     // S3 버킷의 객체 목록을 나열 (ListObjectsV2에 필요)
                "s3:PutObject",      // S3 버킷에 객체 업로드
                "s3:DeleteObject",   // S3 버킷에서 객체 삭제 (--delete 옵션 사용 시 필요)
                "s3:GetObject",      // S3 버킷에서 객체 다운로드
                "s3:PutObjectAcl"    // --acl public-read 사용 시 필요
            ],
            "Resource": [
                "arn:aws:s3:::bovo-client",        // 특정 버킷 자체
                "arn:aws:s3:::bovo-client/*"       // 특정 버킷 내 모든 객체
            ]
        }
    ]
}

④ 해당 정책을 적용할 I AM 유저로 들어가 [권한 탭]에서 '권한 추가'를 선택해준후 해당 정책을 연결해준다.

왜 이런 권한이 필요한가?

다른 블로그나 chatgpt에서의 배포 정책에 대해 내가 실행하지 않은 방법을 찾다가 추가한 방법이다.

그러나 해결된 이후 다시 충분히 살펴보니 해당 custom 정책이 버킷을 특정하고 권한을 최소범위로 설정하여 보안을 지켰을뿐 해당 권한자체는 기존에 S3FullAccess에서 범위를 축소한 것이나 다름없다.

  • 기존 S3 Full Access 정책
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:*",
                "s3-object-lambda:*"
            ],
            "Resource": "*"
        }
    ]
}
Effect 명시적 정책에 대한 허용 혹은 차단
principal 접근을 허용 혹은 차단하고자 하는 대상
Action 허용 혹은 차단하고자 하는 접근 타입
Resource 요청의 목적지가 되는 서비스
Condition 명시적 조건이 유효하다고 판단될 수 있는 조건
  • custom한 정책의 세부 설명
    - s3:ListBucket
    S3 배포 액션은 배포 전 현재 버킷의 객체 목록을 확인하고, 변경 사항을 파악하기 위해 이 권한이 필요합니다. ListObjectsV2 API 호출은 이 권한을 필요로 합니다.

  • s3:PutObject
    S3 버킷에 새로운 파일이나 변경된 파일을 업로드하는 데 필요합니다.

  • s3:DeleteObject
    awact/s3-action의 args: --delete 옵션을 사용하면, 소스 디렉토리에 없는 버킷의 파일을 삭제하므로 이 권한이 필요합니다.

  • s3:GetObject
    배포된 파일을 확인하거나, 특정 시나리오(예: CDN 캐시 무효화 전 파일 내용 확인)에서 필요할 수 있습니다.

  • s3:PutObjectAcl
    객체를 업로드할 때 public-read와 같은 특정 ACL(Access Control List)을 설정하려고 할 때 필요합니다.

이 정책을 생성하고 권한을 추가함으로써 SignatureDoesNotMatch 오류가 해결될 것이라고 기대했지만, 위에서 설명했듯 권한 범위만 특정한 것으로 여전히 오류가 발생했다.

3단계: --acl public-read 옵션과 버킷 ACL 비활성화

문제 발생 및 해결 과정

이 단계에서 외국 블로그 글이 결정적인 힌트를 주었다. 배포를 처음단계부터 다시 살피고자 검색했고, 아래의 블로그글을 찾게 되었다.

참조 블로그
CI/CD break

블로그 글의 저자는 jakejarvis/s3-sync-action 액션(나의 경우 awact/s3-action@master에서)에서 --acl public-read 옵션 때문에 AWS 업로드가 차단되었다고 설명했다. 나는 awact/s3-action 액션을 사용하고 있었고, 마찬가지로 args: --acl public-read --delete 옵션을 사용하고 있었다.

블로그 글에 따르면, S3 bucket이 이미 버킷 정책(Bucket Policy)을 통해 접근을 제어하고 있거나, 버킷 생성 시 객체 ACL(Access Control List)이 비활성화되어 있다면, workflow에서 --acl public-read 옵션을 통해 객체 ACL을 설정하려는 시도는 AWS에 의해 거부될 수 있다고 설명하고 있다. 이는 권한 부족 오류(SignatureDoesNotMatch나 Access Denied)로 나타날 수 있다.

  • 해결책
    workflow YAML 파일에서 awact/s3-action step의 args에서 --acl public-read 옵션을 제거하고, args: --delete만 남겨두다.

  • YAML

      - name: Upload to S3
        uses: awact/s3-action@master
        with: 
          # --acl public-read 제거
          args: --delete 
        env:
          SOURCE_DIR: './bovo_client/dist'
          AWS_REGION: 'ap-northeast-2'
          AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

이 변경 후, 기존의 SignatureDoesNotMatch 오류 대신 InvalidAccessKeyId 오류가 발생하는 것을 확인했다. 이는 이전 SignatureDoesNotMatch 오류가 --acl public-read 옵션으로 인한 권한 충돌 때문이었을 가능성을 시사한다.

왜 이렇게 설정해야 하는가?

최신 AWS S3 버킷은 기본적으로 객체 ACL이 비활성화되어 있고, 버킷 정책(Bucket Policy)을 통해 접근 권한을 관리하는 것이 권장된다. 버킷 정책은 권한 관리를 중앙 집중화하여 더 쉽고 안전하게 만든다. 만약 버킷에서 ACL이 비활성화되어 있는데도 PutObjectAcl을 시도하면 AWS는 이를 거부한다. 따라서 불필요하거나 충돌을 일으킬 수 있는 --acl public-read 옵션을 제거하는 것이 올바른 접근이다.

4단계: IAM 사용자 생성 시 '액세스 키 모범 사례 및 대안' 재확인

문제 발생 및 해결 과정

--acl public-read 옵션 제거 후 InvalidAccessKeyId 오류가 발생했고, 이는 AWS_ACCESS_KEY_ID 자체가 유효하지 않다는 의미이다.

이 역시 여러 사이트를 검색하던 중 알게되어 시도했던 방법이다.

참조 사이트
access key 모범 사례 및 대안 설정

IAM 사용자 생성 시 액세스 키 모범 사례 및 대안 섹션에서 '기타'가 아닌 'AWS 외부에서 실행되는 애플리케이션'을 체크하고 IAM 유저를 생성하여 Access Key를 발급받은 것이었다.
access key 모범 사례 및 대안 설정 화면

왜 이렇게 설정해야 하는가?

초기 '기타' 옵션을 선택한 이유는 특정환경을 가정하지 않은 일반적인 옵션이라 생각하여 나의 프로젝트에도 별 무리없이 적용될 것이라 생각했고, 해당 step이 accessKey에 영향이 없는 일종의 사용목적을 묻는 과정이라 생각했기 때문이다.

다만 여러 글들을 읽고 생각해보니 Github Action이 일어나고 Access Key를 사용하는 것 자체가 AWS 외부에서 실행되는 상황이며, 그에 맞는 내부적인 권한 및 보안 설정을 적용하는 step이라고 생각이 들기 시작했다.

InvalidAccessKeyId 해결

InvalidAccessKeyId 오류는 Access Key ID가 존재하지 않거나 유효하지 않다는 의미인데, I AM 유저를 새로 생성하여 해당 '액세스 키 모범 사례 및 대안'을 'AWS 외부에서 실행되는 애플리케이션'으로 설정한 후 새로 발급받은 access key와 secret access key를 Github repo의 setting에 적용하니 환경 변수 누락 문제와 ListObjectsV2에서의 SignatureDoesNotMatch 에러가 모두 해결되었다.

최종 정리

다른 프로젝트와 달리 나의 팀 프로젝트에서 발생된 환경 변수의 누락은 특정한 어떤 한 요소의 문제라기 보다는 보다 복합적인 문제로 인해 발생된 것으로 생각된다. 해결된 지금도 아리송한 느낌이다. 한가지 확실한 점은 이번 일을 통해 CI/CD를 구축하기 위해 어떠한 요소들을 점검해야 하는지 이전보다는 명확하게 파악되었다.


정리

1. 정확한 GitHub Secrets 설정
Access Key ID, Secret Access Key, S3 버킷 이름, AWS region 등 모든 시크릿 값이 오타나 누락 없이 정확하게 입력되어야 한다.

2. 적절한 IAM 정책 권한
S3 버킷에 필요한 최소한의 권한(ListBucket, PutObject, DeleteObject 등)이 IAM 사용자에게 부여되어야 한다. AmazonS3FullAccess와 같은 관리형 정책은 편리하지만, 최소 권한 원칙에는 위배될 수 있으므로 사용자 지정 정책을 고려해야 한다.

3. workflow action 옵션
사용 중인 GitHub Actions (예: awact/s3-action)의 옵션(예: args: --acl public-read)이 AWS S3 버킷의 설정(예: ACL 비활성화)과 충돌하지 않는지 확인해야 한다.

4. IAM 사용자 생성 모범 사례
특히 AWS 외부에서 실행되는 애플리케이션과 같은 올바른 '액세스 키 모범 사례 및 대안'을 선택하여 Access Key를 발급받는 것이 key의 유효성과 AWS 서비스가 해당 key를 인식하는 방식에 영향을 미칠 수 있다.

profile
신입 개발자 지망생

0개의 댓글