# modules/controlplane

**Spinbox is experimental: APIs change and data may be lost.** What is built and what proves it is on [Status](https://spin-stack.github.io/website/reference/status.md); what Spinbox does not do is on [Limitations](https://spin-stack.github.io/website/overview/limitations.md).

> spin-terraform-aws, the network, bucket, database, control plane and proxy, used on its own.

<!-- Generated by tools/refgen from spin-terraform-aws:modules/controlplane/variables.tf. Edit the source, not this file. -->

Source: `github.com/spin-stack/spin-terraform-aws//modules/controlplane?ref=<tag>`. Generated from its `variables.tf` and `outputs.tf`.

## Variables

| Variable | Type | Default |
|---|---|---|
| [`spin_version`](#spin_version) | `string` | **required** |
| [`domain`](#domain) | `string` | **required** |
| [`identity_record_days`](#identity_record_days) | `number` | `400` |
| [`collector`](#collector) | `object({ metric_interval = optional(string, "60s") })` | `{}` |
| [`name`](#name) | `string` | `"spin"` |
| [`internal_zone`](#internal_zone) | `string` | `"spin.internal"` |
| [`acme_email`](#acme_email) | `string` | `""` |
| [`vpc_cidr`](#vpc_cidr) | `string` | `"10.42.0.0/16"` |
| [`availability_zones`](#availability_zones) | `number` | `3` |
| [`instance_type`](#instance_type) | `string` | `"t8i.medium"` |
| [`root_volume_gb`](#root_volume_gb) | `number` | `40` |
| [`admin_email`](#admin_email) | `string` | `""` |
| [`flow_logs`](#flow_logs) | `bool` | `true` |
| [`dns_query_logs`](#dns_query_logs) | `bool` | `false` |
| [`log_retention_days`](#log_retention_days) | `number` | `30` |
| [`proxy_allowed_cidrs`](#proxy_allowed_cidrs) | `list(string)` | `["0.0.0.0/0"]` |
| [`proxy_instance_type`](#proxy_instance_type) | `string` | `"t8i.micro"` |
| [`decommission`](#decommission) | `bool` | `false` |
| [`object_lock_days`](#object_lock_days) | `number` | `30` |
| [`database_lock_days`](#database_lock_days) | `number` | `14` |
| [`runner_policy`](#runner_policy) | `object({ shutdown_grace = optional(string, "100s") preemption_source = optional(string, "aws") })` | `{}` |
| [`installation_config`](#installation_config) | `string` | `""` |
| [`runner_idle_minutes`](#runner_idle_minutes) | `number` | `60` |
| [`quiet_hours`](#quiet_hours) | `string` | `""` |
| [`time_zone`](#time_zone) | `string` | `"UTC"` |
| [`tags`](#tags) | `map(string)` | `{}` |
| [`image_ids`](#image_ids) | `map(string)` | `{}` |
| [`monthly_budget_usd`](#monthly_budget_usd) | `number` | the module's own |
| [`budget_emails`](#budget_emails) | `list(string)` | `[]` |
| [`image_measurements`](#image_measurements) | `map(list(object({ pcr4 = string pcr7 = string pcr12 = string statement = optional(string) bundle = optional(string) # Why the build is not to be run, as measurements.json says of a revoked one: no machine of it # is trusted, and a release whose newest build is revoked is not installed (attested_key.tf). revoked = optional(string) })))` | the module's own |

### spin_version

The release the installation runs, as v&lt;YYYYMMDD>.&lt;N>: every machine boots the Spin OS image of it (spin-stack/ami), published into this account and tagged spin:version.

- Type: `string`
- **Required**

### domain

The base domain: the dashboard is app.&lt;domain>, workspaces are \*.ws.&lt;domain>.

- Type: `string`
- **Required**

### identity_record_days

How many days the record of each identity a host signs is kept (identity/issued/ in the volumes bucket): longer than the bucket's lock, and long enough to answer who took an identity when.

- Type: `number`
- Default: `400`

### collector

The collector: how often metrics are pushed to it. Which Alloy it is, is the image's.

- Type: `object({ metric_interval = optional(string, "60s") })`
- Default: `{}`

### name

The installation's name, one per region of an account: every resource's prefix, the SSM path (/spin/&lt;name>/...) each machine's document is at, and - with the region - every IAM name. Two installations of one account may share a name in two regions, and not in one (claim.tf).

- Type: `string`
- Default: `"spin"`

### internal_zone

The private zone the components reach each other by: cp.&lt;this> and proxy.&lt;this>, resolved only inside the VPC.

- Type: `string`
- Default: `"spin.internal"`

### acme_email

The address Let's Encrypt writes to; empty is admin@&lt;domain>.

- Type: `string`
- Default: `""`

### vpc_cidr

The VPC's range, a /16. One /20 public subnet per availability zone is carved from its first half, and one /24 for the proxy per zone from x.x.136.0 on.

- Type: `string`
- Default: `"10.42.0.0/16"`

### availability_zones

How many zones to make subnets in. The control plane, the proxy and the runners may each be started in any of them, which is what gives a group somewhere to go when a zone runs out.

- Type: `number`
- Default: `3`

### instance_type

The control plane's machine: the control plane, its database (PostgreSQL), the collector, and the stores of logs, traces and metrics it runs.

- Type: `string`
- Default: `"t8i.medium"`

### root_volume_gb

The control plane's disk: beside the read-only OS, /var takes the rest - the database, the logs' and metrics' stores and their caches. A new size is a new machine, replaced beside the old.

- Type: `number`
- Default: `40`

### admin_email

Who the first administrator signs in as; empty is admin@&lt;domain>. The one-time password is in the admin_password_parameter output's parameter.

- Type: `string`
- Default: `""`

### flow_logs

Keep the VPC's flow log in CloudWatch: every connection a security group accepted or refused. At a few hosts it is cents a month; a fleet whose workspaces move a lot of data pays about $0.50 a GB of flow records.

- Type: `bool`
- Default: `true`

### dns_query_logs

Keep the VPC resolver's query log in CloudWatch: every name looked up, a workspace's included. It is Route 53 Resolver's, and off unless asked for. Each installation that turns it on takes one of the ten CloudWatch Logs resource policies a region of an account may have.

- Type: `bool`
- Default: `false`

### log_retention_days

How long the flow and query logs are kept.

- Type: `number`
- Default: `30`

### proxy_allowed_cidrs

Where users may reach the proxy on 443 from: the dashboard, workspaces and SSH. Anywhere by default; an office's or a VPN's ranges close the installation to everyone else. The runners reach it from inside the VPC whatever this says, and 80 stays open for the ACME challenge.

- Type: `list(string)`
- Default: `["0.0.0.0/0"]`

### proxy_instance_type

The proxy's machine: Caddy and nothing else, the one address the internet reaches.

- Type: `string`
- Default: `"t8i.micro"`

### decommission

Set to true, and applied, before destroying the installation: its buckets lose the policies that refuse every object write from outside the VPC - the destroy runs from outside it - and a destroy empties every bucket, data included, bypassing the GOVERNANCE retention once the legal holds are lifted (README.md, "Removing an installation"). Never set on an installation that is to keep its data.

- Type: `bool`
- Default: `false`

### object_lock_days

The bucket's default Object Lock retention, in GOVERNANCE mode. A deleted object's bytes last this long, and the bucket's lifecycle (bucket.tf) removes them a day after.

- Type: `number`
- Default: `30`

### database_lock_days

The database archive's default Object Lock retention, in GOVERNANCE mode: how long what a delete or an overwrite left there can still be restored. The lifecycle removes it a day after (database_bucket.tf).

- Type: `number`
- Default: `14`

### runner_policy

The host policy a runner starts with when it joins: how long it stays up once told to stop - a little less than the two minutes a spot reclaim gives - and the cloud that announces a reclaim.

- Type: `object({ shutdown_grace = optional(string, "100s") preemption_source = optional(string, "aws") })`
- Default: `{}`

### installation_config

The installation's config file (spin's configs/spin-example.yaml), as YAML. This module adds
what it knows - the domain, who joins as a host, the autoscaling settings below - and writes
it where the control plane reads it at every start, which seeds the database from it before
it serves. What the file later says differently is shown in the dashboard to apply or
dismiss, and nothing is written over what an administrator decided.

- Type: `string`
- Default: `""`

### runner_idle_minutes

How long the fleet has no workspace running, starting or waiting before the runners' group is emptied.

- Type: `number`
- Default: `60`

### quiet_hours

HH:MM-HH:MM in time_zone when an idle fleet is emptied at once rather than after runner_idle_minutes. A workspace asked for then still brings a runner up. Empty is none.

- Type: `string`
- Default: `""`

### time_zone

The IANA zone quiet_hours are in.

- Type: `string`
- Default: `"UTC"`

### tags

Tags on everything this module creates.

- Type: `map(string)`
- Default: `{}`

### image_ids

Spin OS AMIs of your own, one per role - control-plane, runner and proxy - instead of those images.json names for spin_version in this region. Each must carry that release, laid for that role: a machine refuses a document of another release, and an image laid for another role.

- Type: `map(string)`
- Default: `{}`

### monthly_budget_usd

What the installation may cost in a month, in US dollars: an AWS Budget on its spin:installation tag (budget.tf), which the control plane reads and alerts reach. Null is no budget.

- Type: `number`
- Default: `null` (the module's own default)

### budget_emails

Who AWS emails when the month's cost passes 80 % or 100 % of monthly_budget_usd, or is forecast to pass it.

- Type: `list(string)`
- Default: `[]`

### image_measurements

The builds of each role's image - control-plane, runner and proxy - oldest first, each the PCRs an instance of it measures (its manifest's measurements, SHA384 in lowercase hex), instead of measurements.json's: the control plane's key is answered to the control plane's builds (attested_key.tf), a host joins only as the runner's, and the proxy is given its token only as the proxy's (host_attestation). The control plane's and the runner's also carry statement and bundle, base64, as spin-os images sign writes them: what their images' publisher signed of those PCRs. The last of a role's is the image image_ids names. Required with image_ids, and for a release published before its PCRs were.

- Type: `map(list(object({ pcr4 = string pcr7 = string pcr12 = string statement = optional(string) bundle = optional(string) # Why the build is not to be run, as measurements.json says of a revoked one: no machine of it # is trusted, and a release whose newest build is revoked is not installed (attested_key.tf). revoked = optional(string) })))`
- Default: `null` (the module's own default)

## Outputs

| Output | What it is |
|---|---|
| `name` | The installation's name, claimed in its region (claim.tf), which the runners' group and security group are named for. |
| `iam_name` | What the installation's IAM names begin with: its name and its region, since IAM is the account's and two regions may each have an installation of one name. The runners' role and instance profile are named for it. |
| `vpc_id` | The VPC the runners go in. |
| `subnet_ids` | One public subnet per zone, for the runners' group. |
| `security_group_id` | The control plane's group; the runners module opens 8080 on it to its own. |
| `session_manager_policy_arn` | Session Manager into a machine, and nothing else of SSM: what the runners' role is given in place of AmazonSSMManagedInstanceCore, which reads every parameter. |
| `url` | The control plane as runners reach it, the address its certificate names. |
| `ca_cert` | The installation's CA certificate, which every component trusts the control plane by. |
| `runner_document_arn` | The runners' document, the one object of the documents' bucket the runners' role reads. |
| `runner_user_data` | A runner's user data: its role and the runners' document, as the systemd credentials Spin OS's spin-boot takes. |
| `images` | The Spin OS images of the installation's release, by role - the control plane's, the runners' and the proxy's: each one's id and root device. The runners module boots the runner's. |
| `boot_log_group` | The CloudWatch log group every machine's boot writes to, a stream per &lt;role>/&lt;step>/&lt;instance-id>: a role's machines are one stream prefix. |
| `boot_log_policy_arn` | Writing a machine's boot to that group, and nothing else: the runners module attaches it to the runners' role. |
| `standard_vcpus` | The on-demand standard vCPUs the control plane's and the proxy's machines take at most: two of each while an update replaces one. The runners module counts them against the quota on-demand runners share. |
| `name_servers` | The domain's public zone's nameservers, which its registrar points at. |
| `domain` | The base domain; the runners' relay is tunnel.app.&lt;domain>, dialled at relay_dial. |
| `public_zone_id` | The domain's public zone, where modules/identity-issuer puts id.&lt;domain>. |
| `identity_key_arn` | The KMS key the installation's identity key is sealed under, which only an attested runner opens. |
| `identity_documents` | The commands that write the identity issuer's documents - discovery, key set, allowed signers and SPIFFE bundle - for modules/identity-issuer to publish: the identity key as the control plane keeps it in its bucket (its public half is all that is read) and the X.509 CA the first host to sign an SVID made beside it, then the documents from them. Run them with credentials that may read both, and commit what they write. |
| `internal_zone_id` | The private zone the components reach each other by, cp.&lt;internal_zone> and proxy.&lt;internal_zone>. |
| `relay_dial` | Where a runner dials the relay, host:port: the proxy inside the VPC, by the name the proxy of the moment points at itself. |
| `collector` | Where every process of the installation pushes its telemetry, host:port. Where the collector sends it is set in the dashboard. |
| `boundary_arn` | The permissions boundary every role of the installation carries; the runners module puts it on the runners' role. |
| `proxy_ip` | The proxy's elastic IP, where app.&lt;domain>, tunnel.app.&lt;domain> and \*.ws.&lt;domain> point: the one address of the installation the internet reaches. |
| `proxy_group` | The proxy's group of one. Its machine, for `aws ssm start-session --target` (it has no SSH): aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names &lt;this> --query 'AutoScalingGroups[0].Instances[0].InstanceId'. |
| `bucket` | The volumes' bucket: every workspace's disk, versioned under Object Lock, reached only through the VPC's endpoint. |
| `database_bucket` | The database's archive: the base backups and WAL the control plane ships as it writes, and restores from when its machine is replaced. Its role alone reaches it. |
| `controlplane_group` | The control plane's group of one. `aws autoscaling start-instance-refresh --auto-scaling-group-name &lt;this>` replaces its machine beside itself; its machine is reached with `aws ssm start-session`, as the proxy's is. |
| `admin_email` | Who the first administrator signs in as. |
| `admin_password_parameter` | The SSM SecureString holding the first administrator's one-time password: aws ssm get-parameter --with-decryption --name &lt;this>. The first sign-in asks for a new one. |
| `region` | The region everything here is in, read from the provider rather than asked for. |
| `runner_idle_minutes` | How long the fleet is idle before the runners' group is emptied, which is why a first workspace waits for a machine's boot. |
