Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Netbox nativo de la nube en Google Cloud Platform

By Mike SparrJan 24, 202312 min read

Esta página también está disponible en English, Deutsch, Français, Italiano, 日本語 y Português.

netbox

Cómo entender mejor tus redes y equipos con la gestión de direcciones IP (IPAM)

Últimamente he visto que cada vez más clientes reportan problemas de red, sobre todo de peering, por colisiones en sus rangos de IP. Es una señal clara de que toca planificar y administrar las direcciones IP en toda la organización.

Aunque puedes llevar el control de tus IPs en una hoja de cálculo compartida, también existen herramientas de software para esto. En este post te muestro cómo correr una herramienta open source muy popular para la gestión de direcciones IP (IPAM) llamada Netbox de forma nativa en la nube sobre Google Cloud Platform (GCP).

Stack tradicional

Históricamente, Netbox se ejecuta en una o varias máquinas virtuales detrás de un servidor web. Existe una imagen de Docker mantenida por la comunidad, pero las únicas instrucciones disponibles son para correrla con docker compose. Esta arquitectura se parece bastante a la de muchas aplicaciones que las empresas construyen u operan, así que es un excelente caso para mostrar también cómo migrar a la nube pública.

how to migrate to public cloud

Fuente: Netbox — instalación estándar de Netbox

Diseño nativo de la nube

Decidí investigar cómo funciona la imagen de Docker, sus dependencias y parámetros de configuración, para desplegarla en GCP usando solamente servicios administrados. Este ejemplo sirve para ilustrar cómo puedes hacer un "move and improve" o un "rip and replace" de aplicaciones cuando migras a la nube pública.

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

Instalación revisada de Netbox en GCP usando servicios administrados

Componentes de la aplicación

  • Aplicación Netbox (app en Python con el framework Django)
  • Base de datos PostgreSQL (Cloud SQL)
  • Redis (Cloud Memorystore)

Decisiones de diseño

  • Base de datos y caché administradas (Cloud SQL, Cloud Memorystore)
  • IPs solo privadas para bases de datos y caché (Private Service Access)
  • DNS privado para los hostnames de las bases de datos (Cloud DNS)
  • Secretos almacenados en secret manager (Secret Manager)
  • Runtime de contenedores serverless (Cloud Run, Artifact Registry)
  • Load balancer global con TLS (HTTP(S) Load Balancing, Managed Certificate)
  • WAF firewall (Cloud Armor)

Algo con lo que veo batallar a muchas organizaciones es conectar servicios administrados y apps serverless mediante direcciones IP privadas. Este ejemplo muestra cómo se reservan rangos privados en tu red VPC y luego se asignan a los servicios administrados, creando un puente de conectividad.

Cloud DNS se usa para establecer hostnames privados con los que las apps se conectan a las bases de datos. Esto da más flexibilidad a futuro si llegas a cambiar de base de datos o necesitas hacer failover, porque basta con actualizar tus registros DNS y las apps siguen apuntando al mismo dominio. En teoría, podría haber usado DNS forwarding y conectarlo todo a mi dominio público, pero internamente no era necesario, así que usé example.com.

No necesitábamos la VM bastion (o jump host), pero levanté una para probar conexiones mientras armaba todo. Lo normal es desplegar un bastion en un managed instance group (MIG) tamaño 1 y sin direcciones IP externas.

Aplicación web segura y con load balancing

secure load-balanced web application

Para mostrar mejor cómo encaja todo, usé un dominio personal y registré un "A record" para la dirección IP estática que asigné al Global Load Balancer; con eso se aprovisionó automáticamente un certificado administrado.

automatically-provisioned

Para mayor seguridad, apliqué una política de Cloud Armor (WAF firewall) al load balancer y restringí los rangos de IP (ver más abajo).

Código de implementación

El código de abajo muestra paso a paso los comandos que usé para configurar todo: red, variables de entorno y secretos, bases de datos, artifact registry e imágenes de Docker, Cloud Run, load balancing y WAF firewall.

#!/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

Complejidad adicional y consideraciones

Una de las razones por las que elegí Netbox para mostrar cómo modernizar y desplegar aplicaciones de forma nativa en la nube es justamente su complejidad técnica. La aplicación incluye file system, sesiones, workers e incluso un proceso diario de limpieza por cron.

Netbox application to illustrate

Fragmento de Docker Compose para la aplicación Netbox (fíjate en netbox-worker y netbox-housekeeping)

El fragmento del archivo docker-compose.yaml de arriba ilustra una característica de YAML llamada anchors, que no es exclusiva de docker-compose.

feature of yaml

Puedes duplicar configuraciones de forma concisa y luego sobrescribir comandos para correr distintos scripts en tiempo de ejecución.

Recrear los workers en Cloud Run

Para recrear este tipo de funcionalidad en Cloud Run, existen los flags --cmd y --args que puedes agregar. Lo más práctico es duplicar los comandos que usaste para desplegar la aplicación principal, cambiarle el nombre y luego sobrescribir el CMD para ejecutar un entrypoint distinto, como en el ejemplo:

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 diario de housekeeping con Cloud Run y Cloud Scheduler

El job diario de housekeeping puede correrse creando un servicio duplicado en Cloud Run y luego programando ese job para que invoque el servicio mediante 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"

Una vez desplegado el servicio de housekeeping, habilitamos la API de Cloud Scheduler:

gcloud services enable cloudscheduler.googleapis.com

Luego creamos una service account, le otorgamos permisos de invoker y creamos el job programado:

# 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

File systems

Mi objetivo era demostrar que se puede separar una aplicación compleja como Netbox y desplegarla en la nube usando Cloud Run y otros servicios administrados. Puede que no sea la mejor solución para esta app en particular, pero es posible.

Si necesitas usar el file system, hoy las plataformas serverless tienen limitaciones, así que tal vez prefieras correrlo en Kubernetes Engine, o incluso en una VM de Compute Engine. Puedes correr una VM como contenedor, lo cual queda bastante elegante, y luego adjuntar discos/volúmenes según se necesite.

Un truco para tener un "file system" simple en Cloud Run es aprovechar Secret Manager, tal como lo hice en el código de ejemplo y en el fragmento de abajo.

# 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

Buena práctica: service accounts separadas

Aunque los ejemplos que compartí usan una service account separada para el add-on de Cloud Scheduler, lo más recomendable es crear service accounts separadas para cada servicio y para el bastion (VM), asignándoles solo los roles de IAM mínimos necesarios. Esto se alinea con el principio de mínimo privilegio.

Para el servicio de Cloud Run, conviene crear una service account aparte llamada "netbox-runner" y otorgarle solo los roles necesarios, como:

Espero que este ejemplo deje claro cómo modernizar aplicaciones existentes y aprovechar los servicios administrados en la nube pública. Si solo buscas poner Netbox en marcha, los snippets de código de arriba te servirán, pero también puedes considerar correrlo en una VM o en K8s.

También podrías convertir el ejemplo funcional a Terraform usando soluciones de terceros como Terraformer, o incluso las propias herramientas de exportación masiva de GCP, que pueden hacer ingeniería inversa de tu infraestructura existente y generar el código Terraform.

Si tu organización está enfrentando retos similares con colisiones de IPs al configurar sus redes, quizás sea hora de poner en práctica IPAM, ya sea con una hoja de cálculo compartida o con una herramienta popular como Netbox.

¡Feliz clouding!