Skip to main content
This is where the previous seven tutorials come together into one real thing: a small but genuinely production-shaped deployment, with an environment you iterate on quickly and a production environment that only changes behind review.

What we’re building

  • A NEO Lite Pro VM as the app tier (biznetgio_neolite_pro_vm / NeoliteProVm), since it’s the dedicated-resource tier meant for real workloads rather than the entry-level NEO Lite.
  • A NEO Object Storage instance, bucket, and credential (biznetgio_object_storage* / ObjectStorage*) for the app’s data and backups.
  • Two environments, staging and production, same code, different sizing and different money.
  • State in a shared backend, secrets in CI, and every change to production going through a reviewed pull request.

1. Repository layout

Following Structure a real project:

2. Two environments

Following Environments, this uses Terraform workspaces named staging and production (or, in Pulumi, stacks with the same names). Staging runs with pay_with_credit_card = false and the smallest product tier, so every plan can be rehearsed for free; production runs with the real tier and real billing.

3. Where state lives

Following State, remote backends, and team collaboration, this deployment uses the recommended path for both tools rather than the advanced, unverified one: Terraform Cloud for the Terraform version, Pulumi Cloud (the default) for the Pulumi version. Both give you locking and history without needing to verify a third party’s S3 compatibility first.

4. Secrets

Following Manage secrets the right way: the Biznet GIO API token lives in two GitHub Actions environments, staging and production, the latter with required reviewers turned on. The Pulumi version also needs PULUMI_ACCESS_TOKEN alongside it.

5. The module

Extending Reusable modules and components with storage alongside the VM. storage_label is its own input, kept separate from the environment name, because biznetgio_object_storage’s label field has its own 6-16 character constraint independent of how long your environment names are.
modules/app-stack/variables.tf:
modules/app-stack/main.tf:
modules/app-stack/outputs.tf:
Root main.tf calls it once per workspace. vm_product_id is picked by name rather than products[0] - see Understanding the product catalog for why, and the NEO Lite Pro catalog for the full pricing table. storage_product_id = 8 stays a hardcoded literal because Object Storage has no catalog data source at all - see why:

6. The pipeline, with a deliberate promotion step

Extending CI/CD with GitHub Actions to two environments instead of one. Staging applies automatically on every merge, since nothing there costs real money or serves real traffic; production needs the required reviewer configured on its GitHub environment:
needs: apply_staging means production only runs after staging has already applied cleanly; the environment: production required reviewer still pauses it for approval regardless.

7. Guardrails

Everything from Testing, validation, and guardrails applies here unchanged: terraform fmt -check and terraform validate on every PR, the plan posted as a comment, and a reviewer specifically checking that no one-shot trigger or create-only field changed by accident before approving production.

8. Wiring it into your systems

These providers provision and manage lifecycle; they are not an inventory system, so a couple of things are deliberately not resource attributes:
  • SSH access: the console user and password you set (ssh_and_console_user / console_password) plus the keypair’s private_key output are what authenticate you. The assigned public IP isn’t exposed as a resource attribute on biznetgio_neolite_pro_vm; find it in the NEO Lite Pro entry in the portal after the first apply, and record it in whatever inventory your systems already use.
  • App configuration: hand the object storage credential’s access_key and secret_key outputs to your app as environment variables or a secret store, pointed at the region’s S3-compatible endpoint (nos.<region>.neo.id), exactly as the Object Storage reference describes for real workloads.

9. Operating it afterward

  • Drift: a scheduled GitHub Actions workflow (on: schedule) running only plan/preview, not apply, catches changes made outside this pipeline, for example someone editing a resource directly in the portal. Post a comment or alert if the diff is non-empty.
  • Scaling: grow the VM by changing vm_product_id (triggers a change-package call), grow its disk with disk_size (grow-only), grow storage with quota (grow-only). None of these require recreating the resource.
  • Teardown: destroying production is exactly as real as applying it. Send it through the same pull request and review flow, never as a one-off local command.

Where to go from here

  • Run it yourself: the example repos - this whole stack, plus every other product line, as runnable copy-paste examples with CI/CD included
  • FAQ for the operational questions that come up once this is running
  • Triggers and actions for the destructive one-shot actions this stack deliberately doesn’t automate
  • Every tutorial in this track, if any single step above needs more depth than the summary here gives it