Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

GCPで実現するクラウドネイティブなNetbox

By Mike SparrJan 24, 202312 min read

このページはEnglishDeutschEspañolFrançaisItalianoPortuguêsでもご覧いただけます。

netbox

IPアドレス管理(IPAM)でネットワークと機器を可視化する

最近、IPアドレスレンジの衝突が原因でピアリングをはじめとするネットワーク関連のトラブルを訴えるお客様が増えています。これは、組織全体でIPアドレスを計画的に管理すべきだという明確なサインです。

共有スプレッドシートでIPを管理することもできますが、専用のソフトウェアツールも数多くあります。本記事では、人気のオープンソースIPアドレス管理(IPAM)ツールNetboxを、Google Cloud Platform(GCP)上でクラウドネイティブに動かす方法を紹介します。

従来のスタック

従来、Netboxは1台または複数台の仮想マシン上で動かし、その前段にWebサーバーを配置する構成が一般的でした。コミュニティが管理するDockerイメージもありますが、提供されている手順はdocker composeでの実行のみです。とはいえ、このアーキテクチャは多くの企業が構築・運用しているアプリケーションと共通点が多く、パブリッククラウドへの移行例としても格好の題材です。

how to migrate to public cloud

出典:Netbox — 標準的なNetboxインストール構成

クラウドネイティブな設計

そこで、Dockerイメージの仕組みや依存関係、設定パラメータを調べ上げ、マネージドサービスのみでGCPにデプロイしてみることにしました。パブリッククラウドへ移行する際の「move and improve(移行して改善する)」あるいは「rip and replace(置き換える)」という二つのアプローチを示す例として参考にしていただければと思います。

rip-and-replace-or-move-improve-public-web

マネージドサービスを活用したGCP上のNetbox構成(再設計版)

アプリケーションの構成要素

  • Netboxアプリケーション(Djangoフレームワークを用いたPythonアプリ)
  • PostgreSQLデータベース(Cloud SQL)
  • Redis(Cloud Memorystore)

設計上の判断

  • マネージドのデータベースとキャッシュ(Cloud SQL、Cloud Memorystore)
  • データベースとキャッシュはプライベートIPのみで運用(Private Service Access)
  • データベースのホスト名はプライベートDNSで解決(Cloud DNS)
  • シークレットはSecret Managerに格納
  • サーバーレスのコンテナランタイム(Cloud Run、Artifact Registry)
  • TLS対応のグローバルロードバランサー(HTTP(S) Load Balancing、Managed Certificate)
  • WAFファイアウォール(Cloud Armor)

多くの組織がつまずくのが、マネージドサービスやサーバーレスアプリをプライベートIPで接続する部分です。本例では、VPCネットワーク内でプライベートレンジを予約し、それをマネージドサービスに割り当てて接続のブリッジを作る方法を示しています。

Cloud DNSは、アプリがデータベースに接続するためのプライベートホスト名を提供する目的で使います。こうしておけば、将来データベースを差し替えたりフェイルオーバーが必要になったりした場合でも、DNSレコードを更新するだけで済み、アプリ側は同じドメインを参照し続けられます。理屈の上ではDNSフォワーディングを使ってパブリックドメインに接続することも可能ですが、内部利用では不要なのでexample.comを使用しました。

bastion(踏み台)VMは必須ではありませんが、構築中の接続テスト用に1台立ち上げました。本番運用であれば、外部IPを持たせず、サイズ1のマネージドインスタンスグループ(MIG)でデプロイするのが通例です。

セキュアでロードバランス対応のWebアプリケーション

secure load-balanced web application

全体の構成をわかりやすく示すため、個人ドメインを使い、グローバルロードバランサーに割り当てた静的IPアドレスに対して「Aレコード」を登録しました。マネージド証明書は自動でプロビジョニングされます。

automatically-provisioned

セキュリティを強化するため、ロードバランサーにCloud Armor(WAFファイアウォール)ポリシーを適用し、IPレンジを制限しています(後述)。

実装コード

以下のコードは、ネットワーキング、環境変数とシークレット、データベース、Artifact RegistryとDockerイメージ、Cloud Run、ロードバランシング、WAFファイアウォールに至るまで、構築に使ったコマンドを順を追って示したものです。

#!/usr/bin/env bash
#####################################################################
# REFERENCES
# - https://docs.netbox.dev/en/stable/installation/3-netbox/
# - https://github.com/netbox-community/netbox-docker/wiki/
# - https://hub.docker.com/r/netboxcommunity/netbox
# - https://cloud.google.com/sql/docs/postgres/configure-private-ip
# - https://cloud.google.com/sql/docs/postgres/create-instance
# - https://cloud.google.com/sql/docs/postgres/create-manage-databases#gcloud
# - https://cloud.google.com/sql/docs/postgres/create-manage-users#gcloud
# - https://cloud.google.com/memorystore/docs/redis/create-manage-instances#creating_a_redis_instance_with_a_specific_ip_address_range
# - https://cloud.google.com/artifact-registry/docs/docker/store-docker-container-images
# - https://cloud.google.com/artifact-registry/docs/docker/pushing-and-pulling
# - https://cloud.google.com/dns/docs/zones#create-private-zone
# - https://cloud.google.com/dns/docs/records
# - https://cloud.google.com/secret-manager/docs/configuring-secret-manager
# - https://cloud.google.com/secret-manager/docs/create-secret
# - https://cloud.google.com/run/docs/configuring/secrets#command-line
# - https://cloud.google.com/run/docs/configuring/connecting-vpc#gcloud
#####################################################################
export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_USER=$(gcloud config get-value core/account) # set current user
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")
export IDNS=${PROJECT_ID}.svc.id.goog # workflow identity domain
export GCP_REGION="us-central1" # CHANGEME (OPT)
export GCP_ZONE="us-central1-a" # CHANGEME (OPT)
export NETWORK_NAME="default"
# enable apis
gcloud services enable compute.googleapis.com \
servicenetworking.googleapis.com \
vpcaccess.googleapis.com \
secretmanager.googleapis.com \
sqladmin.googleapis.com \
redis.googleapis.com \
artifactregistry.googleapis.com \
dns.googleapis.com \
cloudbuild.googleapis.com \
storage.googleapis.com\
run.googleapis.com
# configure gcloud sdk
gcloud config set compute/region $GCP_REGION
gcloud config set compute/zone $GCP_ZONE
#############################################################
# NETWORKING
#############################################################
export NETBOX_NETWORK_NAME="netbox"
export NETBOX_RESERVED_RANGE_NAME="google-managed-services-netbox"
export SUBNET_BASTION_NAME="bastion"
export SUBNET_BASTION_RANGE="10.250.0.0/29"
export CONNECTOR_NAME="serverless-connector"
export CONNECTOR_RANGE="10.200.0.0/28"
export DNS_ZONE="private-zone"
export DNS_SUFFIX="example.com" # CHANGEME (OPT)
export DNS_LABELS="dept=networking" # CHANGEME (OPT)
# create network (custom-mode)
gcloud compute networks create $NETBOX_NETWORK_NAME \
--subnet-mode=custom
# create bastion subnet
gcloud compute networks subnets create $SUBNET_BASTION_NAME \
--region=$GCP_REGION \
--network=$NETBOX_NETWORK_NAME \
--range=$SUBNET_BASTION_RANGE
# allocate private range
gcloud compute addresses create $NETBOX_RESERVED_RANGE_NAME \
--global \
--purpose=VPC_PEERING \
--addresses=10.100.0.0 \
--prefix-length=16 \
--network=projects/$PROJECT_ID/global/networks/$NETBOX_NETWORK_NAME
# create peering for managed services
gcloud services vpc-peerings connect \
--service=servicenetworking.googleapis.com \
--ranges=$NETBOX_RESERVED_RANGE_NAME \
--network=$NETBOX_NETWORK_NAME
# create serverless vpc connector (for peering serverless to VPC)
gcloud compute networks vpc-access connectors create $CONNECTOR_NAME \
--network $NETBOX_NETWORK_NAME \
--region $GCP_REGION \
--range $CONNECTOR_RANGE
# create private zone
gcloud dns managed-zones create $DNS_ZONE \
--description="internal zone" \
--dns-name=$DNS_SUFFIX \
--networks=$NETBOX_NETWORK_NAME \
--labels=$DNS_LABELS \
--visibility=private
# firewall allow ssh
gcloud compute firewall-rules create fw-allow-ssh \
--network=$NETBOX_NETWORK_NAME \
--action=allow \
--direction=ingress \
--target-tags=allow-ssh \
--rules=tcp:22
#############################################################
# NETBOX (ENV/SECRETS)
#############################################################
export SECRET_ID="netbox-secrets"
export SECRET_VERSION=1
export SECRET_FILE=".env-local"
export ENV_FILE="netbox.env"
# individual
export SECRET_DB_PASS="db_password"
export SECRET_REDIS_PASS="redis_password"
export SECRET_REDIS_CACHE_PASS="redis_cache_password"
export SECRET_SU_PASS="superuser_password"
export SECRET_EMAIL_PASS="email_password"
export SECRET_SECRET_KEY="secret_key"
# fetch secret values from local .env file
source $SECRET_FILE
# save file with injected values
cat > $ENV_FILE << EOF
# required
ALLOWED_HOSTS="*"
DB_HOST=$POSTGRES_INSTANCE.$DNS_SUFFIX
DB_PORT=$POSTGRES_PORT
DB_NAME=netbox
DB_USER=netbox
REDIS_CACHE_DATABASE=1
REDIS_CACHE_HOST=$REDIS_INSTANCE.$DNS_SUFFIX
REDIS_CACHE_INSECURE_SKIP_TLS_VERIFY=false
REDIS_CACHE_SSL=false
REDIS_DATABASE=0
REDIS_HOST=$REDIS_INSTANCE.$DNS_SUFFIX
REDIS_INSECURE_SKIP_TLS_VERIFY=false
REDIS_SSL=false
EOF
# add config to env
source $ENV_FILE
# create secret for all vars
gcloud secrets create $SECRET_ID --replication-policy="automatic"
gcloud secrets versions add $SECRET_ID --data-file=${PWD}/$SECRET_FILE # version 1
# create env secrets
echo -n $DB_PASSWORD | gcloud secrets create $SECRET_DB_PASS \
--replication-policy="automatic" \
--data-file=-
# redis auth string after creation
# redis_cache auth string after creation
echo -n $SUPERUSER_PASSWORD | gcloud secrets create $SECRET_SU_PASS \
--replication-policy="automatic" \
--data-file=-
echo -n $EMAIL_PASSWORD | gcloud secrets create $SECRET_EMAIL_PASS \
--replication-policy="automatic" \
--data-file=-
echo -n $SECRET_KEY | gcloud secrets create $SECRET_SECRET_KEY \
--replication-policy="automatic" \
--data-file=-
#############################################################
# DATABASE (POSTGRES)
#############################################################
export POSTGRES_INSTANCE="netbox-db"
export POSTGRES_VERSION="POSTGRES_14"
export POSTGRES_TIER="db-f1-micro"
export POSTGRES_PORT=5432
gcloud beta sql instances create $POSTGRES_INSTANCE \
--database-version=$POSTGRES_VERSION \
--tier=$POSTGRES_TIER \
--network=projects/$PROJECT_ID/global/networks/$NETBOX_NETWORK_NAME \
--no-assign-ip \
--allocated-ip-range-name=$NETBOX_RESERVED_RANGE_NAME \
--region=$GCP_REGION
# get internal IP
export POSTGRES_HOST=$(gcloud beta sql instances describe $POSTGRES_INSTANCE --format="value(ipAddresses.ipAddress)")
# add to private-zone DNS
gcloud dns record-sets transaction start --zone=$DNS_ZONE
gcloud dns record-sets transaction add $POSTGRES_HOST \
--name="$POSTGRES_INSTANCE.$DNS_SUFFIX" --ttl="3600" --type="A" --zone=$DNS_ZONE
gcloud dns record-sets transaction execute --zone=$DNS_ZONE
# lock down postgres (admin) user [manually input at prompt]
gcloud sql users set-password postgres \
--instance=$POSTGRES_INSTANCE \
--prompt-for-password
# create netbox user
gcloud sql users create $DB_USER \
--instance=$POSTGRES_INSTANCE \
--password=$DB_PASSWORD
# create database
gcloud sql databases create $DB_NAME \
--instance=$POSTGRES_INSTANCE
#############################################################
# CACHE (REDIS)
#############################################################
export REDIS_INSTANCE="netbox-cache"
export REDIS_VERSION="redis_6_x"
gcloud redis instances create $REDIS_INSTANCE \
--size=1 \
--tier=STANDARD \
--region=$GCP_REGION \
--network=$NETBOX_NETWORK_NAME \
--reserved-ip-range=$NETBOX_RESERVED_RANGE_NAME \
--connect-mode=PRIVATE_SERVICE_ACCESS \
--redis-version=$REDIS_VERSION \
--enable-auth
# get internal IP
export REDIS_HOST=$(gcloud redis instances describe $REDIS_INSTANCE --region $GCP_REGION --format="value(host)")
export REDIS_PORT=$(gcloud redis instances describe $REDIS_INSTANCE --region $GCP_REGION --format="value(port)")
# get auth string
export REDIS_PASSWORD=$(gcloud beta redis instances get-auth-string $REDIS_INSTANCE --region $GCP_REGION --format="value(authString)")
# add to private-zone DNS
gcloud dns record-sets transaction start --zone=$DNS_ZONE
gcloud dns record-sets transaction add $REDIS_HOST \
--name="$REDIS_INSTANCE.$DNS_SUFFIX" --ttl="3600" --type="A" --zone=$DNS_ZONE
gcloud dns record-sets transaction execute --zone=$DNS_ZONE
# add secrets to secret manager
echo -n $REDIS_PASSWORD | gcloud secrets create $SECRET_REDIS_PASS \
--replication-policy="automatic" \
--data-file=-
echo -n $REDIS_PASSWORD | gcloud secrets create $SECRET_REDIS_CACHE_PASS \
--replication-policy="automatic" \
--data-file=-
#############################################################
# COMPUTE (TEST BASTION)
# - NOTE: if real bastion, create in managed instance group size=1
# - NOTE: if real bastion, no external IP and use IAP tunnel only
#############################################################
export BASTION_NAME="bastion-1"
# create compute instance to test from proxy-only network to ILB
gcloud compute instances create $BASTION_NAME \
--machine-type e2-micro \
--zone $GCP_ZONE \
--network $NETBOX_NETWORK_NAME \
--subnet $SUBNET_BASTION_NAME \
--tags allow-ssh
# install netcat
gcloud compute ssh $BASTION_NAME --zone $GCP_ZONE -- sudo apt-get update
gcloud compute ssh $BASTION_NAME --zone $GCP_ZONE -- sudo apt-get -y install netcat
# test internal DNS for database (IP may vary)
gcloud compute ssh $BASTION_NAME --zone $GCP_ZONE -- nc -zv $POSTGRES_INSTANCE.$DNS_SUFFIX $POSTGRES_PORT
# Connection to netbox-db.example.com (10.100.0.5) 5432 port [tcp/postgresql] succeeded!
# test internal DNS for cache (IP may vary)
gcloud compute ssh $BASTION_NAME --zone $GCP_ZONE -- nc -zv $REDIS_INSTANCE.$DNS_SUFFIX $REDIS_PORT
# Connection to netbox-cache.example.com (10.100.1.4) 6379 port [tcp/redis] succeeded!
#############################################################
# ARTIFACT REGISTRY
# - WARNING: arm architecture on Mac will produce non-runnable image
# run pull / tag / push commands from your temp bastion
#############################################################
export REPO_NAME="netbox-repo"
export NETBOX_IMAGE="netboxcommunity/netbox:v3.4-beta1-2.3.0"
export IMAGE_NAME="netbox"
export TAG_NAME="v3.4-beta1-2.3.0"
export IMAGE_PATH=$GCP_REGION-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/$IMAGE_NAME:$TAG_NAME
gcloud artifacts repositories create $REPO_NAME \
--repository-format=docker \
--location=$GCP_REGION \
--description="Docker repository"
# configure auth
gcloud auth configure-docker ${GCP_REGION}-docker.pkg.dev
# fetch latest community netbox image
docker pull $NETBOX_IMAGE
# tag image for artifact registry
docker tag $NETBOX_IMAGE \
$IMAGE_PATH
# push image to artifact registry
docker push $IMAGE_PATH
#############################################################
# NETBOX (CLOUD RUN)
#############################################################
export SERVICE_NAME="netbox"
export SECRET_PATH="env/$SECRET_FILE" # as config in docker-compose.yaml
export SA_EMAIL="[email protected]"
# add compute service account access to secrets
# - NOTE: best practice to create separate service account to run each workload
gcloud secrets add-iam-policy-binding $SECRET_ID \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/secretmanager.secretAccessor"
# individual
gcloud secrets add-iam-policy-binding $SECRET_DB_PASS \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding $SECRET_REDIS_PASS \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding $SECRET_REDIS_CACHE_PASS \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding $SECRET_SU_PASS \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding $SECRET_EMAIL_PASS \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding $SECRET_SECRET_KEY \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/secretmanager.secretAccessor"
# deploy cloud run service (default port 8080) config from env
gcloud run deploy $SERVICE_NAME \
--platform managed \
--no-cpu-throttling \
--allow-unauthenticated \
--vpc-connector $CONNECTOR_NAME \
--ingress=internal-and-cloud-load-balancing \
--region $GCP_REGION \
--image $IMAGE_PATH \
--set-env-vars "ALLOWED_HOSTS=$ALLOWED_HOSTS" \
--set-env-vars "DB_HOST=$POSTGRES_INSTANCE.$DNS_SUFFIX" \
--set-env-vars "DB_PORT=$POSTGRES_PORT" \
--set-env-vars "DB_NAME=$DB_NAME" \
--set-env-vars "DB_USER=$DB_USER" \
--set-env-vars "REDIS_HOST=$REDIS_INSTANCE.$DNS_SUFFIX" \
--set-env-vars "REDIS_PORT=$REDIS_PORT" \
--set-env-vars "REDIS_DATABASE=$REDIS_DATABASE" \
--set-env-vars "REDIS_CACHE_HOST=$REDIS_INSTANCE.$DNS_SUFFIX" \
--set-env-vars "REDIS_CACHE_PORT=$REDIS_PORT" \
--set-env-vars "REDIS_CACHE_DATABASE=$REDIS_CACHE_DATABASE" \
--update-secrets=DB_PASSWORD=$SECRET_DB_PASS:$SECRET_VERSION \
--update-secrets=REDIS_PASSWORD=$SECRET_REDIS_PASS:$SECRET_VERSION \
--update-secrets=REDIS_CACHE_PASSWORD=$SECRET_REDIS_CACHE_PASS:$SECRET_VERSION \
--update-secrets=SUPERUSER_PASSWORD=$SECRET_SU_PASS:$SECRET_VERSION \
--update-secrets=EMAIL_PASSWORD=$SECRET_EMAIL_PASS:$SECRET_VERSION \
--update-secrets=SECRET_KEY=$SECRET_SECRET_KEY:$SECRET_VERSION \
--set-env-vars "DB_WAIT_DEBUG=1"
##########################################################
# Load Balancer
##########################################################
export APP_NAME=$SERVICE_NAME
export TLD="msparr.com" # OVERRIDE PRIOR - CHANGE ME TO DESIRED DOMAIN
export DOMAIN="$SERVICE_NAME.$TLD" # netbox.msparr.com
export EXT_IP_NAME="public-ip"
export BACKEND_SERVICE_NAME="$APP_NAME-service"
export SERVERLESS_NEG_NAME="$APP_NAME-neg"
# create static IP address
gcloud compute addresses create --global $EXT_IP_NAME
# wait 10 seconds and then set var
export EXT_IP=$(gcloud compute addresses describe $EXT_IP_NAME --global --format="value(address)")
echo "Remember to add DNS 'A' record [$DOMAIN] for IP [$EXT_IP]"
# create serverless NEG
gcloud compute network-endpoint-groups create $SERVERLESS_NEG_NAME \
--region=$GCP_REGION \
--network-endpoint-type=serverless \
--cloud-run-service=$SERVICE_NAME
# create backend service
gcloud compute backend-services create $BACKEND_SERVICE_NAME \
--load-balancing-scheme=EXTERNAL \
--global
# add serverless NEG to backend service
gcloud compute backend-services add-backend $BACKEND_SERVICE_NAME \
--network-endpoint-group=$SERVERLESS_NEG_NAME \
--network-endpoint-group-region=$GCP_REGION \
--global
# create URL map
gcloud compute url-maps create $APP_NAME-url-map \
--default-service $BACKEND_SERVICE_NAME
# create managed SSL cert
gcloud beta compute ssl-certificates create $APP_NAME-cert \
--domains $DOMAIN
# create target HTTPS proxy
gcloud compute target-https-proxies create $APP_NAME-https-proxy \
--ssl-certificates=$APP_NAME-cert \
--url-map=$APP_NAME-url-map
gcloud compute forwarding-rules create $APP_NAME-fwd-rule \
--target-https-proxy=$APP_NAME-https-proxy \
--global \
--ports=443 \
--address=$EXT_IP_NAME
# verify app is running (wait 10-15 minutes until cert provisions)
curl -k "https://$DOMAIN" # Unauthorized request
##########################################################
# [OPTIONAL] Restrict Traffic with Cloud Armor security policy
# - https://cloud.google.com/armor/docs/configure-security-policies#gcloud
##########################################################
export INTERNAL_POLICY_NAME="internal-users-policy"
export ALLOWED_CIDR="192.168.0.0/24" # CHANGE ME TO DESIRED IP RANGE
# create policies
gcloud compute security-policies create $INTERNAL_POLICY_NAME \
--description "policy for internal test users"
# update default rules
gcloud compute security-policies rules update 2147483647 \
--security-policy $INTERNAL_POLICY_NAME \
--action "deny-502"
# restrict traffic to desired IP ranges
gcloud compute security-policies rules create 1000 \
--security-policy $INTERNAL_POLICY_NAME \
--description "allow traffic from $ALLOWED_CIDR" \
--src-ip-ranges "$ALLOWED_CIDR" \
--action "allow"
# attach policy to backend service (one at a time)
gcloud compute backend-services update $BACKEND_SERVICE_NAME \
--security-policy $INTERNAL_POLICY_NAME \
--global

複雑さと注意したいポイント

クラウドネイティブな形でのモダナイゼーションとデプロイの題材としてNetboxを選んだ理由のひとつが、その技術的な複雑さにあります。このアプリケーションは、ファイルシステム、セッション、ワーカー、さらには日次のcronによるクリーンアップ処理まで備えています。

Netbox application to illustrate

Netboxアプリケーション用のDocker Composeスニペット(netbox-workerとnetbox-housekeepingに注目)

上記のdocker-compose.yamlのスニペットでは、YAMLの機能であるアンカーが使われています。これはdocker-compose固有のものではありません。

feature of yaml

これにより、設定をすっきりと再利用しつつ、コマンドをオーバーライドして実行時に異なるスクリプトを動かすことができます。

Cloud Runでワーカーを再現する

Cloud Run上で同等の機能を再現するには、--cmd_と--args_というフラグを使います。メインアプリケーションのデプロイコマンドをそのまま流用して名前だけ変更し、CMDをオーバーライドして以下のように別のエントリポイントスクリプトを実行させればOKです。

gcloud run deploy $WORKER_NAME \
--platform managed \
--allow-unauthenticated \
--vpc-connector $CONNECTOR_NAME \
--ingress=internal-and-cloud-load-balancing \
--region $GCP_REGION \
--image $IMAGE_PATH \
--set-env-vars "ALLOWED_HOSTS=$ALLOWED_HOSTS" \
...
--cmd "/opt/netbox/venv/bin/python" \
--args "/opt/netbox/netbox/manage.py" \
--args "rqworker"

Cloud RunとCloud Schedulerによる日次ハウスキーピングジョブ

日次のハウスキーピングジョブは、Cloud Run上に同じサービスをもう1つ作成し、Cloud Schedulerでそのサービスを毎日呼び出すようにスケジューリングすれば実現できます。

gcloud run deploy $HOUSEKEEPING_NAME \
--platform managed \
--allow-unauthenticated \
--vpc-connector $CONNECTOR_NAME \
--ingress=internal-and-cloud-load-balancing \
--region $GCP_REGION \
--image $IMAGE_PATH \
--set-env-vars "ALLOWED_HOSTS=$ALLOWED_HOSTS" \
...
--cmd "/opt/netbox/housekeeping.sh"

ハウスキーピングサービスをデプロイしたら、Cloud Scheduler APIを有効化します。

gcloud services enable cloudscheduler.googleapis.com

続いてサービスアカウントを作成し、invoker権限を付与した上で、スケジュールジョブを作成します。

# fetch the service URL
export SVC_URL=$(gcloud run services describe $HOUSEKEEPING_NAME \
--platform managed --region $GCP_REGION --format="value(status.url)")
#########################################################
# create cloud scheduler job
#########################################################
export SA_NAME="cloud-scheduler-runner"
export SA_EMAIL="${SA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com"
# create service account
gcloud iam service-accounts create $SA_NAME \
--display-name "${SA_NAME}"
# add sa binding to cloud run app
gcloud run services add-iam-policy-binding $HOUSEKEEPING_NAME \
--platform managed \
--region $GCP_REGION \
--member=serviceAccount:$SA_EMAIL \
--role=roles/run.invoker
# create the job to invoke service every day at 2:30 AM
gcloud scheduler jobs create http housekeeping-job --schedule "30 2 * * *" \
--http-method=GET \
--uri=$SVC_URL \
--oidc-service-account-email=$SA_EMAIL \
--oidc-token-audience=$SVC_URL

ファイルシステム

本記事の狙いは、Netboxのような複雑なアプリケーションを切り分け、Cloud Runをはじめとするマネージドサービスでクラウドにデプロイできることを示すことでした。今回のアプリにとってこれが最適解とは言い切れませんが、十分に実現可能です。

ファイルシステムを使いたい場合、現状のサーバーレスプラットフォームには制約がありますので、代わりにKubernetes Engineや、シンプルにCompute EngineのVMで動かすという選択肢が現実的です。VMをコンテナとして動かす方法もスマートで、必要に応じてディスクやボリュームをアタッチできます。

ただし、Cloud Runで簡易的な「ファイルシステム」を実現する小ワザとして、サンプルコードや以下のスニペットのようにSecret Managerを活用する方法もあります。

# create secret for all vars
gcloud secrets create $SECRET_ID --replication-policy="automatic"
gcloud secrets versions add $SECRET_ID --data-file=${PWD}/$SECRET_FILE
# mount file path in cloud run
gcloud run deploy $SERVICE --image $IMAGE_URL \
--update-secrets="/env/netbox.env"=$SECRET_ID:$SECRET_VERSION

ベストプラクティス:サービスアカウントを分ける

本記事の例ではCloud Schedulerアドオン用に別サービスアカウントを用意していますが、ベストプラクティスは、各サービスとbastion(VM)ごとに専用のサービスアカウントを作成し、必要最小限のIAMロールだけを付与することです。これは最小権限の原則に沿った考え方です。

Cloud Runサービスについても、独立した「netbox-runner」サービスアカウントを作成し、以下のような必要なロールだけを付与すべきです。

本記事の例から、既存アプリケーションをモダナイズし、パブリッククラウドのマネージドサービスを使いこなすイメージが伝われば幸いです。ひとまずNetboxを動かしたいだけなら、上記のコードスニペットで十分対応できますが、VMやK8Sでの運用も選択肢として検討する価値があります。

動作確認した構成は、Terraformerのようなサードパーティ製ソリューションや、GCP公式の一括エクスポートツールを使ってTerraformに変換することも可能です。これらは既存インフラをリバースエンジニアリングし、Terraformコードを生成してくれます。

もしネットワーク構築の現場でIPの衝突といった同様の課題に直面しているなら、共有スプレッドシートでもNetboxのような定番ツールでも構いませんので、ぜひIPAMに取り組んでみてください。

Happy clouding!