GitHub Actions에서 환경변수 사용 문제 - boostcamp-2020/IssueTracker-13 GitHub Wiki

요약

  • CI 환경 구축을 위해서 GitHub Actions 스크립트에 npm run test 추가 시도
  • fork된 개인계정 Repo에서 PR을 보냈을 때, 환경변수를 사용하지 못하는 문제가 발생
  • Encrypted Secrets와 pull_request_target 이벤트를 이용하여 해결

문제상황

GitHub Actions CI에서 환경변수 사용하기

  • GitHub Actions에서 프로젝트와 테스트의 정상 구동을 위해서 환경변수 설정이 필요하다.
  • .env 파일은 소스코드와 같이 업로드 되지 않기 때문에 GitHub Actions CI 환경에서 환경변수를 사용할 방법이 필요하다.

Forked Repository에서 보낸 PR에서도 환경변수 사용하기

  • GitHub Actions는 PR을 통해서 source -> target 방향으로 코드를 merge 할 때, source의 GitHub Actions 스크립트 파일(.yml)을 사용한다.
  • 오픈소스 프로젝트의 경우, 제3자가 fork 해서 보낸 PR의 스크립트가 Encrypted Secrets를 접근할 수 있다면 값의 유출 가능성이 존재한다.
    • DB에 접근할 수 있는 HOST, USER, PASSWORD 정보
    • Oauth를 위해 필요한 Client ID, Client Secret 정보
  • 위 이유 때문에 GitHub는 Forked Repo에서 Public Repo로 보내는 PR의 경우 secrets 환경변수의 접근을 허용하지 않는다.
  • 우리 팀은 개인 Repo의 feature 브랜치에서 공동 Repo의 develop 브랜치로 PR을 보내는데, 이때 CI의 구동이 불가능하다.

해결과정

이미 build와 lint에 대해서 GitHub Actions를 통한 CI 환경이 갖추어진 상황에서, Backend API가 완성됨에 따라서 작성된 endpoint 테스트에 대한 test CI를 추가할 필요성이 생겼다. 최초에 backend.yml 파일에 npm run test 스크립트를 추가한 이후 아래와 같은 오류가 발생했다.

CI_error

Backend에서는 현재 GitHub Oauth 로그인을 위해서 Client ID와 Client Secret을 사용하고 있다. 외부에 노출되면 안되는 값이기 때문에 .env 파일로 로컬에서 관리하고 process.env 로 접근하고 있다. GitHub Actions는 나의 로컬 컴퓨터가 아니고, GitHub가 제공한 환경에서 구동되고 이 환경에는 .env 파일과 그 안의 환경변수들이 없기 때문에 문제가 발생한 것이라고 파악했다.

최초에는 GitHub Actions에 환경변수를 제공해주는 방법을 찾아보았다. 간단한 검색을 통해서 GitHub Documenation에서 환경변수를 제공하는 법을 찾아낼 수 있었다.

env_var_docs

위와 같이 스크립트 파일 안의 run 명령어 밑에 env 키워드를 통해서 환경변수를 추가해줄 수 있다고 나와있었다. 하지만 환경변수를 backend.yml 파일에 string 값으로 적어줄 경우 public repository이기 때문에 환경변수가 노출될 것이다. 따라서 .env 파일와 유사한 방식으로 환경변수를 제공해 줄 방법이 필요했다.

추가적인 검색을 통해서 Repository 단위로 비밀 환경변수를 설정하는 방법을 찾아냈다. Repository의 Settings 탭에서 Secrets를 설정해서 string이 아닌 변수값으로 환경변수를 제공해 줄 수 있는 방법이었다.

repo_secrets

위와 같이 부스트캠프 Repository에 Secret 값을 지정해주고, .env 파일의 값들을 Secret으로 옮겨서 넣었다.

backend_ci_script

backend.yml 파일도 secrets 값을 이용하도록 스크립트를 바꾸어 주었다.

하지만 위 작업을 하고 나서도 테스트가 정상적으로 구동되지 않는 문제가 똑같이 발생했다. 검색을 통해서 찾아본 결과, env: 키워드 대신 .env 파일을 스크립트 내부에서 동적으로 생성해주는 방법이 있었다. 혹시 환경변수를 .env 파일을 똑같이 만들어주어야 제대로 dotenv가 적용되는 것일까? Documentation을 읽다 보니 GITHUB_ prefix가 들어간 키워드의 경우 GitHub 자체에서 세팅하는 환경변수와 충돌이 날 수 있어서 사용하면 안된다는 내용을 찾았다. 그렇다면 .env에 들어가는 환경변수 이름을 바꾸어 보아야 할까? 문제를 해결하기 위해서 GITHUB_CLIENT_ID로 쓰던 환경변수명을 CLIENT_ID_GITHUB로 바꾸고, 동적으로 .env 파일을 생성하도록 스크립트를 아래와 같이 바꾸어 보았다.

backend_ci_create_env

위와 같이 스크립트를 바꾸고 나서도 문제가 해결되지 않았다. 디버깅을 위해서 .env 파일을 생성 직후 cat .env를 이용해서 생성된 .env 파일의 내용을 보도록 스크립트를 수정했다.

backend_ci_cat backend_ci_cat_fail

놀랍게도 .env 파일에서는 모든 환경변수가 빈 string값으로 나타나고 있었다. 순간 GitHub CI가 똑똑해서 secret 값의 접근의 경우 자동으로 값을 가려주는 것이 아닌가? 라는 생각이 들었다. 하지만 스크립트 log는 그렇다고 해도 cat으로 파일내용을 찍은 것까지 GitHub Actions가 가려줄 것 같다는 생각은 들지 않았다.

오랫동안 왜 secrets에 접근이 이루어지지 않는지 고민하고 찾아보았으나, stackoverflow에도 만족스러운 결과를 찾지 못하고 있었다. 검색을 계속하다가 결국 GitHub의 개발 블로그에서 최근(2020년 8월)에 작성된 하나의 글을 찾을 수 있었다. 이 글을 찬찬히 읽어보니, Repo 내부의 PR이 아닌 외부 Repo에서 보내는 PR의 경우에는 보안 상의 문제로 secrets 변수의 접근이 허용되지 않는다는 사실을 찾을 수 있었다. 한참 내가 명령어를 잘못 쓰고있나? 환경변수도 node에 들어가는 것과 운영체제 단위가 다른가? 고민을 한참 했었는데 허탈하게도 이런 제한이 있었던 것이다.

no_secret_access

다행히 해당 블로그 글에서는 이 문제가 forking model을 사용하는 수많은 사용자들에게 발생하는 문제였으며, 이것을 고치기 위해서 새로운 개선방법을 도입했다고 나와 있었다. 1번 방법의 경우에는 private repository에서 사용 가능한 세팅으로, 환경설정 탭에서 외부 PR에 대해서도 환경변수 사용을 허용해 주는 세팅이 추가되었다는 방법이었다.

private_repo_forked_PR

우리 프로젝트의 경우에는 public repo이기 때문에 1번의 방법은 사용이 불가능했다. 2번 방법의 경우에는 GitHub Actions Script 파일에서 CI의 구동을 trigger하는 이벤트를 바꾸어주는 방법이었다. 원래 우리는 [push, pull_request] 키워드에 대해서 CI 스크립트를 구동하고 있었다. pull_request 키워드가 PR에서 CI를 구동해주고 있었는데, 이 키워드는 source -> destination 방향의 PR에서 source 쪽의 backend.yml 파일을 이용해서 CI를 돌려주고 있었다. 이를 이용해서 지금까지 나는 스크립트를 원본 repo에 반영하기 전에도 해당 PR을 통해서 스크립트 구동을 확인하고 있었다.

CI_events_original

2번 방법에서 소개하고 있는 키워드인 pull_request_target의 경우에는 위와 달리 destination의 backend.yml 파일을 이용해서 GitHub Actions CI를 구동해주고, 원본 repository의 스크립트는 이미 검증된 파일이기 때문에 secrets에 접근할 수 있도록 허용해 준다고 소개가 되어 있었다.

위 방법을 사용하기 위해서는 boostcamp-2020/IssueTracker-13 Repo에 이미 키워드가 변경된 backend.yml 파일이 반영되어 있어야 했으나, 팀 그라운드 룰에 따라서 우리는 소스코드 변경을 무조건 PR을 통해서 review를 받고 할 수 있도록 합의했었다. 하지만 스크립트를 변경하는 PR을 보낼 경우, 원본 Repo의 스크립트는 PR에서 secrets를 사용하지 못하기 때문에 테스트 CI가 실패하고, 이 때문에 merge가 불가능했다. Deadlock에 걸린 느낌이 이런 것일까? 어쩔 수 없이 팀원들에게 양해를 구하고, GitHub에서 직접 스크립트 파일을 열어서 직접 파일을 아래와 같이 수정해주었다.

CI_events_changed

위와 같이 변경되자 마자 내가 세팅해놓았던 테스트는 마법같이 동작하게 되었다. 이제 우리는 모든 PR을 돌릴 때마다 npm run test를 통해서 테스트 DB에 접속하고, 테스트 DB를 통해서 API를 독립적으로 검증하고 코드를 merge할 수 있게 되었다. 목요일 내내 붙잡고 있던 문제를 해결하고 테스트에 파란불이 떴을 때 정말 짜릿함을 느꼈다. 결국 몇시간 동안 헤매된 문제가 키워드 하나를 바꿈으로서 해결된 것이다. .env 파일을 생성해주는 것도 결국 별 차이를 만들어낸 일이 아니었고, 결국 pull_request를 pull_request_target으로 7글자를 바꾸어주는 것이 모든 문제의 해결방법이었다.

CI_test_success

추후 이 pull_request_target 이라는 키워드의 설명이 GitHub Actions 공식 Documentation에 있었다는 사실을 알게 되었다. 아무리 google에 물어보고, stackoverflow를 뒤져도 나오지 않는 답은 결국 공식 Documentation에 있었던 것이다.

또 한번 다시 공식 Documentation의 중요성과, stackoverflow도 결국 모든 답변을 가지고 있는 것이 아니라는 교훈을 얻을 수 있었다. 결국 나와 동일한 문제상황을 겪은 사람에 의존해서 해결하는 방법은 정말 처음보는 문제를 해결하는 데는 아무 도움을 줄 수 없다.

P.S

PR을 날릴때마다 테스트 DB에 대한 변경이 누적될텐데, 도대체 어떻게 매번 테스트를 일관성있게 구동할 수 있는 지 궁금해진 독자님들이 있나요? 저희 팀은 Umzug을 이용해서 Sequelize Migration을 자동화하고, 테스트가 구동될 때마다 데이터베이스를 자동으로 초기화하고 테스트 데이터를 넣어주고 있습니다. Umzug 또한 Sequelize의 공식문서 한 구석에서 찾아낸 패키지입니다. 👨

umzug