Deploys Shinobi NVR to Google Cloud, mirroring the
existing reference Terraform route: a single Container-Optimized OS (COS) Compute
Engine VM (shinobi-vm) running a docker-compose stack (Shinobi + Caddy for
TLS), delivered via the instance startup-script. It attaches to the existing
IT-managed your-vpc / your-subnet by reference only, keeps SQLite, and
sources all secrets from Secret Manager.
- Terraform module:
terraform/infra/ - Migration helpers:
migration/ - Target: project
your-project, regionus-central1, zoneus-central1-a
The shared network (
your-vpc/your-subnet) is referenced via Terraform data sources and is never created or modified by this module.
gcloudCLI installed and authenticated with Application Default Credentials:gcloud auth login gcloud auth application-default login gcloud config set project your-project- Terraform ≥ 1.5 (
terraform version). - Billing enabled on
your-project, and permission to create Compute, IAM, and Secret Manager resources.
Secret values live only in Secret Manager — never in Terraform, the compose file, or git (NFR-2). Create the three secrets with distinct, strong values. Do not reuse the local hardcoded admin password (FR-4.2):
printf '%s' 'A-NEW-STRONG-ADMIN-PASSWORD' | gcloud secrets create shinobi-admin-password --data-file=- --project your-project
printf '%s' "$(openssl rand -hex 24)" | gcloud secrets create shinobi-cron-key --data-file=- --project your-project
printf '%s' "$(openssl rand -hex 24)" | gcloud secrets create shinobi-plugins-key --data-file=- --project your-projectThe VM service account is granted roles/secretmanager.secretAccessor on each
of these secrets individually (least privilege) by Terraform.
Resolve a digest for the Shinobi image and pin it, rather than deploying a floating tag:
docker buildx imagetools inspect shinobisystems/shinobi:dev
# copy the "Digest: sha256:..." valueSet it in terraform.tfvars:
shinobi_image_digest = "sha256:<resolved-digest>"If shinobi_image_digest is left empty, the stack falls back to
shinobi_image_tag (default dev) — acceptable for a first bring-up, but pin
the digest for any real deploy.
cd terraform/infra
cp terraform.tfvars.example terraform.tfvars # set project_id (+ image digest)
terraform init
terraform plan # review: NO create/modify/destroy on your-vpc or your-subnet
terraform applyRead the outputs:
terraform output external_ip # for the DNS A-record
terraform output app_url # https://shinobi.example.org
terraform output ssh_command # IAP SSH into the VM
terraform output startup_script_log_command # tail the boot log
terraform output camera_check_command # RTSP reachability probeCreate an A record pointing the hostname at the reserved IP:
A shinobi.example.org -> <external_ip from terraform output>
Once DNS resolves, Caddy completes the Let's Encrypt HTTP-01 challenge (port 80)
and serves HTTPS (port 443), proxying to Shinobi on :8080.
# SSH via IAP (no public port 22):
gcloud compute ssh shinobi-vm --zone us-central1-a --project your-project --tunnel-through-iap
# Tail the startup-script log (secret fetch, stack launch, camera probe):
gcloud compute ssh shinobi-vm --zone us-central1-a --project your-project --tunnel-through-iap \
--command='sudo journalctl -u google-startup-scripts.service --no-pager'The camera (10.0.0.10) is outside your-subnet (10.1.0.0/24), so a
route to it is not implied by subnet membership. Verify both stages before
relying on live camera streams.
gcloud compute routes list --project your-project --filter="network:your-vpc"Confirm a route whose destRange covers 10.0.0.0/24 with a next hop
that is the IT-managed gateway (VPN tunnel / interconnect / next-hop-ip).
If no such route exists, halt and request IT add it — this is the primary
(and preferred) path to the lab network.
The startup-script probes this at every boot (look for CAMERA REACHABLE /
CAMERA UNREACHABLE in the startup log). To probe manually:
# via the terraform output helper:
$(terraform -chdir=terraform/infra output -raw camera_check_command)
# or directly from the VM:
gcloud compute ssh shinobi-vm --zone us-central1-a --project your-project --tunnel-through-iap \
--command="timeout 5 bash -c '</dev/tcp/10.0.0.10/554' && echo REACHABLE || echo UNREACHABLE"A failure of either check is a blocking issue for camera use. The default stack contains no Tailscale/VPN sidecar (FR-7.3) — reachability relies on the VPC route above. If the IT-managed gateway path cannot be made to work, the contingent fallback below is authorized.
Per the amended FR-7.3 / design decision D-G, if the route/RTSP probe cannot be
made to pass via the IT gateway, a Tailscale sidecar may be added to the compose
stack (mirroring the reference pattern) so shinobi-vm reaches the camera over the
tailnet. This is Task 10 in the spec and is not built by default; it is
implemented only if the probe fails at deploy time (auth key via a new
shinobi-ts-authkey Secret Manager secret, per NFR-2/NFR-6).
Migrates the live monitors/DB from the local deployment, preserving group
<group-id> and monitors <monitor-id-1>, <monitor-id-2>, <monitor-id-3>, <monitor-id-4>.
The live config lives inside the local container (the host ~/shinobi/config
mount is empty), so the export starts the container and copies from inside it.
# 1. Export + sanitize from the local machine (Docker must be running):
LOCAL_SHINOBI_DIR="$HOME/shinobi" ./migration/export-local-config.sh
# -> writes sanitized super.json/conf.json + SQLite DB to migration/export/
# (local hardcoded password and 'changeme' keys stripped — FR-8.4)
# 2. Import into shinobi-vm (via IAP), preserve IDs, reset the admin password:
PROJECT_ID=your-project ./migration/import-to-gcp.shThe import script:
- copies the sanitized artifacts into
/var/shinobi/configonshinobi-vm, - restarts Shinobi,
- verifies the group + 4 monitor IDs are present (FR-8.2),
- resets the admin account password to the
shinobi-admin-passwordSecret Manager value (Risk R2), and runs a login smoke check.
Confirm every ID line reports present and that you can log in with the Secret
Manager password.
Note on the password reset: Shinobi stores account passwords as an md5 hash in the SQLite
Userstable; the script recomputes the hash from the secret. Confirm this matches your Shinobi version, or reset via the Shinobi UI.
cd terraform/infra
terraform destroyshinobi-ip is protected by prevent_destroy; to release it, remove the
lifecycle block (or terraform state rm) first. The three Secret Manager
secrets are not managed by Terraform — delete them explicitly if desired:
for s in shinobi-admin-password shinobi-cron-key shinobi-plugins-key; do
gcloud secrets delete "$s" --project your-project --quiet
doneTo pause billing without destroying, set vm_desired_status = "TERMINATED" and
terraform apply.