> ## 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.

# Module dan component yang reusable

> Bungkus infrastruktur yang berulang jadi satu Terraform module atau Pulumi component

Begitu dev, staging, dan production butuh bentuk yang sama, satu keypair plus satu VM, cuma beda sizing dan beberapa flag, copy-paste bentuk itu tiga kali artinya benerin bug-nya tiga kali juga. Terraform module dan Pulumi component ngebiarin lu nulis bentuknya sekali terus instantiate 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 sekali per environment dari root module. `var.console_password` di sini itu persis yang udah dideklarasiin lengkap di tutorial [quickstart](/id/quickstart) dan [project structure](/id/tutorials/project-structure). `product_id` dipilih pake nama per environment, pola yang sama kayak di [Environment](/id/tutorials/environments#terraform-workspace) - jangan pernah pass `products[0]` langsung ke module, soalnya module gak punya cara buat warning ke caller-nya kalau value itu asal-asalan:

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

locals {
  # Nama paket NEO Lite beneran - liat /id/products/neolite buat tabel harga lengkap.
  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 dan production pake tier yang sama, jadi satu OS list cukup buat berdua
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"` dulu ke output katalog akun lu sendiri - liat [Langkah 1 di overview katalog](/id/products/overview#langkah-1-liat-katalog-lu-sendiri-sebelum-filter-apapun). Source lokal `./modules/...` udah cukup buat satu repository. Publish ke module registry bisa aja, tapi cuma worth it kalau lebih dari satu repository butuh module yang sama.

## Pulumi component

Yang setara di Pulumi itu `ComponentResource`, dicontohin di sini pake 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 }` daftarin keypair dan VM sebagai child dari component ini di resource tree Pulumi, jadi `pulumi up` dan console Pulumi Cloud nunjukin mereka nested di bawahnya, bukan sebagai dua top-level resource yang gak nyambung.

Instantiate satu per stack. `config` itu persis yang udah dideklarasiin lengkap di [quickstart Pulumi](/id/pulumi-quickstart). `productId` dipilih pake nama, pola yang sama kayak di [Environment](/id/tutorials/environments#pulumi-stack) - jangan pernah pass `products[0]` langsung ke component:

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

// Nama paket NEO Lite beneran - liat /id/products/neolite buat tabel harga
// lengkap. Confirm dulu ke output katalog akun lu sendiri.
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;
```

## Langkah berikutnya

* [Testing, validasi, dan guardrail](/id/tutorials/testing-and-safety) - tangkep kesalahan sebelum module ini manggil API beneran
