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

# Example — AWS VPC

This walkthrough authors a richer template than [Azure Resource Group](/cspm-deploy/create-custom-templates/example-azure-resource-group). It exercises every advanced feature of `template.json`:

* `list(object)` with `min_items`
* Nested `object` inside a `list(object)`
* Deeply nested `list(object)` (subnet → NACL → ingress/egress rules)
* `options` enums for protocol and action choices
* `optional(...)` Terraform syntax for non-required nested fields

The files match the shipped [`tpl-aws-vpc`](https://github.com/Amnify/terraform-template/tree/main/templates/aws/vpc) verbatim. Treat this as the canonical reference for advanced authoring.

The VPC template provisions:

* One `aws_vpc`.
* One or more `aws_subnet` (the user defines them; at least one is required).
* An optional internet gateway (one of the subnets opts in via `create_igw`).
* An optional NAT gateway (one of the subnets opts in via `create_nat_gateway`).
* Optional Network ACLs per subnet, with custom ingress/egress rules.
* Optional route tables per subnet.

***

## 1. Directory Layout

```
templates/
└── aws/
    └── my-vpc/
        ├── main.tf
        └── template.json
```

***

## 2. `main.tf`

The full file is long. It is broken down here into the patterns that matter for template authoring; the complete source is in the [shipped template](https://github.com/Amnify/terraform-template/blob/main/templates/aws/vpc/main.tf).

### Provider and required-providers

```hcl theme={null}
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.region
}
```

### Top-level variables

```hcl theme={null}
variable "region" {
  description = "AWS region"
  type        = string
  default     = "eu-west-1"
}

variable "vpc_name" {
  description = "Name of the VPC"
  type        = string
}

variable "cidr" {
  description = "CIDR block for the VPC"
  type        = string
  default     = "10.0.0.0/16"
}

variable "tags" {
  description = "Tags to apply to resources"
  type        = map(string)
  default     = {}
}
```

### The deeply-nested `subnets` variable

The interesting one. `subnets` is a `list(object)` where each element has an optional NACL object (which itself contains lists of rules) and an optional route table.

```hcl theme={null}
variable "subnets" {
  description = "List of subnets to create"
  type = list(object({
    name                    = string
    cidr                    = string
    availability_zone       = string
    map_public_ip_on_launch = bool
    create_igw              = optional(bool, false)
    create_nat_gateway      = optional(bool, false)
    nacl = optional(object({
      ingress_rules = list(object({
        rule_number      = number
        action           = string
        protocol         = string
        source_cidr      = string
        destination_cidr = string
        from_port        = number
        to_port          = number
      }))
      egress_rules = list(object({
        rule_number      = number
        action           = string
        protocol         = string
        source_cidr      = string
        destination_cidr = string
        from_port        = number
        to_port          = number
      }))
    }))
    route_table = optional(object({
      routes = list(object({
        destination_cidr = string
        target_type      = string
        target_id        = optional(string, "")
      }))
    }))
  }))

  validation {
    condition     = length(var.subnets) > 0
    error_message = "At least one subnet must be defined."
  }
}
```

Notes on the pattern:

* **`optional(bool, false)`** and **`optional(string, "")`** make a nested field optional with a default. The user can omit them in the UI; Terraform treats them as `false` / `""`.
* **`optional(object({...}))`** with no default makes the entire nested object optional — when omitted, the value is `null` and you check `if s.nacl != null` in your `locals`.
* **`validation`** at the bottom enforces `min_items` at plan time. The same constraint is also expressed in `template.json` so it surfaces in the UI before plan runs.

### Resources, locals, outputs

These are standard Terraform — see the [full source](https://github.com/Amnify/terraform-template/blob/main/templates/aws/vpc/main.tf) for the resource declarations. The key idea: rich nested input → flat resources via `for_each` and `flatten` over `locals`.

***

## 3. `template.json`

This is where the advanced manifest features earn their keep. The full file ships at [aws/vpc/template.json](https://github.com/Amnify/terraform-template/blob/main/templates/aws/vpc/template.json). Walk through it section by section.

### Top-level

```json theme={null}
{
  "id": "tpl-aws-my-vpc",
  "name": "AWS VPC",
  "description": "Deploy an AWS Virtual Private Cloud with configurable subnets, internet/NAT gateways, NACLs, and route tables.",
  "category": "infrastructure",
  "cloud_provider": "aws",
  "iac_type": "terraform",
  "icon": "Network",
  "variables": [ /* ... */ ]
}
```

### `region` with `options`

User-facing region selector with a curated dropdown.

```json theme={null}
{
  "name": "region",
  "type": "string",
  "description": "AWS region",
  "default": "eu-west-1",
  "required": true,
  "sensitive": false,
  "options": [
    "eu-west-1", "eu-central-1", "us-east-1", "us-east-2",
    "us-west-2", "ap-southeast-1", "eu-west-2"
  ]
}
```

### `subnets` — `list(object)` with `min_items`

The outer shape. Note `min_items: 1` enforces "at least one subnet" in the UI.

```json theme={null}
{
  "name": "subnets",
  "type": "list(object)",
  "description": "List of subnets to create",
  "required": true,
  "sensitive": false,
  "min_items": 1,
  "schema": [ /* nested fields, below */ ]
}
```

### Subnet fields — primitives

Inside the `schema`:

```json theme={null}
{ "name": "name",                    "type": "string", "required": true,  "sensitive": false },
{ "name": "cidr",                    "type": "string", "required": true,  "sensitive": false },
{ "name": "availability_zone",       "type": "string", "required": true,  "sensitive": false },
{ "name": "map_public_ip_on_launch", "type": "bool",   "required": true,  "sensitive": false, "default": false },
{ "name": "create_igw",              "type": "bool",   "required": false, "sensitive": false, "default": false },
{ "name": "create_nat_gateway",      "type": "bool",   "required": false, "sensitive": false, "default": false }
```

### Subnet `nacl` — nested `object` with deeply nested `list(object)`

The NACL is itself an `object` (`required: false`), which in turn contains two `list(object)` fields. Each rule has primitives and option-driven enums:

```json theme={null}
{
  "name": "nacl",
  "type": "object",
  "description": "Optional Network ACL for this subnet. Omit to use the VPC default NACL (allow-all).",
  "required": false,
  "sensitive": false,
  "schema": [
    {
      "name": "ingress_rules",
      "type": "list(object)",
      "description": "Inbound rules (NACLs are stateless — remember to allow return traffic in egress)",
      "required": true,
      "sensitive": false,
      "schema": [
        { "name": "rule_number",      "type": "number", "required": true, "sensitive": false, "description": "Rule priority (lowest first)" },
        { "name": "action",           "type": "string", "required": true, "sensitive": false, "options": ["allow", "deny"] },
        { "name": "protocol",         "type": "string", "required": true, "sensitive": false, "options": ["6", "17", "-1", "1"], "description": "6=TCP, 17=UDP, -1=all, 1=ICMP" },
        { "name": "source_cidr",      "type": "string", "required": true, "sensitive": false },
        { "name": "destination_cidr", "type": "string", "required": true, "sensitive": false },
        { "name": "from_port",        "type": "number", "required": true, "sensitive": false },
        { "name": "to_port",          "type": "number", "required": true, "sensitive": false }
      ]
    },
    {
      "name": "egress_rules",
      "type": "list(object)",
      "description": "Outbound rules",
      "required": true,
      "sensitive": false,
      "schema": [ /* same shape as ingress_rules */ ]
    }
  ]
}
```

This is three levels deep: subnet → nacl (object) → ingress\_rules (list of object) → fields. Amnify renders this with nested groups and add/remove buttons at each list level. There is no hard depth limit.

### Subnet `route_table` — optional `object`

```json theme={null}
{
  "name": "route_table",
  "type": "object",
  "description": "Optional custom route table for this subnet. Omit to use the VPC main route table.",
  "required": false,
  "sensitive": false,
  "schema": [
    {
      "name": "routes",
      "type": "list(object)",
      "description": "Route entries",
      "required": true,
      "sensitive": false,
      "schema": [
        { "name": "destination_cidr", "type": "string", "required": true,  "sensitive": false },
        {
          "name": "target_type",
          "type": "string",
          "required": true,
          "sensitive": false,
          "options": ["internet_gateway", "nat_gateway", "vpc_peering", "transit_gateway", "network_interface"]
        },
        {
          "name": "target_id",
          "type": "string",
          "required": false,
          "sensitive": false,
          "description": "Not needed for internet_gateway or nat_gateway (auto-resolved). Required for vpc_peering, transit_gateway, network_interface."
        }
      ]
    }
  ]
}
```

### A note on conditional requirements

The `target_id` field above is *conceptually* required only for some `target_type` values (`vpc_peering`, `transit_gateway`, `network_interface`) but optional for others (`internet_gateway`, `nat_gateway`, where Amnify auto-resolves the ID).

Amnify's `condition` field on a variable can hide or show a field based on a sibling's value, but it does not currently support multi-value conditions ("show when target\_type is one of \[a, b, c]"). The pattern used by the shipped template is:

* Mark `target_id` as `required: false` in `template.json`.
* Document the per-`target_type` requirement in the `description`.
* Enforce the rule at plan time with a Terraform `validation` block.

```hcl theme={null}
validation {
  condition = alltrue([
    for s in var.subnets : s.route_table == null ? true : alltrue([
      for r in s.route_table.routes :
        contains(["internet_gateway", "nat_gateway"], r.target_type) || r.target_id != ""
    ])
  ])
  error_message = "target_id is required when target_type is vpc_peering, transit_gateway, or network_interface."
}
```

This is a common pattern: the manifest captures the structural shape, Terraform enforces cross-field semantics.

***

## 4. Validate Locally

```bash theme={null}
cd templates/aws/my-vpc
terraform init -backend=false
terraform validate
```

For deeply nested templates, also try `terraform plan` against a hand-crafted `terraform.tfvars.json` to make sure the variable shape lines up. Example:

```json theme={null}
{
  "region": "eu-west-1",
  "vpc_name": "demo",
  "cidr": "10.0.0.0/16",
  "subnets": [
    {
      "name": "public-a",
      "cidr": "10.0.1.0/24",
      "availability_zone": "eu-west-1a",
      "map_public_ip_on_launch": true,
      "create_igw": true
    }
  ],
  "tags": {}
}
```

```bash theme={null}
terraform plan -var-file=terraform.tfvars.json -backend=false
```

(If you have AWS credentials configured locally; otherwise `validate` is enough to catch type mismatches.)

***

## 5. Publish

```bash theme={null}
git add templates/aws/my-vpc
git commit -m "Add custom AWS VPC template"
git push origin main
```

Then click **Sync Templates** in Amnify Deploy.

***

## Lessons From This Template

If you take only three things from this example, take these:

1. **Mirror the `.tf` and `template.json` shapes exactly.** Every nested `optional(object({...}))` in Terraform corresponds to an `object` with `required: false` in JSON. Every `list(object({...}))` corresponds to `list(object)` with a `schema` array.
2. **Use `options` for enums.** Anywhere your Terraform takes one of a known set of strings, expose it as `options`. The UI dropdown prevents typos and the user does not need to memorize the valid values.
3. **Combine manifest validation with Terraform `validation`.** The manifest catches structural issues (missing required field, empty list); Terraform `validation` blocks catch semantic issues (cross-field rules). Use both.

***

## Next Steps

* [template.json Reference](/cspm-deploy/create-custom-templates/template-json-reference) — for any field this example didn't exercise (e.g. `condition`, `resource_type`).
* [Variable Types & Secrets](/cspm-deploy/create-custom-templates/variable-types-and-secrets) — for `sensitive` variables, which the VPC template doesn't use.
