Cloud Run에 안전하게 배포

1. 개요

보안을 강화하기 위해 Cloud Run에 서비스를 배포하는 기본 단계를 수정한 다음 배포된 앱에 안전하게 액세스하는 방법을 알아봅니다. 앱이 '파트너 등록 서비스'임 는 Cymbal Eats와 협력하는 회사에서 음식 주문을 처리하는 데 사용합니다.

학습할 내용

Cloud Run에 앱을 배포하기 위한 최소 기본 단계를 약간만 변경하면 보안을 크게 강화할 수 있습니다. 기존 앱 및 배포 안내를 따르고 배포 단계를 변경하여 배포된 앱의 보안을 강화합니다.

그러면 앱에 대한 액세스를 승인하고 승인된 요청을 하는 방법이 표시됩니다.

여기서는 애플리케이션 배포 보안을 자세히 다루지는 않지만, 향후 모든 앱 배포에서 큰 노력을 들이지 않고도 보안을 개선할 수 있는 변경사항을 살펴보겠습니다.

2. 설정 및 요구사항

자습형 환경 설정

  1. Google Cloud Console에 로그인하여 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다. 아직 Gmail이나 Google Workspace 계정이 없는 경우 계정을 만들어야 합니다.

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 프로젝트 이름은 이 프로젝트 참가자의 표시 이름입니다. 이는 Google API에서 사용하지 않는 문자열이며 언제든지 업데이트할 수 있습니다.
  • 프로젝트 ID는 모든 Google Cloud 프로젝트에서 고유하며, 변경할 수 없습니다(설정된 후에는 변경할 수 없음). Cloud 콘솔이 고유한 문자열을 자동으로 생성합니다. 보통은 그게 뭔지 상관하지 않습니다. 대부분의 Codelab에서는 프로젝트 ID (일반적으로 PROJECT_ID로 식별됨)를 참조해야 합니다. 생성된 ID가 마음에 들지 않으면 무작위로 다른 ID를 생성할 수 있습니다. 또는 직접 시도해 보고 사용 가능한지 확인할 수도 있습니다. 이 단계 이후에는 변경할 수 없으며 프로젝트 기간 동안 유지됩니다.
  • 참고로 세 번째 값은 일부 API에서 사용하는 프로젝트 번호입니다. 이 세 가지 값에 대한 자세한 내용은 문서를 참고하세요.
  1. 다음으로 Cloud 리소스/API를 사용하려면 Cloud 콘솔에서 결제를 사용 설정해야 합니다. 이 Codelab 실행에는 많은 비용이 들지 않습니다. 이 튜토리얼이 끝난 후에 요금이 청구되지 않도록 리소스를 종료하려면 만든 리소스를 삭제하거나 전체 프로젝트를 삭제하면 됩니다. Google Cloud 새 사용자에게는 미화 $300 상당의 무료 체험판 프로그램에 참여할 수 있는 자격이 부여됩니다.

Cloud Shell 활성화

  1. Cloud Console에서 Cloud Shell 활성화853e55310c205094.png를 클릭합니다.

55efc1aaa7a4d3ad.png

이전에 Cloud Shell을 시작한 적이 없는 경우 기능을 설명하는 중간 화면 (스크롤해야 볼 수 있는 부분)이 표시됩니다. 이 경우 계속을 클릭하세요 (다시 표시되지 않음). 이 일회성 화면은 다음과 같습니다.

92662c6a846a5c.png

Cloud Shell을 프로비저닝하고 연결하는 데 몇 분 정도만 걸립니다.

9f0e51b578fecce5.png

가상 머신에는 필요한 개발 도구가 모두 들어 있습니다. 영구적인 5GB 홈 디렉터리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 이 Codelab에서 대부분의 작업은 브라우저나 Chromebook만 사용하여 수행할 수 있습니다.

Cloud Shell에 연결되면 인증이 완료되었고 프로젝트가 해당 프로젝트 ID로 이미 설정된 것을 볼 수 있습니다.

  1. Cloud Shell에서 다음 명령어를 실행하여 인증되었는지 확인합니다.
gcloud auth list

명령어 결과

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Cloud Shell에서 다음 명령어를 실행하여 gcloud 명령어가 프로젝트를 알고 있는지 확인합니다.
gcloud config list project

명령어 결과

[core]
project = <PROJECT_ID>

또는 다음 명령어로 설정할 수 있습니다.

gcloud config set project <PROJECT_ID>

명령어 결과

Updated property [core/project].

환경 설정

이 실습에서는 Cloud Shell 명령줄에서 명령어를 실행합니다. 일반적으로 명령어를 복사하여 그대로 붙여넣을 수 있지만 경우에 따라 자리표시자 값을 올바르게 변경해야 할 수도 있습니다.

  1. 이후 명령어에서 사용할 수 있도록 환경 변수를 프로젝트 ID로 설정합니다.
export PROJECT_ID=$(gcloud config get-value project)
export REGION=us-central1
export SERVICE_NAME=partner-registration-service
  1. 앱을 실행할 Cloud Run 서비스 API, NoSQL 데이터 스토리지를 제공하는 Firestore API, 배포 명령어에 사용할 Cloud Build API, 빌드 시 애플리케이션 컨테이너를 보관하는 데 사용할 Artifact Registry를 사용 설정합니다.
gcloud services enable \
  run.googleapis.com \
  firestore.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com
  1. 기본 모드로 Firestore 데이터베이스를 초기화합니다. 이 명령어는 App Engine API를 사용하므로 먼저 사용 설정해야 합니다.

명령어는 App Engine의 리전을 지정해야 합니다. 이 리전은 사용하지 않지만 기록상의 이유로 만들어야 하며, 데이터베이스의 리전도 지정해야 합니다. App Engine에는 us-central을 사용하고 데이터베이스에는 nam5를 사용합니다. nam5는 미국 멀티 리전 위치입니다. 멀티 리전 위치는 데이터베이스의 가용성과 내구성을 최대화합니다.

gcloud services enable appengine.googleapis.com

gcloud app create --region=us-central
gcloud firestore databases create --region=nam5
  1. 샘플 앱 저장소를 클론하고 디렉터리로 이동합니다.
git clone https://github.com/GoogleCloudPlatform/cymbal-eats.git

cd cymbal-eats/partner-registration-service

3. README 검토

편집기를 열고 앱을 구성하는 파일을 살펴봅니다. 이 앱을 배포하는 데 필요한 단계를 설명하는 README.md를 확인합니다. 이러한 단계 중 일부에는 고려해야 할 암시적 또는 명시적 보안 결정이 포함될 수 있습니다. 배포된 앱의 보안을 개선하기 위해 여기에 설명된 대로 이러한 선택사항 중 몇 가지를 변경할 예정입니다.

3단계 - npm install 실행

앱에 사용된 모든 서드 파티 소프트웨어의 출처와 무결성을 아는 것이 중요합니다. 소프트웨어 공급망 보안 관리는 Cloud Run에 배포된 앱뿐만 아니라 모든 소프트웨어 빌드와도 관련이 있습니다. 이 실습에서는 배포에 중점을 두었으므로 이 영역은 다루지 않지만 주제에 대해 별도로 조사하는 것이 좋습니다.

4단계 및 5단계 - deploy.sh 수정 및 실행

이 단계에서는 대부분의 옵션을 기본값으로 두고 앱을 Cloud Run에 배포합니다. 이 단계를 수정하여 다음 두 가지 방법으로 배포를 더욱 안전하게 보호합니다.

  1. 인증되지 않은 액세스를 허용하면 안 됩니다. 탐색 중에 시험해 보기 위해 허용하는 것이 편리할 수 있지만 상업적 파트너가 사용할 수 있는 웹 서비스이므로 항상 사용자를 인증해야 합니다.
  2. 애플리케이션에서 필요한 것보다 더 많은 API 및 리소스 액세스 권한을 가질 수 있는 기본 계정 대신 필요한 권한만 있는 전용 전용 서비스 계정을 사용해야 한다고 지정합니다. 이를 최소 권한의 원칙이라고 하며 애플리케이션 보안의 기본 개념입니다.

6~11단계 - 샘플 웹 요청을 만들어 올바른 동작 확인

이제 애플리케이션 배포에는 인증이 필요하므로 이러한 요청에는 이제 요청자의 신원에 대한 증명이 포함되어야 합니다. 이러한 파일을 변경하는 대신 명령줄에서 직접 요청할 수 있습니다.

4. 안전하게 서비스 배포

deploy.sh 스크립트에서 필요에 따라 확인된 두 가지 변경사항이 있습니다. 인증되지 않은 액세스를 허용하지 않는 것이고, 다른 하나는 최소한의 권한이 있는 전용 서비스 계정을 사용하는 것입니다.

먼저 새 서비스 계정을 만든 후 deploy.sh 스크립트를 수정하여 해당 서비스 계정을 참조하고 인증되지 않은 액세스를 허용하지 않습니다.그런 다음 수정된 스크립트를 실행하여 서비스를 배포한 후 수정된 deploy.sh 스크립트를 실행합니다.

서비스 계정을 만들고 필요한 액세스 권한을 Firestore/Datastore에 부여합니다.

gcloud iam service-accounts create partner-sa

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:partner-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role=roles/datastore.user

deploy.sh 수정

deploy.sh 파일을 수정하여 인증되지 않은 액세스(–no-allow-unauthenticated)를 허용하고 배포된 앱의 새 서비스 계정(–service-account)을 지정합니다. GOOGLE_PROJECT_ID를 내 프로젝트 ID로 수정합니다.

처음 두 줄을 삭제하고 아래와 같이 다른 세 줄을 변경합니다.

gcloud run deploy $SERVICE_NAME \
  --source . \
  --platform managed \
  --region ${REGION} \
  --no-allow-unauthenticated \
  --project=$PROJECT_ID \
  --service-account=partner-sa@${PROJECT_ID}.iam.gserviceaccount.com

서비스 배포

명령줄에서 deploy.sh 스크립트를 실행합니다.

./deploy.sh

배포가 완료되면 명령어 결과의 마지막 줄에 새 앱의 서비스 URL이 표시됩니다. 환경 변수에 URL을 저장합니다.

export SERVICE_URL=<URL from last line of command output>

이제 curl 도구를 사용하여 앱에서 주문을 가져와 봅니다.

curl -i -X GET $SERVICE_URL/partners

curl 명령어의 -i 플래그는 출력에 응답 헤더를 포함하도록 지시합니다. 출력의 첫 번째 줄은 다음과 같습니다.

HTTP/2 403

앱이 인증되지 않은 요청을 허용되지 않는 옵션과 함께 배포되었습니다. 이 curl 명령어에는 인증 정보가 포함되어 있지 않으므로 Cloud Run에서 거부합니다. 실제로 배포된 애플리케이션은 이 요청에서 데이터를 실행하거나 수신하지도 않습니다.

5. 인증된 요청 수행

배포된 앱은 웹 요청을 통해 호출됩니다. Cloud Run에서 이를 허용하려면 웹 요청을 인증해야 합니다. 다음과 같은 형식의 Authorization 헤더를 포함하여 웹 요청을 인증합니다.

Authorization: Bearer identity-token

ID 토큰은 신뢰할 수 있는 인증 제공업체가 발급한, 암호화 서명 및 인코딩된 짧은 문자열입니다. 이 경우 만료되지 않은 유효한 Google 발급 ID 토큰이 필요합니다.

사용자 계정으로 요청하기

Google Cloud CLI 도구는 기본 인증된 사용자의 토큰을 제공할 수 있습니다. 다음 명령어를 실행하여 계정의 ID 토큰을 가져오고 ID_TOKEN 환경 변수에 저장합니다.

export ID_TOKEN=$(gcloud auth print-identity-token)

기본적으로 Google에서 발급한 ID 토큰은 1시간 동안 유효합니다. 다음 curl 명령어를 실행하여 이전에 거부된 요청이 승인되지 않았기 때문에 요청을 생성합니다. 이 명령어에는 필요한 헤더가 포함됩니다.

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $ID_TOKEN"

명령어 결과는 요청이 허용되고 처리 중임을 나타내는 HTTP/2 200로 시작해야 합니다. 한 시간 후에 이 요청을 다시 시도하면 토큰이 만료되었으므로 실패합니다. 응답 본문은 출력 끝의 빈 줄 뒤에 있습니다.

{"status":"success","data":[]}

아직 파트너가 없습니다.

두 개의 curl 명령어와 함께 디렉터리의 샘플 JSON 데이터를 사용하여 파트너를 등록합니다.

curl -X POST \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d "@example-partner.json" \
  $SERVICE_URL/partner

curl -X POST \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d "@example-partner2.json" \
  $SERVICE_URL/partner

이전의 GET 요청을 반복하여 이제 등록된 모든 파트너를 확인합니다.

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $ID_TOKEN"

훨씬 더 많은 콘텐츠가 포함된 JSON 데이터가 표시되어 등록된 두 파트너에 관한 정보를 확인할 수 있습니다.

승인되지 않은 계정으로 요청

이전 단계에서 인증된 요청은 인증되었을 뿐 아니라 인증된 사용자 (귀하의 계정)가 승인되었기 때문에 성공적으로 이루어졌습니다. 즉, 계정에 앱을 호출할 권한이 있습니다. 인증된 모든 계정이 이러한 작업을 할 수 있는 것은 아닙니다.

이전 요청에 사용된 기본 계정은 앱이 포함된 프로젝트를 만들었고 이 계정에 기본적으로 계정의 Cloud Run 애플리케이션을 호출할 수 있는 권한을 부여했기 때문에 승인되었습니다. 필요한 경우 이 권한을 취소할 수 있으며, 프로덕션 애플리케이션에서는 이 권한이 바람직합니다. 지금 하는 대신 할당된 권한이나 역할이 없는 새 서비스 계정을 만들어 배포된 앱에 액세스해 봅니다.

  1. tester라는 서비스 계정을 만듭니다.
gcloud iam service-accounts create tester
  1. 새 계정에 대한 ID 토큰은 이전에 기본 계정에 대해 발급받은 것과 거의 동일한 방식으로 받게 됩니다. 하지만 이 경우 기본 계정에 서비스 계정을 가장할 수 있는 권한이 있어야 합니다. 계정에 이 권한을 부여합니다.
export USER_EMAIL=$(gcloud config list account --format "value(core.account)")

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="user:$USER_EMAIL" \
  --role=roles/iam.serviceAccountTokenCreator
  1. 이제 다음 명령어를 실행하여 TEST_IDENTITY 환경 변수에 이 새 계정의 ID 토큰을 저장합니다. 명령어에 오류 메시지가 표시되면 1~2분 정도 기다린 후 다시 시도하세요.
export TEST_TOKEN=$( \
  gcloud auth print-identity-token \
    --impersonate-service-account \
    "tester@$PROJECT_ID.iam.gserviceaccount.com" \
)
  1. 이전과 마찬가지로 다음 ID 토큰을 사용하여 인증된 웹 요청을 수행합니다.
curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $TEST_TOKEN"

요청이 인증되었지만 승인되지 않았으므로 명령어 결과는 다시 HTTP/2 403로 시작됩니다. 새 서비스 계정에 이 앱을 호출할 권한이 없습니다.

계정 승인

사용자 또는 서비스 계정에 Cloud Run 요청을 보내려면 해당 서비스에 대한 Cloud Run 호출자 역할이 있어야 합니다. 다음 명령어를 사용하여 테스터 서비스 계정에 해당 역할을 부여합니다.

export REGION=us-central1
gcloud run services add-iam-policy-binding ${SERVICE_NAME} \
  --member="serviceAccount:tester@$PROJECT_ID.iam.gserviceaccount.com" \
  --role=roles/run.invoker \
  --region=${REGION}

새 역할이 업데이트될 때까지 1~2분 정도 기다린 후 인증된 요청을 반복합니다. 처음 저장한 후 1시간 이상 지났다면 새 TEST_TOKEN을 저장합니다.

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $TEST_TOKEN"

이제 명령어 결과가 HTTP/1.1 200 OK로 시작되며 마지막 줄에 JSON 응답이 포함됩니다. 이 요청은 Cloud Run에서 수락되고 앱에서 처리되었습니다.

6. 프로그램 인증과 사용자 인증 비교

지금까지 작성한 인증된 요청은 curl 명령줄 도구를 사용했습니다. 대신 사용되었을 수 있는 다른 도구와 프로그래밍 언어가 있습니다. 하지만 일반 웹페이지가 있는 웹브라우저를 사용하여 인증된 Cloud Run을 요청할 수는 없습니다. 사용자가 링크를 클릭하거나 버튼을 클릭하여 웹페이지에서 양식을 제출하면 브라우저가 인증된 요청에 Cloud Run에 필요한 Authorization 헤더를 추가하지 않습니다.

Cloud Run의 기본 제공 인증 메커니즘은 최종 사용자가 아닌 프로그램에서 사용하기 위한 것입니다.

참고:

Cloud Run은 사용자 대상 웹 애플리케이션을 호스팅할 수 있지만 이러한 종류의 애플리케이션은 사용자의 인증되지 않은 요청을 허용하도록 Cloud Run을 설정해야 합니다. 웹 브라우저입니다. 애플리케이션에 사용자 인증이 필요한 경우 애플리케이션은 Cloud Run에 사용자 인증을 요청하는 대신 이를 처리해야 합니다. 애플리케이션은 Cloud Run 외부의 웹 애플리케이션과 동일한 방식으로 이 작업을 수행할 수 있습니다. 그 방법은 이 Codelab에서 다루지 않습니다.

지금까지 예시 요청에 대한 응답은 웹페이지가 아니라 JSON 객체였습니다. 이 파트너 등록 서비스는 프로그램에서 사용하기 위한 것이고 JSON은 프로그램에서 사용하기에 편리한 형식이기 때문입니다. 다음으로 이 데이터를 사용하고 사용하기 위한 프로그램을 작성하고 실행합니다.

Python 프로그램의 인증된 요청

프로그램은 표준 HTTP 웹 요청을 통해 Authorization 헤더를 포함하여 안전한 Cloud Run 애플리케이션의 인증된 요청을 할 수 있습니다. 이러한 프로그램에서 유일하게 해결해야 할 새로운 과제는 해당 헤더에 배치할 유효한 만료되지 않은 ID 토큰을 구하는 것입니다. 이 토큰은 Google Cloud Identity and Access Management (IAM)를 사용하여 Cloud Run에서 검증하므로 IAM에서 인식하는 기관에서 토큰을 발급하고 서명해야 합니다. 프로그램에서 이러한 토큰의 발급을 요청하는 데 사용할 수 있는 클라이언트 라이브러리가 여러 언어로 제공됩니다. 이 예시에서 사용할 클라이언트 라이브러리는 Python google.auth입니다. 일반적으로 웹 요청을 하기 위한 몇 가지 Python 라이브러리가 있습니다. 이 예에서는 인기 있는 requests 모듈을 사용합니다.

첫 번째 단계는 두 개의 클라이언트 라이브러리를 설치하는 것입니다.

pip install google-auth
pip install requests

기본 사용자에 대한 ID 토큰을 요청하는 Python 코드는 다음과 같습니다.

credentials, _ = google.auth.default()
credentials.refresh(google.auth.transport.requests.Request())
identity_token = credentials.id_token

Cloud Shell과 같은 명령어 셸 또는 컴퓨터에서 표준 터미널 셸을 사용하는 경우 기본 사용자는 셸 내에서 인증된 사용자입니다. Cloud Shell에서는 일반적으로 Google에 로그인한 사용자입니다. 다른 경우에는 사용자가 gcloud auth login 또는 다른 gcloud 명령어로 인증된 것입니다. 사용자가 로그인한 적이 없는 경우 기본 사용자가 없으며 이 코드는 실패합니다.

다른 프로그램을 요청하는 프로그램의 경우 일반적으로 사용자의 ID가 아니라 요청하는 프로그램의 ID를 사용합니다. 이러한 경우에 사용하는 것이 바로 서비스 계정입니다. Cloud Firestore와 같은 API 요청 시 사용하는 ID를 제공하는 전용 서비스 계정으로 Cloud Run 서비스를 배포했습니다. 프로그램이 Google Cloud 플랫폼에서 실행되면 클라이언트 라이브러리는 자동으로 할당된 서비스 계정을 기본 ID로 사용하므로 두 경우 모두 동일한 프로그램 코드가 작동합니다.

추가된 승인 헤더를 사용하여 요청하는 Python 코드는 다음과 같습니다.

auth_header = {"Authorization": "Bearer " + identity_token}
response = requests.get(url, headers=auth_header)

다음 전체 Python 프로그램은 Cloud Run 서비스에 인증된 요청을 보내 등록된 모든 파트너를 검색한 다음 파트너의 이름과 할당된 ID를 출력합니다. 아래 명령어를 복사하고 실행하여 이 코드를 print_partners.py 파일에 저장합니다.

cat > ./print_partners.py << EOF
def print_partners():
    import google.auth
    import google.auth.transport.requests
    import requests

    credentials, _ = google.auth.default()
    credentials.refresh(google.auth.transport.requests.Request())
    identity_token = credentials.id_token

    auth_header = {"Authorization": "Bearer " + identity_token}
    response = requests.get("${SERVICE_URL}/partners", headers=auth_header)

    parsed_response = response.json()
    partners = parsed_response["data"]

    for partner in partners:
        print(f"{partner['partnerId']}: {partner['name']}")


print_partners()
EOF

셸 명령어로 이 프로그램을 실행합니다. 먼저 기본 사용자로 인증해야 프로그램에서 이러한 사용자 인증 정보를 사용할 수 있습니다. 아래의 gcloud auth 명령어를 실행합니다.

gcloud auth application-default login

안내에 따라 로그인을 완료합니다. 그런 다음 명령줄에서 프로그램을 실행합니다.

python print_partners.py

출력은 다음과 같이 표시됩니다.

10102: Zippy food delivery
67292: Foodful

사용자 ID로 인증되었고 이 프로젝트의 소유자이므로 기본적으로 프로그램을 실행할 권한이 있으므로 프로그램의 요청이 Cloud Run 서비스에 도달했습니다. 이 프로그램은 서비스 계정의 ID로 실행되는 것이 더 일반적입니다. Cloud Run 또는 App Engine과 같은 대부분의 Google Cloud 제품에서 실행되는 경우 기본 ID는 서비스 계정이 되고 개인 계정 대신 사용됩니다.

7. 축하합니다.

축하합니다. Codelab을 완료했습니다.

다음 단계:

다른 Cymbal Eats Codelab 살펴보기:

삭제

이 튜토리얼에서 사용된 리소스 비용이 Google Cloud 계정에 청구되지 않도록 하려면 리소스가 포함된 프로젝트를 삭제하거나 프로젝트를 유지하고 개별 리소스를 삭제하세요.

프로젝트 삭제

비용이 청구되지 않도록 하는 가장 쉬운 방법은 튜토리얼에서 만든 프로젝트를 삭제하는 것입니다.