GKEでWebアプリを公開してCloud Armorでアクセス制限してみた

記事タイトルとURLをコピーする

G-gen の出口です。当記事では、Google Kubernetes Engine(GKE)にデプロイした Web アプリケーション Ingress でインターネット公開する方法、またそのアプリに Cloud Armor ポリシーを設定して、アクセス元 IP アドレスを制限する方法を解説します。

概要

検証の概要

当記事で検証した構成は、以下のとおりです。

この構成では、GKE 上で動作するアプリケーションが、Firestore に保存されたユーザー情報を、入力されたユーザ ID に基づいて表示します。GKE クラスタに対しては、 Ingressリソースとして外部アプリケーションロードバランサを作成します。そのロードバランサに対して、Cloud Armor を用いて IP アドレスによるアクセス制限を適用します。

Google Kubernetes Engine とは

Google Kubernetes Engine(以下、GKE)は、Google Cloud(旧称 GCP)が提供する Kubernetes のマネージドサービスです。

以下の記事で GKE について解説していますので、ご参照ください。

blog.g-gen.co.jp

Ingress とは

Ingress は、Kubernetes クラスタ外部からクラスタ内部への HTTP(S) アクセスを管理する Kubernetes の API オブジェクトです。この Ingress と Service を組み合わせることで、Service の背後にある Pod にデプロイされているアプリケーションへ外部からアクセスできるようになります。

GKE には、組み込みの GKE Ingress コントローラが用意されています。GKE で Ingress リソースを作成すると、このコントローラによってアプリケーションロードバランサが GKE クラスタ外部に自動的に構成されます。

Cloud Armor とは

Cloud Armor は、Google Cloud が提供するフルマネージドの WAF(Web Application Firewall)サービスです。Cloud Armor のセキュリティポリシーを作成してアプリケーションロードバランサに関連付けることで、アクセス制限をかけることができます。

以下の記事で Cloud Armor について解説していますので、ご参照ください。

blog.g-gen.co.jp

Firestore の準備

データベースの作成

Firestore で (default) データベースを作成します。

(default) データベースは Firestore のデフォルトとなるデータベースで、Firestore クライアントライブラリや Google Cloud CLI から接続するときにデータベース ID を指定しない場合、自動的に (default) データベースに接続されます。

gcloud firestore databases create \
--database="(default)" \
--location=asia-northeast1 \
--type=firestore-native

データの準備

作成した (default) データベースに、コレクション ID が users で、age , id , name という3つのフィールドを持っているドキュメントを追加します。

アプリケーションの準備

ソースコードの作成

GKE 上で動作するアプリケーションのソースコードを作成します。

ディレクトリ構成は以下の通りです。

.
├── main.py
├── requirements.txt
├── Dockerfile
└── templates
    └── index.html

main.py

ソースコードは以下のとおりです。

PROJECT_ID には、ご自身のプロジェクト ID を設定してください。

from flask import Flask, render_template, request
from google.cloud import firestore
PROJECT_ID = "プロジェクト ID に置き換えてください"
app = Flask(__name__)
# Firestore インスタンス初期化
db = firestore.Client(project=PROJECT_ID)
COLLECTION_NAME = "users" # Firestore コレクション名
ID_FIELD_NAME = "id" # Firestore IDフィールド名
@app.route("/", methods=["GET", "POST"])
def index():
data = None
message = None
if request.method == "POST":
try:
user_id = request.form.get("user_id")
if not user_id:
message = "IDを入力してください。"
return render_template('index.html', data=data, message=message)
doc_ref = db.collection(COLLECTION_NAME).where(ID_FIELD_NAME, "==", user_id).get()
if doc_ref:
data = doc_ref[0].to_dict()
else:
message = "指定されたIDのデータは見つかりませんでした。"
except Exception as e:
message = f"エラーが発生しました:{str(e)}"
return render_template("index.html", data=data, message=message)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=True)

templates/index.html

ユーザ ID を入力するフォームが搭載されていて、そのユーザ ID を Firestore で検索して受け取った結果を表示する HTML ファイルを作成します。

<!DOCTYPE html>
<html>
<head>
<title>Firestore Viewer</title>
</head>
<body>
<h1>Firestore Data Viewer</h1>
<form method="POST">
<label for="user_id">IDを入力:</label>
<input type="text" id="user_id" name="user_id">
<button type="submit">検索</button>
</form>
<br>
{% if data %}
<h2>データ:</h2>
<p>名前: {{ data.name }}</p>
<p>年齢: {{ data.age }}</p>
{% elif message %}
<p>{{ message }}</p>
{% endif %}
</body>
</html>

requirements.txt

使用するライブラリを、以下の通りに定義します。

Flask==3.1.0
google-cloud-firestore==2.19.0

Dockerfile

GKE へデプロイするために Docker イメージを用意する必要があるため、Dockerfile を作成します。

FROM python:3.12-slim
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD [ "python", "./main.py" ]

Artifact Registry の作成

Docker イメージを保存するための Artifact Registry 標準リポジトリを作成します。

以下のコマンドで、firestore-app という名前のリポジトリを作成します。

gcloud artifacts repositories create firestore-app \
--repository-format=docker \
--location=asia-northeast1

Artifact Registry にアップロード

Cloud Build を利用して Docker イメージをビルドし、作成した Artifact Registry にプッシュします。

プロジェクト ID に置き換えてください の部分を、ご自身のプロジェクト ID に置き換えて、Dockerfile を作成したディレクトリで以下のコマンドを実行します。

PROJECT="プロジェクト ID に置き換えてください"
gcloud builds submit --tag asia-northeast1-docker.pkg.dev/${PROJECT}/firestore-app/firestore-app

GKE クラスタの設定

GKE クラスタの作成

以下のコマンドで、firestore-app という名前の Autopilot モードの GKE クラスタを作成します。

Autopilot モードではノード、スケーリング、セキュリティといったクラスタ構成が Google Cloud によって管理されます。GKE のベスト プラクティスと推奨事項を遵守されたクラスタ構成が提供されます。

gcloud container clusters create-auto firestore-app \
--location=asia-northeast1 \
--project=${PROJECT}

サービスアカウントの設定

今回の構成では、GKE クラスタ上で実行されるワークロードから Firestore へのアクセスが必要になります。

GKE Autopilot モード では Workload Identity Federation が常に有効化されており、GKE の ServiceAccount リソースを IAM プリンシパルとして直接関連付けることができます。この機能を利用して、Firestore へのアクセス権を付与します。

Workload Identity Federation についてや、その他の認証方法については以下の記事で解説しておりますので、ご参照ください。

blog.g-gen.co.jp

blog.g-gen.co.jp

ServiceAccount リソースの作成

以下のコマンドで、Kubernetes ServiceAccout リソース firestore-app を作成します。

kubectl create serviceaccount firestore-app

権限の付与

作成した ServiceAccout を参照する IAM ポリシーを作成します。

以下のコマンドで、作成した ServiceAccout に対して Cloud Datastore 閲覧者 (roles/datastore.viewer) ロールを付与します。

プロジェクト番号に置き換えてください の部分を、ご自身のプロジェクト番号に置き換えてください。

PROJECT_NUMBER="プロジェクト番号に置き換えてください"
gcloud projects add-iam-policy-binding projects/${PROJECT} \
--role=roles/datastore.viewer \
--member=principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT}.svc.id.goog/subject/ns/default/sa/firestore-app

GKE クラスタにアプリケーションをデプロイ

以前の手順で Artifact Registry にアップロードしたコンテナイメージを使用して、アプリケーションを GKE クラスタに登録します。

以下のマニフェストファイル deployment.yaml を GKE クラスタに適用し、Deployment リソースを作成します。

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: firestore-app-deployment
namespace: default
labels:
app: firestore-app
spec:
replicas: 3
selector:
matchLabels:
app: firestore-app
template:
metadata:
labels:
app: firestore-app
spec:
serviceAccountName: firestore-app # 作成した ServiceAccout の名前
containers:
- name: firestore-app
image: asia-northeast1-docker.pkg.dev/(プロジェクトID)/firestore-app/firestore-app:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: 50m
memory: 52Mi

続いて、アプリケーションを公開するための Service を GKE クラスタに登録します。

以下のマニフェストファイル service.yaml を GKE クラスタに適用し、Service リソースを作成します。

# service.yaml
apiVersion: v1
kind: Service
metadata:
name: firestore-app-service
namespace: default
annotations:
cloud.google.com/neg: '{"ingress": true}'
spec:
selector:
app: firestore-app
ports:
- port: 8080
protocol: TCP
targetPort: 8080
type: NodePort

Ingress を設定してアプリケーションを公開

最初に Cloud Armor のセキュリティポリシーを構成せずに、アクセス元の制限を設定してしない状態で Ingress リソースを作成してみます。

以下のマニフェストファイル ingress.yaml を GKE クラスタに適用します。

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: firestore-app-ingress
namespace: default
annotations:
kubernetes.io/ingress.class: "gce"
spec:
defaultBackend:
service:
name: firestore-app-service
port:
number: 8080

今回の検証では、Ingress によって作成されるアプリケーションロードバランサは HTTP によってアクセスされます。

HTTPS を使用する場合は、SSL/TLS 証明書を Ingress と関連付ける必要があります。詳細は、以下の記事をご参照ください。

blog.g-gen.co.jp

Ingress によって作成されたアプリケーションロードバランサの外部 IP アドレスを確認し、アクセスすると Firestore Viewer というタイトルのアプリケーションの画面が表示されます。

Firestore に追加したユーザ ID を入力すると、そのユーザ ID に対応するユーザ情報が表示されることが確認できます。

Cloud Armor による アクセス制限を適用

Cloud Armor のセキュリティポリシーを設定

アクセス元の IP アドレスを制限する Cloud Armor のセキュリティポリシーを作成します。

まず以下のコマンドで、firestore-app-policy という名前のセキュリティポリシーを作成します。

gcloud compute security-policies create firestore-app-policy

次に、作成したセキュリティポリシーのデフォルトのルール (優先度 2,147,483,647) を更新し、すべてのアクセス元からのアクセスを制限します。

gcloud compute security-policies rules update 2147483647 \
--security-policy=firestore-app-policy \
--action="deny-403"

特定の IP アドレス範囲からのアクセスを許可するルールを作成します。

ルールの優先度はデフォルトのルールよりも高く設定します。以下の例では、優先度 1000 で設定しています。

gcloud compute security-policies rules create 1000 \
--security-policy=firestore-app-policy \
--src-ip-ranges="<許可する IP アドレス範囲>" \
--action="allow"

BackendConfig を作成

Ingress と Cloud Armor のセキュリティポリシーを関連付けるために、GKE のカスタムリソースである BackendConfig リソースを作成します。

以下のマニフェストファイル backend.yaml を GKE クラスタに適用します。

apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
name: firestore-app-backendconfig
spec:
securityPolicy:
name: firestore-app-policy # 作成したセキュリティポリシーの名前

Service と BackendConfig の関連付け

BackendConfig は Ingress リソースに対してではなく、 Service リソースの Service ポートに関連付けられます。GKE Ingress コントローラはアプリケーションロードバランサを作成する際に、この関連付けられた BackendConfig の設定を利用します。

マニフェストファイル service.yaml を以下のように更新して、 GKE クラスタに適用します。

# service.yaml
apiVersion: v1
kind: Service
metadata:
name: firestore-app-service
namespace: default
annotations:
cloud.google.com/neg: '{"ingress": true}'
cloud.google.com/backend-config: '{"ports": {"8080":"firestore-app-backendconfig"}}' # 追記
spec:
selector:
app: firestore-app
ports:
- port: 8080
protocol: TCP
targetPort: 8080
type: NodePort

動作確認

まず、Cloud Armor のセキュリティポリシーで許可していない IP アドレス範囲からアクセスしてみます。

Ingress によって作成されたアプリケーションロードバランサの外部 IP アドレスにアクセスすると、以下のようにアクセスが拒否されることが確認できます。

次に、ポリシーで許可された IP アドレス範囲からアクセスしてみます。

すると、以下のように Firestore Viewer アプリケーションの画面が表示されることが確認できます。

出口 晋太朗 (記事一覧)

クラウドソリューション部

2024年7月にG-genに入社。
福岡在住で、Google Cloud をマスターするため日々エンジニアとして修行中。