Cloud Functions を別のプロジェクトから内部トラフィック扱いで実行する

通常、Cloud Functions を内部トラフィックからの呼び出しに限定したい場合は、Cloud Functions と呼び出し元(例えば GCE インスタンス)のプロジェクトが同一であることが必要です。
しかし、VPC Service Controls を使って Cloud Functions を境界の保護対象にすることで、Cloud Functions とその呼び出し元のプロジェクトが異なる場合でも、内部トラフィック扱いで実行することが可能になります。

Cloud Functions のアクセス制限

Cloud Functions のアクセス制限には大別してふたつの方法があります。

  • Identity-based
  • Network-based

cloud.google.com

詳しい説明は上記の公式ドキュメントを参照頂くと良いかと思いますが、簡単にまとめると以下のようになります。

  • アイデンティティ(サービスアカウントなど)に関数を呼び出す権限が必要
  • 関数の呼び出し元ネットワークを上り (Ingress) の設定で制限できる

関数の呼び出し元のネットワークが自身の組織やプロジェクトに限定される場合は内部トラフィックからの呼び出しのみを許可することで、関数のエンドポイントへの外部からのアクセスを防止することができます。
また、今回は触れませんが、関数から VPC への下り (Egress) の通信を行いたい場合は Serverless VPC Access Connector を利用します。

実装

Cloud Functions とその呼び出し元のプロジェクトが異なる環境を作り、VPC Service Controls を使ってプロジェクトをまたいで関数を内部トラフィックとして実行するまでの流れを示します。

1. プロジェクトの作成

まずは 2 つプロジェクトを作成します。
これらは同一の組織内に作成します。

$ gcloud projects list
PROJECT_ID: bar-127298
NAME: bar-127298
PROJECT_NUMBER: 785238314710

PROJECT_ID: foo-192847
NAME: foo-192847
PROJECT_NUMBER: 865726560248

2. GCE インスタンスの作成

関数の呼び出し元となる GCE インスタンスを各プロジェクトに作成します。

cloudshell:~$ gcloud compute instances list --project foo-192847
NAME: instance-foo
ZONE: us-central1-a
MACHINE_TYPE: e2-medium
PREEMPTIBLE:
INTERNAL_IP: 10.128.0.3
EXTERNAL_IP: 35.239.219.39
STATUS: RUNNING

cloudshell:~$ gcloud compute instances describe instance-foo --project foo-192847 --zone us-central1-a
...
serviceAccounts:
- email: sa-instance-foo@foo-192847.iam.gserviceaccount.com
...

cloudshell:~$ gcloud compute instances list --project bar-127298
NAME: instance-bar
ZONE: us-central1-a
MACHINE_TYPE: e2-medium
PREEMPTIBLE:
INTERNAL_IP: 10.128.0.3
EXTERNAL_IP: 34.69.40.102
STATUS: RUNNING

$ gcloud compute instances describe instance-bar --project bar-127298 --zone us-central1-a
...
serviceAccounts:
- email: sa-instance-bar@bar-127298.iam.gserviceaccount.com
...

3. Cloud Functions の作成

Cloud Functions の関数を一方のプロジェクト (foo-192847) に作成します。
関数の呼び出し元は 内部トラフィックのみ (ALLOW_INTERNAL_ONLY) に制限します。

cloudshell:~ (foo-192847)$ gcloud functions describe function-1
availableMemoryMb: 256
buildId: fc93c012-9859-4fc2-aa85-099012dc32c7
buildName: projects/865726560248/locations/us-central1/builds/fc93c012-9859-4fc2-aa85-099012dc32c7
dockerRegistry: CONTAINER_REGISTRY
entryPoint: helloWorld
httpsTrigger:
  securityLevel: SECURE_ALWAYS
  url: https://us-central1-foo-192847.cloudfunctions.net/function-1
ingressSettings: ALLOW_INTERNAL_ONLY
labels:
  deployment-tool: console-cloud
maxInstances: 3000
name: projects/foo-192847/locations/us-central1/functions/function-1
runtime: nodejs16
serviceAccountEmail: foo-192847@appspot.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/uploads-620971246542.us-central1.cloudfunctions.appspot.com/0be327c3-5853-40ae-96f1-766c3fccc3d7.zip
status: ACTIVE
timeout: 60s
updateTime: '2022-04-05T13:04:10.169Z'
versionId: '1'

各 GCE のサービスアカウントに関数の呼び出し権限を付与します。

$ gcloud functions get-iam-policy function-1
bindings:
- members:
  - serviceAccount:sa-instance-bar@bar-127298.iam.gserviceaccount.com
  - serviceAccount:sa-instance-foo@foo-192847.iam.gserviceaccount.com
  role: roles/cloudfunctions.invoker
etag: BwXb6E6wnRg=
version: 1

4. ネットワークによるアクセス制限の確認

この時点では、同じプロジェクト (foo-192847) に含まれる instance-foo からの HTTP トリガのみが内部トラフィックと判定されます。
HTTP トリガで関数を呼び出す際、呼び出し元は認証情報を明示的に指定する必要があるため、Authorization ヘッダに ID トークンを指定して関数の実行をリクエストしています。

参考:https://cloud.google.com/functions/docs/securing/authenticating

instance-foo:~$ gcloud config list
account = sa-instance-foo@foo-192847.iam.gserviceaccount.com
disable_usage_reporting = True
project = foo-192847

instance-foo:~$ curl -H "Authorization: bearer $(gcloud auth print-identity-token)" https://us-central1-foo-192847.cloudfunctions.net/function-1
Hello World!

Cloud Functions のデプロイされているプロジェクトとは別のプロジェクト (bar-127298) のインスタンス instance-bar は、IAM ポリシによる権限付与がされているものの、ネットワークによるアクセス制限を受けているため実行できません。

参考:https://cloud.google.com/functions/docs/troubleshooting#internal-traffic

instance-bar:~$ gcloud config list
account = sa-instance-bar@bar-127298.iam.gserviceaccount.com
disable_usage_reporting = True
project = bar-127298

instance-bar:~$ curl -H "Authorization: bearer $(gcloud auth print-identity-token)" https://us-central1-foo-192847.cloudfunctions.net/function-1
<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>403 Forbidden</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Forbidden</h1>
<h2>Access is forbidden.</h2>
<h2></h2>
</body></html>

5. VPC Service Controls のサービス境界を作成

以下のように、2つのプロジェクトと Cloud Functions を指定して境界を作成します。

cloudshell:~$ gcloud access-context-manager perimeters describe dev_perimeter
name: accessPolicies/973808997062/servicePerimeters/dev_perimeter
status:
  resources:
  - projects/865726560248
  - projects/785238314710
  restrictedServices:
  - cloudfunctions.googleapis.com
  vpcAccessibleServices: {}
title: dev_perimeter

境界が有効になったことを確認してみます。
例えば、Cloud Functions のユーザアカウントによるテスト実行は境界外からのアクセスとなるため、VPC Service Controls により実行不可になっていることがわかります。
なお、実行したい場合は境界への Ingress ルールでユーザアカウントからの API アクセスを許可する必要があります。

f:id:polar3130:20220406002249p:plain

境界が有効になったので bar-127298 プロジェクトのインスタンス instance-bar からも内部トラフィック扱いで foo-192847 プロジェクトの関数を呼び出すことができるようになりました。
関数の実行をリクエストして確認します。

instance-bar:~$ curl -H "Authorization: bearer $(gcloud auth print-identity-token)" https://us-central1-foo-192847.cloudfunctions.net/function-1
Hello World!

おわりに

VPC Service Controls を使って Cloud Functions を境界の保護対象にすることで、Cloud Functions とその呼び出し元のプロジェクトが異なる場合でも、内部トラフィック扱いで実行する方法をご紹介しました。
理由は様々ですが Cloud Functions と呼び出し元を同一のプロジェクトに配置できないケースはそれなりにあると思いますので、そうした場合でもネットワークベースのアクセス制限を行えることがわかりました。