Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Netbox cloud-native no Google Cloud Platform

By Mike SparrJan 24, 202312 min read

Esta página também está disponível em English, Deutsch, Español, Français, Italiano e 日本語.

netbox

Organize redes e equipamentos com gerenciamento de endereços IP (IPAM)

Ultimamente, vários clientes têm relatado problemas de rede — principalmente em peering — por causa de colisões de faixas de IP. Esse é um sinal claro de que está na hora de planejar e gerenciar os endereços IP de toda a organização.

Dá para controlar seus IPs em uma planilha compartilhada, mas também existem ferramentas dedicadas. Este post mostra como rodar uma ferramenta open source bem popular de gerenciamento de endereços IP (IPAM), o Netbox, de forma cloud-native no Google Cloud Platform (GCP).

Stack tradicional

Historicamente, o Netbox roda em uma ou mais máquinas virtuais com um servidor web na frente. Existe uma imagem Docker mantida pela comunidade, mas as únicas instruções são para subir tudo com docker compose. Essa arquitetura lembra muitas das aplicações que as empresas constroem ou rodam — e por isso é uma ótima candidata para também ilustrar como migrar para a nuvem pública.

how to migrate to public cloud

Fonte: Netbox — instalação padrão do Netbox

Arquitetura cloud-native

Resolvi entender como a imagem Docker funciona, suas dependências e parâmetros de configuração, e implantá-la no GCP usando apenas serviços gerenciados. Este exemplo serve para ilustrar como você pode "mover e melhorar" ou "trocar e substituir" aplicações ao migrar para a nuvem pública.

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

Instalação revisada do Netbox no GCP usando serviços gerenciados

Componentes da aplicação

  • Aplicação Netbox (app em Python usando o framework Django)
  • Banco de dados PostgreSQL (Cloud SQL)
  • Redis (Cloud Memorystore)

Decisões de arquitetura

  • Banco de dados e cache gerenciados (Cloud SQL, Cloud Memorystore)
  • IPs apenas privados para banco e cache (Private Service Access)
  • DNS privado para os hostnames do banco (Cloud DNS)
  • Segredos guardados no secret manager (Secret Manager)
  • Runtime de contêiner serverless (Cloud Run, Artifact Registry)
  • Load balancer global com TLS (HTTP(S) Load Balancing, Managed Certificate)
  • Firewall WAF (Cloud Armor)

Algo com que vejo muitas empresas penarem é conectar serviços gerenciados e apps serverless via endereços IP privados. Este exemplo mostra como reservar faixas privadas na sua rede VPC e atribuí-las aos serviços gerenciados, criando uma ponte de conectividade.

O Cloud DNS é usado para criar hostnames privados que os apps usam para se conectar aos bancos. Isso traz mais flexibilidade no futuro, caso você precise trocar o banco ou fazer failover, porque basta atualizar os registros DNS e os apps continuam apontando para o mesmo domínio. Em tese, eu poderia ter usado encaminhamento de DNS e conectado tudo ao meu domínio público, mas, internamente, isso não é necessário, então usei example.com.

A VM bastion (ou jump host) não era obrigatória, mas subi uma para testar conexões enquanto montava o ambiente. Normalmente, um bastion é implantado em um managed instance group (MIG) de tamanho 1 e sem endereços IP externos.

Aplicação web segura e com load balancer

secure load-balanced web application

Para mostrar como tudo se encaixa, usei um domínio pessoal e cadastrei um "registro A" para o IP estático que atribuí ao Global Load Balancer; um certificado gerenciado foi provisionado automaticamente.

automatically-provisioned

Para reforçar a segurança, apliquei uma política do Cloud Armor (firewall WAF) ao load balancer e restringi as faixas de IP (veja abaixo).

Código de implementação

O código abaixo mostra, passo a passo, os comandos que usei para configurar tudo: rede, variáveis de ambiente e segredos, bancos de dados, artifact registry e imagens Docker, Cloud Run, load balancing e firewall 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

Complexidades adicionais e pontos de atenção

Um dos motivos para eu escolher o Netbox como exemplo de modernização e implantação cloud-native é justamente a sua complexidade técnica. A aplicação inclui sistema de arquivos, sessões, workers e até um cron diário de limpeza.

Netbox application to illustrate

Trecho do Docker Compose da aplicação Netbox (repare em netbox-worker e netbox-housekeeping)

O trecho do arquivo docker-compose.yaml acima ilustra um recurso do YAML chamado anchors, que não é exclusivo do docker-compose.

feature of yaml

Você pode duplicar configurações de forma enxuta e, em seguida, sobrescrever os comandos para rodar scripts diferentes em tempo de execução.

Recriando workers no Cloud Run

Para recriar esse tipo de funcionalidade no Cloud Run, dá para usar as flags --cmd e --args. Eu duplicaria os comandos usados para implantar a aplicação principal, mudaria o nome e sobrescreveria o CMD para executar um script de entrypoint diferente, como abaixo:

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"

Job diário de housekeeping com Cloud Run e Cloud Scheduler

O job diário de housekeeping pode rodar em um serviço duplicado no Cloud Run, agendado para invocar esse serviço todos os dias por meio do 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"

Depois de implantar o serviço de housekeeping, habilitamos a API do Cloud Scheduler:

gcloud services enable cloudscheduler.googleapis.com

Em seguida, criamos uma service account, damos a ela permissão de invoker e criamos o job agendado:

# 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

Sistemas de arquivos

Meu objetivo era provar que dá para separar uma aplicação complexa como o Netbox e implantá-la na nuvem usando Cloud Run e outros serviços gerenciados. Pode não ser a melhor solução para esse app específico, mas é viável.

Se você precisa do sistema de arquivos, hoje as plataformas serverless ainda têm limitações, e talvez seja mais interessante rodar no Kubernetes Engine ou até mesmo em uma VM do Compute Engine. Você pode rodar uma VM como contêiner, o que é bem prático, e anexar discos/volumes conforme a necessidade.

Mas tem um truque para ter um "sistema de arquivos" simples no Cloud Run: usar o Secret Manager, como fiz no código de exemplo e no trecho abaixo.

# 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

Boa prática: service accounts separadas

Embora os exemplos que compartilhei mostrem uma service account separada apenas para o add-on do Cloud Scheduler, a boa prática seria criar service accounts distintas para cada serviço e para o bastion (VM), atribuindo só as roles IAM mínimas que cada um precisa. Isso segue o princípio do menor privilégio.

Para o serviço do Cloud Run, devemos criar uma service account "netbox-runner" separada e conceder a ela apenas as roles necessárias, como:

Espero que este exemplo mostre como você pode modernizar aplicações existentes e tirar proveito de serviços gerenciados na nuvem pública. Se você só quer colocar o Netbox no ar, os snippets acima já dão conta do recado — mas vale considerar também rodar em VM ou K8s.

Outra opção é converter o exemplo para Terraform usando soluções de terceiros como o Terraformer, ou até as próprias ferramentas de bulk export do GCP, que fazem engenharia reversa da sua infraestrutura existente e geram o código Terraform.

Se a sua organização enfrenta desafios parecidos com colisões de IP ao configurar redes, talvez seja hora de adotar IPAM — seja com uma planilha compartilhada ou com uma ferramenta consagrada como o Netbox.

Boas nuvens!