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 namedstaging 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.
- Terraform
- Pulumi (TypeScript)
modules/app-stack/variables.tf:modules/app-stack/main.tf:modules/app-stack/outputs.tf: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’sprivate_keyoutput are what authenticate you. The assigned public IP isn’t exposed as a resource attribute onbiznetgio_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_keyandsecret_keyoutputs 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 onlyplan/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 withdisk_size(grow-only), grow storage withquota(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