【Terraform】 第2世代の Cloud Run を利用したセキュアなアーキテクチャの実現
【Terraform】 第2世代の Cloud Run を利用したセキュアなアーキテクチャの実現
Hogetic Lab でデータエンジニアをしている大橋と申します。
今回は、第2世代の Cloud Run を使用して、シンプルな構成ながらセキュアなアーキテクチャを実現する方法について紹介します。
はじめに
現代のソフトウェア開発では、技術スタックやマイクロサービス化の進展に伴い、様々なAPIと連携する必要があります。また、APIの開発時には認証などの複雑な要素を後回しにして、まずは素早く機能を実装したいというニーズがあります。開発環境自体もコンテナ化されていることが一般的となっており、効率的なデプロイとスケーリングが求められます。
こうした背景から、セキュリティを確保しながら、コンテナ化されたアプリケーションを 簡単にクラウド上にデプロイする方法を検討することが、本システム構築のモチベーションとなっています。
システム構成の概要
今回のシステム構成は以下の通りです:
- API:Cloud Run 上で稼働するコンテナ化された API サーバー。
- フロントエンド:Cloud Run 上で稼働し、APIと通信するウェブアプリケーション。
- 専用 VPC:Cloud Run サービスをセキュアに接続するための専用の Virtual Private Cloud。
前提条件
- Terraform のインストール
- デプロイ先のプロジェクトの作成
- 必要な API の有効化
手順
1. プロジェクトのセットアップ
まず、GCP プロジェクトを作成し、必要な API を有効にします。
今回はテスト用のため、デプロイの際はオーナー権限を利用しています。
2. VPCの作成
専用の VPC を作成し、Cloud Runと接続するためのサブネットを設定します。
resource "google_compute_network" "demo" {
name = "demo"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "demo" {
name = "demo"
ip_cidr_range = "10.0.0.0/24"
region = "asia-northeast1"
network = google_compute_network.demo.id
}
4. Cloud Runサービスのデプロイ
内部 APIとフロントエンドのそれぞれに対して、Cloud Run サービスをデプロイします。ここでは例として、簡単な Python Flask アプリケーションを使用します。
API
今回作成した API サーバーは単純な JSON レスポンスを返すサーバーです。
社内でも PoC として開発している場合、認証などの機能は後回しにして機能開発を実施することがあるかと思います。
今回もそのような開発をイメージし、認証などの処理は行っていません。
サーバーコード
from flask import Flask, jsonify
import os
app = Flask(__name__)
@app.route('/', methods=['GET'])
def get_dummy_data():
dummy_data = {
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com",
"age": 30,
"address": {
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345"
}
}
return jsonify(dummy_data)
if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0", port=os.environ["PORT"])
Dockerfile
# ベースイメージとしてPythonを使用
FROM python:3.9.12
# 作業ディレクトリを作成
WORKDIR /app
# 必要なパッケージをインストール
COPY requirements.txt .
RUN pip install -r requirements.txt
ENV PYTHONUNBUFFERED True
# アプリケーションのソースコードをコピー
COPY . .
# アプリを起動
CMD ["python", "main.py"]
requirements.txt
flask
フロントエンド
サーバーコード
from flask import Flask, jsonify
import requests
import os
# API の URL を環境変数から取得
API_URL = os.environ["API_URL"]
app = Flask(__name__)
@app.route('/', methods=['GET'])
def get_external_data():
try:
response = requests.get(API_URL)
response.raise_for_status()
data = response.json()
return jsonify(data)
except requests.exceptions.RequestException as e:
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0", port=os.environ["PORT"])
Dockerfile
# ベースイメージとしてPythonを使用
FROM python:3.9.12
# 作業ディレクトリを作成
WORKDIR /app
# 必要なパッケージをインストール
COPY requirements.txt .
RUN pip install -r requirements.txt
ENV PYTHONUNBUFFERED True
# アプリケーションのソースコードをコピー
COPY . .
# アプリを起動
CMD ["python", "main.py"]
requirements.txt
flask
requests
イメージのビルドと Cloud Run の作成
null_resource
を利用して、変数として指定したイメージのタグの変更をトリガーとして利用して、イメージのビルドとプッシュを行います。
そして、その後作成したイメージを Cloud Run としてデプロイします。
Cloud Run は作成した VPC に直接接続し、認証無しで内部からのみアクセス出来るように構成します。
resource "null_resource" "frontend" {
triggers = {
image_version = "${var.frontend_image_tag}"
}
provisioner "local-exec" {
command = <<EOT
docker build -t asia-northeast1-docker.pkg.dev/${var.project}/${google_artifact_registry_repository.demo.repository_id}/frontend:${var.frontend_image_tag} \
-f ./frontend/Dockerfile ./frontend/
docker push asia-northeast1-docker.pkg.dev/${var.project}/${google_artifact_registry_repository.demo.repository_id}/frontend:${var.frontend_image_tag}
EOT
}
}
resource "null_resource" "backend" {
triggers = {
image_version = "${var.backend_image_tag}"
}
provisioner "local-exec" {
command = <<EOT
docker build -t asia-northeast1-docker.pkg.dev/${var.project}/${google_artifact_registry_repository.demo.repository_id}/backend:${var.backend_image_tag} \
-f ./backend/Dockerfile ./backend
docker push asia-northeast1-docker.pkg.dev/${var.project}/${google_artifact_registry_repository.demo.repository_id}/backend:${var.backend_image_tag}
EOT
}
}
resource "google_service_account" "demo" {
account_id = "demo-account"
}
resource "google_project_iam_member" "demo" {
project = var.project
role = "roles/run.admin"
member = "serviceAccount:${google_service_account.demo.email}"
}
resource "google_cloud_run_v2_service" "frontend" {
name = "frontend"
location = "asia-northeast1"
template {
service_account = google_service_account.demo.email
containers {
image = "asia-northeast1-docker.pkg.dev/${var.project}/${google_artifact_registry_repository.demo.repository_id}/frontend:${var.frontend_image_tag}"
resources {
limits = {
cpu = "1"
memory = "2Gi"
}
}
env {
name = "API_URL"
value = google_cloud_run_v2_service.backend.uri
}
}
vpc_access {
egress = "ALL_TRAFFIC"
network_interfaces {
network = google_compute_network.demo.name
subnetwork = google_compute_subnetwork.demo.name
}
}
}
traffic {
percent = 100
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
}
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
depends_on = [null_resource.frontend]
}
resource "google_cloud_run_service_iam_policy" "frontend" {
location = google_cloud_run_v2_service.frontend.location
service = google_cloud_run_v2_service.frontend.name
policy_data = data.google_iam_policy.frontend.policy_data
}
data "google_iam_policy" "frontend" {
binding {
role = "roles/run.invoker"
members = [
"allUsers",
]
}
}
resource "google_cloud_run_v2_service" "backend" {
name = "backend"
location = "asia-northeast1"
template {
service_account = google_service_account.demo.email
containers {
image = "asia-northeast1-docker.pkg.dev/${var.project}/${google_artifact_registry_repository.demo.repository_id}/backend:${var.backend_image_tag}"
}
vpc_access {
egress = "ALL_TRAFFIC"
network_interfaces {
network = google_compute_network.demo.name
subnetwork = google_compute_subnetwork.demo.name
}
}
}
traffic {
percent = 100
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
}
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
depends_on = [null_resource.backend]
}
resource "google_cloud_run_service_iam_policy" "backend" {
location = google_cloud_run_v2_service.backend.location
service = google_cloud_run_v2_service.backend.name
policy_data = data.google_iam_policy.backend.policy_data
}
data "google_iam_policy" "backend" {
binding {
role = "roles/run.invoker"
members = [
"allUsers",
]
}
}
5. Load Balancer の作成
作成した Cloud Run を Load Balancer に組み込み、フロントエンドのみ外部からアクセス出来るようにします。
フロントエンドと API の通信は、内部での通信となります。
resource "google_compute_region_network_endpoint_group" "frontend" {
name = "frontend"
network_endpoint_type = "SERVERLESS"
region = "asia-northeast1"
cloud_run {
service = google_cloud_run_v2_service.frontend.name
}
}
resource "google_compute_region_network_endpoint_group" "backend" {
name = "backend"
network_endpoint_type = "SERVERLESS"
region = "asia-northeast1"
cloud_run {
service = google_cloud_run_v2_service.backend.name
}
}
resource "google_compute_backend_service" "frontend" {
name = "frontend"
load_balancing_scheme = "EXTERNAL"
protocol = "HTTP"
backend {
group = google_compute_region_network_endpoint_group.frontend.id
}
lifecycle {
ignore_changes = [
iap,
]
}
}
resource "google_compute_backend_service" "backend" {
name = "backend"
load_balancing_scheme = "EXTERNAL"
protocol = "HTTP"
backend {
group = google_compute_region_network_endpoint_group.backend.id
}
}
resource "google_compute_url_map" "demo" {
name = "demo"
default_service = google_compute_backend_service.frontend.id
}
resource "google_compute_global_address" "demo" {
name = "demo"
}
resource "google_compute_managed_ssl_certificate" "demo" {
provider = google-beta
name = "demo"
managed {
domains = [
"${var.doman_name}",
]
}
}
resource "google_compute_target_https_proxy" "demo" {
name = "demo"
url_map = google_compute_url_map.demo.self_link
ssl_certificates = [google_compute_managed_ssl_certificate.demo.id]
}
resource "google_compute_global_forwarding_rule" "demo" {
name = "demo"
target = google_compute_target_https_proxy.demo.self_link
port_range = "443"
ip_address = google_compute_global_address.demo.address
}
5. Terraformによる一括デプロイ
作成した Terraform を実行して、インフラを一括デプロイします。
コード全体は以下のリポジトリにあります。
デプロイ完了後にアクセスしたキャプチャが以下です。
6. オプション設定
必要に応じて以下を実施します。
Cloud Identity-Aware-Proxy の設定
フロントエンドに認証機能がない場合、Cloud Identity-Aware-Proxy(iap) を有効化して、外部からのアクセス時に Google 認証を強制することができます。
IAP for Cloud Run の有効化
なお、今回作成した Terrafrom では iap の有効化は無視するようにしています。
iap は手動でしか設定できない箇所があったり、シークレットのような機微情報を Terraform に記載する必要があるため、
後から手動で追加できるようにしました。
resource "google_compute_backend_service" "frontend" {
name = "frontend"
load_balancing_scheme = "EXTERNAL"
protocol = "HTTP"
backend {
group = google_compute_region_network_endpoint_group.frontend.id
}
lifecycle {
ignore_changes = [
iap,
]
}
}
おわりに
第2世代の Cloud Run では直接 VPC に接続できるようになったことで、よりシンプルな形でセキュアなネットワーク構成を実現できます。
また、Load Balancer の下に組み込むことで Cloud Identity-Aware-Proxy や Cloud Armor を利用することで更なるセキュリティを実現できます。
※ データ分析、データ基盤構築、及び AI 活用に関するご相談は、以下よりお気軽にお問い合わせください。
お問い合わせフォーム
Discussion