Skip to content

Getting Started with Terraform ​

This guide stands up a minimal Frostmoln configuration with Terraform.

1. Configure the provider ​

Create main.tf:

hcl
terraform {
  required_providers {
    frostmoln = {
      source = "registry.terraform.io/frostmoln/frostmoln"
    }
  }
}

provider "frostmoln" {
  # Credentials are read from provider arguments or environment variables.
  # Generate an API key in the portal under Settings -> API Keys.
}

Using OpenTofu? Keep the source address fully qualified — see the note on the provider overview.

Provide your API key via the environment so it stays out of source control:

bash
export FROSTMOLN_API_KEY=fmk_...

Selecting a tenant ​

By default the provider manages resources in your account's default tenant. To target another tenant, set tenant_id on the provider — or the FROSTMOLN_TENANT_ID environment variable:

hcl
provider "frostmoln" {
  tenant_id = "00000000-0000-0000-0000-000000000000"
}

Targeting a tenant other than your default requires an fm CLI / OIDC session (fm auth login) whose user belongs to multiple tenants — an API key is bound to a single tenant. One tenant per provider instance; to manage more than one tenant in the same configuration, declare a second provider with an alias. Don't re-point tenant_id on a provider that already manages resources — use a separate aliased provider (and terraform state mv), or Terraform tries to recreate them in the new tenant.

2. Define resources ​

The smallest configuration that actually gives you a machine you can log into is a VPC, a gateway for it, a subnet, a security group, an SSH key and the instance itself. Add this to main.tf:

hcl
resource "frostmoln_vpc" "main" {
  name = "tf-vpc"
  cidr = "10.0.0.0/16"
}

# A VPC is an isolated network until a gateway is declared for it: no outbound
# internet and no platform DNS. Leave this out and the instance below still
# boots, but cloud-init cannot reach anything and name resolution fails.
resource "frostmoln_gateway" "main" {
  vpc_id = frostmoln_vpc.main.id
  mode   = "public_ip"
}

resource "frostmoln_subnet" "main" {
  name   = "tf-subnet"
  vpc_id = frostmoln_vpc.main.id
  cidr   = "10.0.1.0/24"
  zone   = "falkenberg"
}

resource "frostmoln_security_group" "web" {
  name   = "tf-web"
  vpc_id = frostmoln_vpc.main.id
}

resource "frostmoln_security_group_rule" "ssh" {
  security_group_id = frostmoln_security_group.web.id
  direction         = "ingress"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_cidr       = "203.0.113.4/32" # your own address, not 0.0.0.0/0
  description       = "SSH from the office"
}

resource "frostmoln_ssh_key" "laptop" {
  name       = "my-laptop"
  public_key = file(pathexpand("~/.ssh/id_ed25519.pub"))
}

data "frostmoln_image" "ubuntu" {
  name = "ubuntu-24.04"
}

resource "frostmoln_instance" "web" {
  name      = "tf-web"
  image_id  = data.frostmoln_image.ubuntu.id
  flavor_id = "gp1.small"
  zone      = "falkenberg"
  vpc_id    = frostmoln_vpc.main.id
  subnet_id = frostmoln_subnet.main.id

  security_groups = [frostmoln_security_group.web.id]
  ssh_key_names   = [frostmoln_ssh_key.laptop.name]
}

# An address of its own, so you can reach the instance from outside. The
# depends_on is what makes `terraform destroy` work: a gateway refuses to be
# removed while public IPs in its VPC still point at it, and without this edge
# nothing tells Terraform to release the address before the gateway.
resource "frostmoln_public_ip" "web" {
  instance_id = frostmoln_instance.web.id

  depends_on = [frostmoln_gateway.main]
}

output "web_address" {
  value = frostmoln_public_ip.web.address
}

A few things in there are worth knowing before you run it.

  • A VPC has no way to the internet until a gateway is declared for it.frostmoln_vpc carries no connectivity argument on purpose — in Terraform the connectivity choice is the presence or absence of frostmoln_gateway. A VPC without one is isolated in both directions, and because platform DNS and managed-service connectivity are reached over the same path, instances in it cannot resolve names either. That is the usual reason a first terraform apply looks like it worked and then cloud-init fails on every package it tries to install. See Gateway.
  • image_id and flavor_id take IDs, not display names. A flavor's ID is its SKU, so gp1.small can be written literally — see what is on offer with fm compute flavor list, or read it in Terraform with the frostmoln_flavor / frostmoln_flavors data sources. Images are identified by UUID, so look one up by name with the frostmoln_image data source as above.
  • security_groups decides what can reach in. Omit it, or set it to [], and the instance falls back to the tenant's default group, which allows all outbound traffic but admits inbound only from instances in that same group — so nothing from the internet, SSH included. A group you create yourself starts the same way round: outbound allowed, no inbound rules at all, so the inbound you want has to be written out as frostmoln_security_group_rule.ssh does above.
  • ssh_key_names takes key names. frostmoln_ssh_key is identified by its name within a tenant, so frostmoln_ssh_key.laptop.name is the value to pass. Terraform's file() does not expand ~ by itself — wrap the path in pathexpand() as above, or give an absolute path.
  • zone is optional. Leave it out and the platform picks an availability zone and records it in state. Keep the subnet and the instance in the same one.

3. Apply ​

bash
terraform init
terraform plan
terraform apply

Terraform tracks state, so subsequent apply runs only change what differs.

4. Tear it down ​

terraform destroy removes the resources again, with three deliberate stops in the way:

  • The gateway refuses to be destroyed until the configuration says the loss of connectivity is intended. Add acknowledge_connectivity_loss = true to frostmoln_gateway.main, terraform apply that, and then destroy — and put the line back only when you next need it, since a true left behind disarms the guard for the life of the resource.
  • The gateway also refuses while public IPs in its VPC still depend on it. The depends_on above settles that for the one address in this configuration, but any other public IP in the same VPC — a load balancer's, or one this configuration does not manage — has to be disassociated first. See Public IPs.
  • Destroying frostmoln_public_ip releases the address for good. It returns to a shared pool and is re-issued to whoever asks next, so an address that is published in DNS or sitting in someone's allow-list is worth protecting with Terraform's own lifecycle { prevent_destroy = true }.