> ## Documentation Index
> Fetch the complete documentation index at: https://biznetgio.creations.ren/llms.txt
> Use this file to discover all available pages before exploring further.

# Reusable modules and components

> Package repeated infrastructure into a Terraform module or Pulumi component

Once dev, staging, and production each need the same shape, a keypair plus a VM, with only the sizing and a couple of flags different, copy-pasting that shape three times means fixing a bug in it three times too. Terraform modules and Pulumi components let you write the shape once and instantiate it per environment.

## Terraform module

```
infra/
  modules/
    neolite-vm/
      main.tf
      variables.tf
      outputs.tf
  main.tf
```

`modules/neolite-vm/variables.tf`:

```hcl theme={null}
variable "vm_name" {
  type = string
}

variable "product_id" {
  type = number
}

variable "select_os" {
  type = string
}

variable "cycle" {
  type = string
}

variable "ssh_and_console_user" {
  type = string
}

variable "console_password" {
  type      = string
  sensitive = true
}

variable "pay_with_credit_card" {
  type    = bool
  default = false
}
```

`modules/neolite-vm/main.tf`:

```hcl theme={null}
resource "biznetgio_neolite_keypair" "this" {
  name = "${var.vm_name}-key"
}

resource "biznetgio_neolite_vm" "this" {
  vm_name               = var.vm_name
  product_id            = var.product_id
  select_os              = var.select_os
  keypair_id            = biznetgio_neolite_keypair.this.keypair_id
  cycle                 = var.cycle
  ssh_and_console_user  = var.ssh_and_console_user
  console_password      = var.console_password
  pay_with_credit_card  = var.pay_with_credit_card
}
```

`modules/neolite-vm/outputs.tf`:

```hcl theme={null}
output "vm_status" {
  value = biznetgio_neolite_vm.this.status
}

output "keypair_private_key" {
  value     = biznetgio_neolite_keypair.this.private_key
  sensitive = true
}
```

Instantiate it once per environment from the root module. `var.console_password` here is exactly what the [quickstart](/quickstart) and [project structure](/tutorials/project-structure) tutorials already declared in full. `product_id` is picked by name per environment, the same pattern used in [Environments](/tutorials/environments#terraform-workspaces) - never pass a module `products[0]` directly, since a module has no way to warn its caller that the value is arbitrary:

```hcl theme={null}
data "biznetgio_neolite_products" "all" {}

locals {
  # Real NEO Lite package names - see /products/neolite for the full pricing table.
  staging_product = [
    for p in data.biznetgio_neolite_products.all.products : p if p.name == "MS 4.2"
  ][0]
  production_product = [
    for p in data.biznetgio_neolite_products.all.products : p if p.name == "MS 4.2"
  ][0]
}

# staging and production use the same tier, so one OS list covers both
data "biznetgio_neolite_os_list" "ubuntu" {
  product_id = local.staging_product.product_id
}

module "web_staging" {
  source                = "./modules/neolite-vm"
  vm_name                = "web-staging"
  product_id            = local.staging_product.product_id
  select_os              = data.biznetgio_neolite_os_list.ubuntu.oss[0].name
  cycle                 = "m"
  ssh_and_console_user  = "adminuser"
  console_password      = var.console_password
  pay_with_credit_card  = false
}

module "web_production" {
  source                = "./modules/neolite-vm"
  vm_name                = "web-production"
  product_id            = local.production_product.product_id
  select_os              = data.biznetgio_neolite_os_list.ubuntu.oss[0].name
  cycle                 = "m"
  ssh_and_console_user  = "adminuser"
  console_password      = var.console_password
  pay_with_credit_card  = true
}
```

Confirm `"MS 4.2"` against your own account's catalog output first - see [Step 1 in the catalog overview](/products/overview#step-1-look-at-your-own-catalog-before-you-filter-anything). A local `./modules/...` source is enough for one repository. Publishing to a module registry is possible but only worth it once more than one repository needs the same module.

## Pulumi component

The equivalent in Pulumi is a `ComponentResource`, shown here in TypeScript:

```typescript theme={null}
import * as pulumi from "@pulumi/pulumi";
import * as biznetgio from "@shirasakaren/biznetgio";

export interface NeoliteWebServerArgs {
  productId: pulumi.Input<number>;
  selectOs: pulumi.Input<string>;
  cycle: pulumi.Input<string>;
  sshAndConsoleUser: pulumi.Input<string>;
  consolePassword: pulumi.Input<string>;
  payWithCreditCard?: pulumi.Input<boolean>;
}

export class NeoliteWebServer extends pulumi.ComponentResource {
  public readonly vmStatus: pulumi.Output<string>;
  public readonly keypairPrivateKey: pulumi.Output<string>;

  constructor(name: string, args: NeoliteWebServerArgs, opts?: pulumi.ComponentResourceOptions) {
    super("biznetgio-tutorials:index:NeoliteWebServer", name, {}, opts);

    const keypair = new biznetgio.NeoliteKeypair(`${name}-key`, {
      name: `${name}-key`,
    }, { parent: this });

    const vm = new biznetgio.NeoliteVm(`${name}-vm`, {
      vmName: name,
      productId: args.productId,
      selectOs: args.selectOs,
      keypairId: keypair.keypairId,
      cycle: args.cycle,
      sshAndConsoleUser: args.sshAndConsoleUser,
      consolePassword: args.consolePassword,
      payWithCreditCard: args.payWithCreditCard ?? false,
    }, { parent: this });

    this.vmStatus = vm.status;
    this.keypairPrivateKey = keypair.privateKey;
    this.registerOutputs({ vmStatus: this.vmStatus });
  }
}
```

`{ parent: this }` registers the keypair and VM as children of the component in Pulumi's resource tree, so `pulumi up` and the Pulumi Cloud console show them nested under it instead of as two unrelated top-level resources.

Instantiate one per stack. `config` is exactly what the [Pulumi quickstart](/pulumi-quickstart) already declared in full. `productId` is picked by name, the same pattern used in [Environments](/tutorials/environments#pulumi-stacks) - never pass a component `products[0]` directly:

```typescript theme={null}
const stack = pulumi.getStack();
const products = biznetgio.neoliteProductsOutput();

// Real NEO Lite package name - see /products/neolite for the full pricing table.
// Confirm it against your own account's catalog output first.
const productId = products.products.apply((items) => {
  const match = items.find((p) => p.name === "MS 4.2");
  if (!match) throw new Error("no NEO Lite product named 'MS 4.2' found");
  return match.productId;
});

const osList = biznetgio.neoliteOsListOutput({ productId: productId });

const web = new NeoliteWebServer(`web-${stack}`, {
  productId: productId,
  selectOs: osList.oss[0].name,
  cycle: "m",
  sshAndConsoleUser: "adminuser",
  consolePassword: config.requireSecret("consolePassword"),
  payWithCreditCard: stack === "production",
});

export const vmStatus = web.vmStatus;
```

## Next steps

* [Testing, validation, and guardrails](/tutorials/testing-and-safety) - catch mistakes before this module ever calls the real API
