GCRGitHub ActionsNext.JSCI/CD

Github Actions를 이용한 Google Cloud Run 배포 자동화

2022.10.03


최근에 감사하게도 블로그의 어떠한 글로 많은 분들이 찾아오시면서, GitHub Markdown API 호출에 대한 rate limit이 걸렸고, 정상적으로 글을 읽을 수 없던 사건이 발생했다. 기존 블로그 설계가 잘못되었음을 느꼈고, Golang + React로 만들어진 블로그를 Next.JS를 사용하도록 설계를 변경했다. 이 과정에서 github repository로 push할 경우, Github에서 제공해주는 worflow 중 Build and Deploy to Cloud Run를 사용하여 배포가 진행되도록 하였다. 상세한 방법은 글의 맨 마지막 부분에 작성하였다.

기존 블로그의 구조 · 문제점 파악 / 개선 방법 선정

최초에는 위와 같은 형태로 블로그를 개발하여 배포하였다. 기존 구조에서는 다음과 같은 몇 가지 문제점이 있었다.

  • 블로그 글이 쉽게 검색되지 않음
  • GitHub API Rate Limiting으로 인해, 단시간에 사용자가 몰릴 경우 markdown을 html로 파싱하는 API 접근이 막힌다. (Public 경우 1시간에 60번 / Token이 주어지더라도 1시간에 5,000번)

  • 블로그를 만든 최초 목적을 생각해보면, 많은 사람들이 방문하는 것에는 관심이 없었다. 그저 나만의 블로그를 만들어보는 것이 재미있었고, 공부한 내용을 블로그에 올리며 지식을 쌓는것이 목적이었다.

  • 우아한 블로그를 만들고 싶었으나, 결과적으로는 안티패턴 범벅의 서버/클라이언트 코드가 완성되었고, 유지보수를 해야하는 상황이 발생했을 때, 과거에 내가 작성한 코드를 이해하기위해 적지않은 시간을 사용해야했다.

  • 부스트캠프 웹풀스택 슬랙 채널에 블로그 글을 공유했을 때, 백엔드 마스터님께서 개발바닥 오픈채팅방으로 글을 공유해주셨고, 이로 인해 생전 처음보는 Analytics 그래프를 맞이하였다.

  • 나의 글이 누군가에게 도움이 될 수도 있다는 것에 참 기쁘고 감사했고, 이런 순간들을 많이 경험하고 싶다는 생각이 들었다.
  • 그러나, 현재 블로그 구조로는 사람들이 검색해서 들어올 수도 없고, Token을 제공하는 식으로 API를 요청하더라도 정해진 rate limit 이상으로 요청이 들어온다면 똑같은 장애 상황을 맞이하게 된다.
  • 기존 구조를 그대로 활용한다면, 서버에 배포하기 전, github api를 활용하여 렌더링 된 html을 그리는 방식이 있었으나, 만약 글의 갯수가 rate limit 이상으로 많아진다면, 똑같은 문제를 맞이하게 된다고 생각했다. (평생 글을 5,000개 이상 쓸 수 있을까?..)
  • 따라서, 기존에 열심히 만들어놓은 component들을 편하게 재활용하고, 구조도 조금만 바꾸면 되겠다는 판단으로, Next.JS를 활용하여 SSG를 사용하는 블로그로 만들겠다는 결정을 했다.
  • 이렇게 완성된 블로그는 서버에서 렌더링 된 html을 제공해주니까(SSR) 크롤러가 데이터를 잘 가져갈 것이고, 사용자가 컨텐츠를 보기까지 걸리는 속도도 기존에 비해 빨라질 것이라는 판단이 들고나서부터, 블로그 개선 작업에 더욱 박차를 가하게 되었다.

Next.JS?

Next.js gives you the best developer experience with all the features you need for production: hybrid static & server rendering, TypeScript support, smart bundling, route pre-fetching, and more. No config needed.

  • 완성된 구조는 아래와 같다. 처음과 비교했을때 상당히 간단해졌다고 생각한다.

  • 블로그를 개발할 당시에도 Next.JS의 존재해 대해 알고 있었지만, 그 당시 Next.JS를 사용하지않은 이유는 다음과 같다.

    • CSR과 SSR의 차이가 무엇인지 모름.
    • 어떻게 SSR로 구현할 수 있는지 방식을 모름.
    • Next.JS 사용 방법에 대한 이해가 없음.
  • Next.JS는 라이브러리에서 지정해놓은 method 들을 활용하여(getStaticProps, getStaticPaths 등...), 데이터를 채워넣은 html을 빌드한다.

  • /pages 내부가 곧 routing 경로가 된다.

  • next export로 Static HTML을 Export할 수 있으며, 빌드에 필요한 대부분의 feature들을 사용가능하지만, node.js 서버를 필요로 하는 기능들은 지원되지 않는다. 링크

Google Cloud Run

완전 관리형 서버리스 플랫폼에서 원하는 언어(Go, Python, 자바, Node.js, .NET)를 사용하여 확장 가능하고 컨테이너화된 앱을 빌드하고 배포합니다. 신규 고객에게는 Cloud Run에 사용할 수 있는 300 달러의 무료 크레딧이 제공됩니다. 모든 고객에게 매월 200만 건의 요청이 무료로 제공되며 크레딧이 차감되지 않습니다.

서버리스 플랫폼이라고 보면 된다. 코드가 사용 중인 시간으로 요금을 청구하는 방식이다. 가격

등급 CPU 메모리 요청3
무료 처음 180,000vCPU-초/월 무료 처음 360,000GiB-초/월 무료 요청 200만 개/월 무료
  • GiB-초 : 1초동안 1기비바이트의 인스턴스를 실행하거나, 4초 동안 256MiB의 인스턴스를 실행하는 경우를 의미
  • 즉, 트래픽이 정말 많지 않은 서비스의 경우 거의 무료로 서버를 사용할 수 있다는 장점이 있다. (그러나 콜드 스타트를 고려해야한다. 참고)
  • 갑작스러운 트래픽 급증이 발생할 경우, 최대 인스턴스 수 미만으로 인스턴스를 생성하여 알아서 트래픽을 분할해준다.
  • 개인적으로 Docker container를 만들어서 배포하는 방식이 편리하여 애용하고 있다.

GitHub Actions

이 포스트를 작성한 이유는 이 부분에 있다.

  • 이전에는 shell script를 별도로 작성하여, GitHub에 push하는 것과 별도로, shell script를 실행시켜주는 식으로 빌드를 진행했으나, 이 과정을 GitHub Actions의 workflow 중 Build and Deploy to Cloud Run 을 사용하여 서버에 배포하는 과정을 위임하는 것이 좋겠다고 판단했다.

  • Local에서 build하다보니, docker image들 삭제하는 것을 까먹으면 금새 노트북 용량이 부족해지는 현상이 발생하기도 했다.

  • 특정 브랜치에 push하면 build되는 action이 실행되도록 할 수 있으며, commit message에 특정 키워드가 존재하면 push나 pull_request 이벤트로 인해 발생하는 workflow 실행을 skip할 수 있다. 참고

    • 키워드 : [skip ci][ci skip][no ci][skip actions], [actions skip]
    • 또는, 내가 원하는 키워드에만 action이 실행되도록 설정할 수도 있다. 참고
  • GitHub Actions를 통해 Google Cloud에 접근하는 방법에는 여러가지가 있으나, 링크 에서는 Workload Identity Federation 을 사용하는 예시가 잘 작성되어있으며, Service Account Key JSON을 export하는 것으로 인한 보안 위험을 감수하기 보다는 이 방식을 따르기를 추천하고 있다.


GitHub Actions + Google Cloud Run으로 배포 자동화 방법

1. repository에 worflow 추가

  1. 자동으로 생성되는 .github/workflows/google-cloudrun-docker.yml 내부에서 env 부분을 프로젝트에 맞게 수정해준다. (아래는 예시)
...

env:
    PROJECT_ID: "projectname-123456" # Dashboard에서 확인
    GAR_LOCATION: "gcr.io/projectname-123456/cloud_run_service_name"
    # gcr.io/${PROJECT_ID}/${SERVICE_NAME} 구조이다.
    SERVICE: "cloud_run_service_name" # Cloud Run에서 생성한 서비스 이름
    REGION: "asia-east1" # 혹은 원하는 region

...
  1. 이후부터, 링크 설명을 따라 진행하면 되나, optional 한 부분 중 permission을 grant하는 부분이 진행되지 않으면 workflow가 permission denied 될 것이다.
export PROJECT_ID="projectname-123456" # Dashboard에서 확인

# 만약, fish shell을 사용하고 있다면 ${VARIABLE} 이 아니라 $VARIABLE 과 같이 사용해야한다.

# 1. iam 계정 생성하기
gcloud iam service-accounts create "account-name" \
  --project "${PROJECT_ID}"

# 2. iam service enable
gcloud services enable iamcredentials.googleapis.com \
  --project "${PROJECT_ID}"

# 3. workload-identity-pool 생성
gcloud iam workload-identity-pools create "service-pool" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --display-name="Service pool"

# 4. workload-identity-pool 정보 가져오기
# "service-pool" 은 3. 에서 생성한 "service-pool" 값 사용
gcloud iam workload-identity-pools describe "service-pool" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --format="value(name)"

export WORKLOAD_IDENTITY_POOL_ID="..." # value from above

# 5. workload-identity-pools로 oidc 생성
# --workload-identity-pool 값으로 3. 에서 생성한 "service-pool" 값 사용
gcloud iam workload-identity-pools providers create-oidc "service-provider" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="service-pool" \
  --display-name="Service provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
  --issuer-uri="https://token.actions.githubusercontent.com"

export REPO="username/name"

# 6. 계정에 iam policy 추가
# account-name은 1.에서 사용한 account-name으로 설정
gcloud iam service-accounts add-iam-policy-binding "account-name@${PROJECT_ID}.iam.gserviceaccount.com" \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"

# 7. workload-identity-pools value 생성
# "service-provicer"는 5. 에서 생성한 "service-provider" 값 사용
# --workload-identity-pool 값으로 3. 에서 생성한 "service-pool" 값 사용
gcloud iam workload-identity-pools providers describe "service-provider" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="service-pool" \
  --format="value(name)"
  • 7. 의 결과 값을 WIF_PROVIDER 로, WIF_SERVICE_ACCOUNT{ACCOUNT_NAME}@{PROJECT_ID}.iam.gserviceaccount.com 형태로 GitHub Secret을 지정한다.

  • 한번 생성한 secret 값은 update만 가능하고, 읽을 순 없다. 알맞은 값으로 잘 설정하자.
  • 여기서 설정한 값을 Google Auth 단계에서 읽고, access_token 을 생성한다.
  • 위의 과정을 따라하였으나 오류가 난다면, 아마도 권한이 없다는 오류일 확률이 높다. 만약 권한 오류가 맞다면, 다음과 같이 해보자.
  1. 해당프로젝트의 Google Cloud 콘솔 - IAM으로 들어가서, WIF_SERVICE_ACCOUNT 계정이 존재하는지 확인한다.
  2. 만약 없다면 grant access로 new principals로 해당 계정을 추가해준다.

  3. 해당 계정에 권한을 추가로 부여한다.

  • 이 때 필요한 권한은 다음 4가지이다.
    • Cloud Run Admin
    • Cloud Stoage for Firebase Service Agent
    • Service Account User
    • Storage Object Admin
  1. 완료된 후, 다시 실패했던 workflow를 재시작하거나, push event를 발생시켜본다.

추가 참고 링크