Part 1: the Terraform repo
One folder per product line, each an independent Terraform root module. The provider is pinned to0.1.0 and required_version is >= 1.0 in every folder.
1.1 The five-file anatomy
Every folder contains the same five files. Their semantics, exactly:versions.tf
required_versionis the minimum Terraform CLI version. The provider itself uses the Terraform Plugin Framework.required_providersmaps the local namebiznetgioto a registry address and pins the version.terraform initdownloads exactly0.1.0from the Terraform Registry.- The empty
provider "biznetgio"block configures the provider with zero inline arguments. The provider reads theBIZNETGIO_API_KEYenvironment variable itself, which is why no token ever appears in a file.
variables.tf
Declares every input with types and defaults. The three recurring variables across folders:
ssh_and_console_user: string, default"adminuser"(or"root"ingpu/). The comments note the API rule: 6 to 32 characters, letters, numbers, dash, dot only.console_password: string,sensitive = true, deliberately no default. The only ways in areTF_VAR_console_passwordor the gitignoredterraform.tfvars. This exists inneolite/,neolite-pro/,gpu/, andcomplete/;baremetal/andobject-storage/authenticate with a keypair only and have no such variable.pay_with_credit_card: bool, defaultfalse.falsestill creates the real resource, it just leaves the invoice unpaid in the portal. See Billing and orders.
terraform.tfvars.example
A committed template of real values. The .gitignore ignores *.tfvars but re-includes *.tfvars.example, so the template is committed and the filled-in copy never is. The example file contains a placeholder password only.
main.tf
The declarations themselves. Two kinds of declarations appear:
datablocks are read-only lookups.data "biznetgio_neolite_products" "all" {}queries the API for the current catalog; the result is referenced asdata.biznetgio_neolite_products.all.products[0].product_id.resourceblocks create and manage things. References between them, likekeypair_id = biznetgio_neolite_keypair.main.keypair_id, are what Terraform turns into a dependency graph and an API call order.localsblocks compute values once, like pickingproducts[0]for the demo.
outputs.tf
Publishes values after apply. Sensitive outputs (keypair_private_key, secret_key, console_url, openvpn_config) are marked sensitive = true so they render masked in the console and in JSON output.
1.2 neolite/: the full NEO Lite story
Five resources and five data sources, wired in the order you would actually use them:
The dependency chain reads as: products → os_list → keypair → vm → disk, snapshot → restore. The
snapshot_id reference is what guarantees the snapshot exists before the restore is ordered.
Commented-out VM options, each fired only when its value changes: power_state (start/stop/suspend/resume/shutdown), rebuild_os (wipes the disk and reinstalls), migrate_to_pro (one-way move to a NEO Lite Pro product id), disk_size (grow-only absolute target).
Outputs: ip_available, vm_status, vm_id (documented as the value for terraform import), keypair_private_key (sensitive), restored_vm_status, change_package_options_raw (sensitive), storage_upgrade_options_raw (sensitive).
1.3 neolite-pro/: same shape, dedicated tier
Four resources and five data sources. Identical wiring to NEO Lite with these exact differences:- No
biznetgio_neolite_pro_vm_from_snapshotexists. Pro snapshots restore only through the portal today; the file’s banner says so. biznetgio_neolite_pro_disk.extrahas a 30 GB minimum instead of 60 GB,service_name = "pro-extra-disk", sameproduct_id = 60.vm_name = "pro-example"(the shorter name also fits the 6 to 16 character limit).- Migration into Pro comes from the Lite side via
migrate_to_proon theneolite/example. - Outputs are the same set minus
restored_vm_status.
variables.tf reuses the same three variables and says so in a comment, pointing at neolite/variables.tf for the full explanation.
1.4 baremetal/: dedicated hardware
Five resources and three data sources, keypair-only authentication (no console password anywhere in the folder;variables.tf has only pay_with_credit_card):
Commented-out server options:
power_state (on/off), reset_trigger (one-shot reboot, change the string to re-fire), rebuild_os (wipes the disk; valid values from the rebuild OS list data source above).
Outputs: server_status, server_ip_address, keypair_private_key (sensitive), valid_rebuild_os_images, additional_ip_address, elastic_storage_status, openvpn_config (sensitive).
1.5 gpu/: the two quirks, concretely
Two resources and three data sources.variables.tf sets ssh_and_console_user to "root" (the GPU images use root).
biznetgio_gpu_keypairexports nokeypair_idattribute. It exportsid(which already is the keypair id),public_key, andprivate_key. The instance references.id.subscriptionandon_demandare object-typed attributes, not blocks. They are assigned with=and exactly one of the two must be set; Terraform rejects a plan with neither or both. The commented-outon_demandform ison_demand = { additional_hours = 0 }and bills hourly. Two one-shot triggers are commented out:rebuild_trigger(wipes the disk) andreserve_additional_hours_trigger.
data.biznetgio_gpu_console.console is side-effecting (every read mints a brand-new one-time console session; safe in terraform output, never reference it from anything evaluated during plan diffing) and data.biznetgio_gpu_graph.graph with timeframe = "hour".
Outputs: gpu_status, keypair_private_key (sensitive), console_url (sensitive), monitoring_graph.
1.6 object-storage/: S3-compatible storage
Four resources and three data sources, no keypair and no password;variables.tf has only pay_with_credit_card:
The upload resource’s comment sets the boundary: the control-plane upload is fine for one small file; anything bigger or bulk should use real S3 tooling (aws-cli, rclone) with the credential, pointed at
nos.<region>.neo.id.
Outputs: storage_status, bucket_name, access_key (sensitive), secret_key (sensitive), active_instances (sensitive because each item carries a redacted raw JSON field, and Terraform treats anything containing a sensitive value as sensitive as a whole).
1.7 complete/: the module
The rootmain.tf does catalog lookups then one module call with ten arguments; variables.tf at the root only forwards console_password and pay_with_credit_card; root outputs.tf passes through every module output.
Inside modules/app-stack/:
main.tfdeclares five resources:biznetgio_neolite_pro_keypair.this(named"${var.name}-key"),biznetgio_neolite_pro_vm.this(vm_name = var.name),biznetgio_object_storage.this,biznetgio_object_storage_bucket.this("${var.name}-assets",acl = "private"), andbiznetgio_object_storage_credential.this. All references use the module’s own resources, so the module is fully self-contained.variables.tfdeclares ten inputs:name(VM name and prefix for bucket and credential, 6 to 16 characters),storage_label(separate on purpose because the storage label has its own 6 to 16 character limit),vm_product_id,select_os,storage_product_id,cycle(default"m"),ssh_and_console_user(default"adminuser"),console_password(sensitive, no default),pay_with_credit_card(defaultfalse),storage_quota(default10).outputs.tfexportsvm_status,bucket_name,access_key(sensitive),secret_key(sensitive),keypair_private_key(sensitive). The comment on the sensitive ones says the module caller owns the responsibility of saving them.- Its own
terraform { required_providers { biznetgio = { source = "registry.terraform.io/shirasakaren/biznetgio" } } }block. Without it, Terraform assumes the defaulthashicorp/*namespace for providers referenced inside a child module and fails with a missing provider error. Any module copied out of this repo must keep that block.
1.8 The workflow
.github/workflows/ci.yml runs terraform fmt -check, terraform init, and terraform validate unconditionally per selected folder, then conditionally plan, apply, or destroy with BIZNETGIO_API_KEY and TF_VAR_console_password secrets. The full line-by-line explanation lives in Pipelines.
Part 2: the Pulumi repo
Six examples, each written in six languages. Every folder is an independent Pulumi project with its ownPulumi.yaml.
2.1 Project anatomy per language
Pulumi.yaml in every folder:
name is unique per folder, runtime selects the language engine (nodejs, python, go, dotnet, java, or yaml). Around it, each language contributes:
The csproj filename matches the
name in Pulumi.yaml (for example biznetgio-example-gpu.csproj). The .gitignore excludes node_modules/, __pycache__/, venv/, bin/, obj/, target/, *.class, and Pulumi.*.yaml with !Pulumi.yaml re-included, so stack state and secrets stay out of git.
2.2 The shared program model
Every program does the same five things, in the same order:- Config. Read the stack settings.
consolePasswordis required and secret (config.requireSecret("consolePassword"),cfg.RequireSecret(...),config.require_secret(...));payWithCreditCardis optional boolean defaulting to false. Set them withpulumi config set --secret consolePassword <value>. - Catalog lookups. Invoke the product catalog, then the OS list for the first product, sometimes IP availability. Functions are read-only API calls.
- Resources. Create the keypair, the VM, and everything else. Passing one resource’s output into another’s input is what builds the dependency graph.
- Outputs. Export the values you care about.
- Run. Each language has its own entry mechanism: TypeScript and Python run top-level; Go wraps everything in
pulumi.Run(func(ctx *pulumi.Context) error {...})with(value, error)returns on every call; .NET uses top-level statements returningawait Deployment.RunAsync(() => {...})and exports via a returnedDictionary<string, object?>; Java wraps inPulumi.run(App::stack)withctx.export(...)calls; YAML declaresconfiguration,variables,resources, andoutputssections instead of code.
2.3 The account id conversion
Resourceids are always strings in Pulumi, but the upstream API sends numbers, so the provider types several “account id” style inputs as numbers. Passing a string id into them is a compile error in every typed language, which is why each language carries a conversion helper. The exact implementations:
- TypeScript
- Python
- Go
- .NET
- Java
- YAML
Where it does not apply, just as important:
keypairIdon the VPS and baremetal VMs is string-typed:NeoliteKeypair,NeoliteProKeypair, andBaremetalKeypairall expose a stringkeypairIdthat passes straight through.- Every Object Storage
accountId(ObjectStorageBucket,ObjectStorageCredential,ObjectStorageObject, and the three catalog functions) is string-typed. No conversion anywhere inobject-storage/orcomplete/. GpuConsoleandGpuGraphtakeaccountIdas a string; the GPU examples passgpu.iddirectly.GpuKeypairexposes nokeypairIdat all, onlyid,publicKey, andprivateKey. The GPU examples convertkeypair.idwith the same helper (inline, askeypair.id.apply((id) => Number(id))in TypeScript,keypair_id = keypair.id.apply(int)in Python, and equivalents elsewhere) and comment why.
object-storage/: running plain code like .length against an Output’s eventual value requires .apply (TypeScript), .apply(len) (Python), ApplyT (Go), .Apply (.NET), .applyValue (Java), or fn::length (YAML). The examples use it to export the count of active instances, buckets, and credentials.
2.4 The examples, inventory per product line
neolite/: products → osList → ipAvailability → keypair → vm (with the four commented-out optionspowerState, rebuildOs, migrateToPro, diskSize) → disk (60 GB minimum, productId: 60) → snapshot → restored VM from snapshot → the two raw JSON functions. Exports: ipAvailable, vmStatus, vmId, keypairPrivateKey, diskStatus, restoredVmStatus, changePackageOptionsRaw, storageUpgradeOptionsRaw.
neolite-pro/: the same minus the from-snapshot resource, with the 30 GB disk minimum and serviceName: "pro-extra-disk". Exports add diskStatus and snapshotStatus, drop restoredVmStatus.
baremetal/: products → keypair → server (publicIp: 1, commented powerState, resetTrigger, rebuildOs) → rebuild OS list (converted accountId) → OpenVPN (no inputs) → additional IP (productId: 10, region: "wjv-1") → assignment (both ids converted) → elastic storage (productId: 20, size: 100, converted metalAccountId). Exports: keypairPrivateKey, serverStatus, serverIpAddress, validRebuildOsImages, openvpnConfig, additionalIpAddress, assignmentStatus, elasticStorageStatus.
gpu/: products → keypair → instance. The billing mode is one property: subscription: { cycle: "m" } (TypeScript/Python object literals), subscription=biznetgio.GpuSubscriptionArgsArgs(cycle="m") (Python’s SDK names the args classes with a doubled ArgsArgs), Subscription: biznetgio.GpuSubscriptionArgsArgs{Cycle: pulumi.String("m")} (Go), Subscription = new GpuInstanceSubscriptionArgs { Cycle = "m" } (.NET), .subscription(GpuSubscriptionArgs.builder().cycle("m").build()) (Java, imported from ren.shirasaka.biznetgio.inputs), and subscription: { cycle: m } (YAML). The alternative onDemand (with additionalHours) is commented out in every language, as are rebuildTrigger and reserveAdditionalHoursTrigger. Then the side-effecting console function and the graph (timeframe: "hour"), both with string accountId. Exports: keypairPrivateKey, gpuStatus, consoleUrl, monitoringGraph.
object-storage/: subscription (productId: 8, quota: 10) → bucket (acl: "public-read") → credential (active: true) → object upload → the three catalog lookups with counts. The source path differs per language: path.join(__dirname, "index.html") (TypeScript), str(pathlib.Path(__file__).parent / "index.html") (Python), "index.html" relative (Go and Java, where pulumi up runs from the project folder), Path.Combine(AppContext.BaseDirectory, "index.html") (.NET), and source: ./index.html (YAML). Exports: storageStatus, bucketName, accessKey, secretKey, objectKey, activeInstanceCount, bucketCount, credentialCount.
2.5 The complete/ example and the component
Thecomplete/ folder wraps a NEO Lite Pro VM plus Object Storage into a reusable AppStack component. The caller (index.ts, __main__.py, main.go, Program.cs, App.java) does the catalog lookups, constructs one AppStack with args, and re-exports its five outputs: vmStatus, bucketName, accessKey, secretKey, keypairPrivateKey.
The component’s internals are identical across languages:
- Registration with the type string
"biznetgio-example:index:AppStack". - Five children: keypair, VM, storage, bucket, credential, each named
${name}-key,${name}-vm,${name}-storage,${name}-assets,${name}-cred. - Each child gets the component as parent:
{ parent: this }(TypeScript),opts=pulumi.ResourceOptions(parent=self)(Python),pulumi.Parent(app)(Go),new CustomResourceOptions { Parent = this }(.NET),CustomResourceOptions.builder().parent(this).build()(Java). The parent wiring groups everything under one tree sopulumi destroytears the whole app down in order. - Outputs are published with
registerOutputs({...})(TypeScript),self.register_outputs({...})(Python),ctx.RegisterResourceOutputs(app, pulumi.Map{...})(Go),RegisterOutputs(new Dictionary<string, object?>{...})(.NET),this.registerOutputs(Map.of(...))(Java). - Args: TypeScript uses an
AppStackArgsinterface; Python a plain class withstorage_quotadefaulting toNoneand the component falling back to 10; Go a struct withStorageQuota *intwhere nil means the 10 GB default; .NET a class with nullableInput<int>? StorageQuotaandargs.StorageQuota ?? 10; Java a builder class (AppStackArgs.java) wherestorageQuotadefaults toInput.of(10); the bucketaclisprivateand the VMvmNameis the component name itself.
yaml/complete/Pulumi.yaml declares the same five resources flat with a comment explaining the difference.
2.6 The workflow
.github/workflows/ci.yml builds each language with its toolchain, then optionally runs preview/up/destroy through pulumi/actions@v7 against the dev stack. Full details in Pipelines.
Part 3: cross-cutting technical reference
The billing model
Every orderable resource takescycle ("m" monthly, "y" annual) and payWithCreditCard. In Pulumi the same fields are cycle and payWithCreditCard; the default-false billing rule applies to both tools. GPU adds the subscription versus onDemand split, and additional_hours reserves extra on-demand hours on top of the default balance.
The one-shot and destructive options
All commented out in every language, all firing only when their value changes:Side-effecting reads
Onlybiznetgio_gpu_console / gpuConsole mints state on read: every evaluation creates a fresh one-time console session. The examples expose it as an output and warn never to reference it from anything evaluated during preview diffing. The raw JSON lookups (changePackageOptions, storageUpgradeOptions) are read-only.
Name limits enforced by the API
vm_name/vmName: 6 to 16 characters. service_name/serviceName (disks): 6 to 16. Object Storage label: 6 to 16. ssh_and_console_user: 6 to 32, letters, numbers, dash, dot only. The examples’ names (neolite-example, extra-disk, pro-extra-disk, gpu-example, metal-example, example) are all chosen to fit.
Secrets per tool
Terraform:console_password via TF_VAR_console_password or gitignored terraform.tfvars; provider auth via BIZNETGIO_API_KEY env. Pulumi: pulumi config set --secret consolePassword; stack state in gitignored Pulumi.<stack>.yaml; provider auth via BIZNETGIO_API_KEY env; CI adds PULUMI_ACCESS_TOKEN.
Part 4: complete file indexes
Terraform repo, every file:
Pulumi repo, every file:
That is everything in both repos. If you change any of it, the Conventions and the walkthroughs tell you how to do it safely.