1. 개요
Cloud Run에 서비스를 배포하는 기본 단계를 수정하여 보안을 개선한 다음 배포된 앱에 안전하게 액세스하는 방법을 알아봅니다. 이 앱은 Cymbal Eats와 협력하여 음식 주문을 처리하는 회사에서 사용하는 Cymbal Eats 애플리케이션의 '파트너 등록 서비스'입니다.
학습할 내용
Cloud Run에 앱을 배포하기 위한 최소 기본 단계에서 몇 가지 작은 변경사항을 적용하면 보안을 크게 강화할 수 있습니다. 기존 앱과 배포 안내를 사용하여 배포된 앱의 보안을 개선하도록 배포 단계를 변경합니다.
그런 다음 앱에 대한 액세스를 승인하고 승인된 요청을 만드는 방법을 확인합니다.
애플리케이션 배포 보안을 자세히 살펴보는 것은 아니며, 향후 모든 앱 배포에 적용하여 아주 적은 노력으로 보안을 개선할 수 있는 변경사항을 살펴봅니다.
2. 설정 및 요구사항
자습형 환경 설정
- Google Cloud Console에 로그인하여 새 프로젝트를 만들거나 기존 프로젝트를 재사용합니다. 아직 Gmail이나 Google Workspace 계정이 없는 경우 계정을 만들어야 합니다.



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

이전에 Cloud Shell을 시작하지 않았으면 설명이 포함된 중간 화면 (스크롤해야 볼 수 있는 부분)이 제공됩니다. 이 경우 계속을 클릭합니다 (이후 다시 표시되지 않음). 이 일회성 화면은 다음과 같습니다.

Cloud Shell을 프로비저닝하고 연결하는 작업은 몇 분이면 끝납니다.

이 가상 머신에는 필요한 개발 도구가 모두 포함되어 로드됩니다. 영구적인 5GB 홈 디렉터리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 이 Codelab에서 대부분의 작업은 브라우저나 Chromebook만 사용하여 수행할 수 있습니다.
Cloud Shell에 연결되면 인증이 완료되었고 프로젝트가 해당 프로젝트 ID로 이미 설정된 것을 볼 수 있습니다.
- 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`
- Cloud Shell에서 다음 명령어를 실행하여 gcloud 명령어가 프로젝트를 알고 있는지 확인합니다.
gcloud config list project
명령어 결과
[core] project = <PROJECT_ID>
또는 다음 명령어로 설정할 수 있습니다.
gcloud config set project <PROJECT_ID>
명령어 결과
Updated property [core/project].
환경 설정
이 실습에서는 Cloud Shell 명령줄에서 명령어를 실행합니다. 일반적으로 명령어를 복사하여 그대로 붙여넣을 수 있지만, 경우에 따라 자리표시자 값을 올바른 값으로 변경해야 합니다.
- 이후 명령어에서 사용할 프로젝트 ID의 환경 변수를 설정합니다.
export PROJECT_ID=$(gcloud config get-value project)
export REGION=us-central1
export SERVICE_NAME=partner-registration-service
- 앱을 실행할 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
- Native 모드로 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
- 샘플 앱 저장소를 클론하고 디렉터리로 이동합니다.
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에 배포합니다. 이 단계를 수정하여 다음과 같은 두 가지 주요 방법으로 배포를 더 안전하게 만듭니다.
- 인증되지 않은 액세스를 허용하지 마세요. 탐색 중에 여러 가지를 시도해 볼 수 있도록 허용하는 것이 편리할 수 있지만, 이는 상업 파트너가 사용하는 웹 서비스이므로 항상 사용자를 인증해야 합니다.
- 애플리케이션이 필요 이상으로 많은 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 수정
인증되지 않은 액세스를 허용하지 않도록(–no-allow-unauthenticated) deploy.sh 파일을 수정하고 배포된 앱의 새 서비스 계정(–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":[]}
아직 파트너가 없습니다.
디렉터리의 샘플 JSON 데이터를 사용하여 다음 두 curl 명령어로 파트너를 등록합니다.
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 애플리케이션을 호출할 수 있는 권한이 부여되었습니다. 필요한 경우 이 권한을 취소할 수 있으며 이는 프로덕션 애플리케이션에서 바람직합니다. 지금 그렇게 하는 대신 권한이나 역할이 할당되지 않은 새 서비스 계정을 만들고 이를 사용하여 배포된 앱에 액세스해 보겠습니다.
tester라는 서비스 계정을 만듭니다.
gcloud iam service-accounts create tester
- 이 새 계정의 ID 토큰은 이전 기본 계정의 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
- 이제 다음 명령어를 실행하여 이 새 계정의 ID 토큰을 TEST_IDENTITY 환경 변수에 저장합니다. 명령어에 오류 메시지가 표시되면 1~2분 정도 기다린 후 다시 시도하세요.
export TEST_TOKEN=$( \
gcloud auth print-identity-token \
--impersonate-service-account \
"tester@$PROJECT_ID.iam.gserviceaccount.com" \
)
- 이 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 웹 요청을 통해 보안 Cloud Run 애플리케이션의 인증된 요청을 할 수 있지만 Authorization 헤더를 포함합니다. 이러한 프로그램의 유일한 새로운 과제는 해당 헤더에 배치할 유효하고 만료되지 않은 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로 자동으로 사용하므로 동일한 프로그램 코드가 두 상황에서 모두 작동합니다.
Authorization 헤더가 추가된 요청을 만드는 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을 살펴보세요.
- Eventarc로 Cloud Workflows 트리거
- Cloud Storage에서 이벤트 처리 트리거
- Cloud Run에서 비공개 CloudSQL에 연결
- Cloud Run에서 완전 관리형 데이터베이스에 연결
- IAP (Identity-Aware Proxy)로 서버리스 애플리케이션 보호
- Cloud Scheduler로 Cloud Run 작업 트리거
- Cloud Run 인그레스 트래픽 보안
- GKE Autopilot에서 비공개 AlloyDB에 연결
삭제
이 튜토리얼에서 사용된 리소스 비용이 Google Cloud 계정에 청구되지 않도록 하려면 리소스가 포함된 프로젝트를 삭제하거나 프로젝트를 유지하고 개별 리소스를 삭제하세요.
프로젝트 삭제
비용이 청구되지 않도록 하는 가장 쉬운 방법은 튜토리얼에서 만든 프로젝트를 삭제하는 것입니다.