Skip to content

frostmoln_public_ip_association (Resource)

Attaches an existing public IP to an instance, as that instance's inbound address.

This is the resource for an address you already have — one reserved earlier, published in DNS, or already sitting in a partner's allow-list. Nothing about the ADDRESS is managed here, only the attachment: look the address up with the frostmoln_public_ip data source and pass its id. Destroying this detaches the address and leaves it allocated to your tenant, ready for the next instance — which is the point, and the difference from destroying a frostmoln_public_ip, where the address itself is released and does not come back.

~> frostmoln_public_ip.instance_id and the frostmoln_public_ip_association resource are mutually exclusive — never use both for the same address. Both express the SAME attachment, so both would manage it: whichever applies second undoes what the first did, every subsequent plan proposes the change again, and the configuration never converges. (It is the same conflict the AWS provider documents between aws_eip.instance and aws_eip_association.)

Pick one per address. Use frostmoln_public_ip.instance_id when the SAME configuration allocates the address, so the address and its attachment are created and destroyed together.

Use frostmoln_public_ip_association when the address ALREADY EXISTS (look it up with the frostmoln_public_ip data source), or when it has to outlive the instance it is attached to. Destroying that resource detaches the address and leaves it allocated to your tenant, whereas destroying a frostmoln_public_ip RELEASES the address for good.

~> Terraform cannot see that an ATTACHED address depends on the VPC's gateway. This resource attaches one, and an address reaches the outside world only through a gateway — but nothing that attaches an address refers to frostmoln_gateway, so nothing orders the two. Terraform runs them concurrently and either can win.

On teardown the gateway can go first, and its delete is then refused ("Gateway is still in use", GATEWAY_IN_USE) because something in the VPC still depends on it — the failure that stops a terraform destroy half way through. On create the attachment can land first, and the platform then attaches a gateway ITSELF to carry it: a frostmoln_gateway that names a public_ip_id is refused after that ("VPC already has a gateway", GATEWAY_EXISTS), and one that names none is not refused at all — it quietly ADOPTS the gateway the platform made, leaving the VPC egressing from whatever address that gateway already had rather than one this configuration names, with origin reading implicit_public_ip.

Where the same configuration manages the gateway, state the ordering yourself: put depends_on = [frostmoln_gateway.<name>] on the resource that makes the ATTACHMENT — this one. Where the gateway is in another module, the dependency is on the module itself: depends_on = [module.<name>]. This resource is not the only one that needs it: frostmoln_apache_instance (public), frostmoln_kubernetes_cluster (public_ip_id), frostmoln_load_balancer (public_ip_id), frostmoln_nginx_instance (public) and frostmoln_public_ip (instance_id) attach addresses too, and each takes the line on itself.

It works only where the gateway is a frostmoln_gateway RESOURCE in the same configuration. A data "frostmoln_gateway" cannot carry the order — a data source is read, never created or destroyed — so depending on one defers a read and sequences nothing.

The ATTACHMENT is the place for it because an address that is merely allocated depends on nothing — and because an address resolved through the frostmoln_public_ip data source has no allocation resource to hang it on at all.

Never put it on an address that a frostmoln_gateway.public_ip_id names — the gateway already depends on that address, so a dependency back the other way is a plan-time Cycle: error, and that reference already sequences the two correctly without help.

Do not write it the other way about — on the gateway, listing what attaches. depends_on orders the resource it is written on, so that reverses both orders and turns a race that sometimes passed into a teardown that fails every time.

It changes ORDER only: nothing is created and nothing is released. It does not arm the gateway's own destroy either — without acknowledge_connectivity_loss the teardown stops at that refusal instead, and never reaches the ordering at all. And if a gateway was already adopted, nothing needs importing or rebuilding: it is in state already — add the ordering so it cannot recur, then give the gateway the address you meant with public_ip_id, which is applied in place.

Every configurable attribute forces replacement. The platform has no in-place re-point — moving an address is a disassociate followed by an associate — so Terraform sequences it as a destroy and a create, and the instance is without the address in between.

~> create_before_destroy does not work on this resource. With it, Terraform creates the replacement BEFORE destroying the old one, and the old one still holds the address: the platform refuses the second attachment with 409 Public IP is already associated and the apply fails, leaving the original attachment in place. There is one address and it can be in one place at a time, so a replacement is necessarily destroy-then-create. Plan for the gap instead — the address is off the instance for the duration of the two calls.

Example Usage

terraform
# Attach an address you ALREADY have to an instance.
#
# The address here was reserved earlier — it is published in DNS and sits in a
# partner's allow-list — so the configuration must NOT allocate a new one, and
# must not release this one when the instance goes away. The data source looks
# the address up without managing it; this resource manages only the attachment.
data "frostmoln_public_ip" "published" {
  address = "203.0.113.10"
}

resource "frostmoln_instance" "web" {
  name      = "web-01"
  flavor_id = data.frostmoln_flavor.medium.id
  image_id  = data.frostmoln_image.ubuntu.id
  vpc_id    = frostmoln_vpc.example.id
  subnet_id = frostmoln_subnet.example.id
}

resource "frostmoln_public_ip_association" "web" {
  public_ip_id = data.frostmoln_public_ip.published.id
  instance_id  = frostmoln_instance.web.id
}

# Replacing the instance moves the address to the new one: every configurable
# attribute forces replacement, so Terraform disassociates and re-associates. The
# ADDRESS is never released — `terraform destroy` on this configuration leaves
# 203.0.113.10 allocated to the tenant, so the DNS record and the partner's
# allow-list entry keep pointing at something that is still yours.
#
# The address IS off the instance between those two calls, and
# `create_before_destroy` cannot close that gap: it would create the new
# association while the old one still holds the address, and one address serves
# one thing at a time — the platform refuses the second attach with
# `409 Public IP is already associated` and the apply fails.

# A multi-NIC instance: `port_id` chooses WHICH interface answers on the address.
# Leave it unset (as above) and it resolves to the instance's first network port,
# which is what a single-homed instance wants. The port must belong to
# `instance_id`; anything else is refused before a request is sent.
data "frostmoln_public_ip" "backoffice" {
  address = "203.0.113.11"
}

resource "frostmoln_public_ip_association" "web_backoffice_nic" {
  public_ip_id = data.frostmoln_public_ip.backoffice.id
  instance_id  = frostmoln_instance.web.id
  port_id      = "b1e0f6c2-1234-4a5b-9c8d-abcdef012345" # the instance's second network port
}

output "web_public_ip" {
  value = data.frostmoln_public_ip.published.address
}

# --------------------------------------------------------------------------
# DO NOT DO THIS. `frostmoln_public_ip.instance_id` and
# frostmoln_public_ip_association are mutually exclusive: both manage the same
# attachment, so each apply undoes the other's and the plan never settles.
#
#   resource "frostmoln_public_ip" "wrong" {
#     instance_id = frostmoln_instance.web.id   # <- one manager
#   }
#
#   resource "frostmoln_public_ip_association" "also_wrong" {
#     public_ip_id = frostmoln_public_ip.wrong.id
#     instance_id  = frostmoln_instance.web.id  # <- a second manager of the same thing
#   }
#
# When the same configuration allocates the address AND the attachment is
# supposed to die with it, use `frostmoln_public_ip.instance_id` alone. Use this
# resource when the address is pre-existing, or has to outlive the instance.
# --------------------------------------------------------------------------

# Allocating here and attaching with this resource is fine too — as long as the
# allocating resource leaves `instance_id` unset, there is still only one
# manager of the attachment. This is what to reach for when the address must
# survive instance replacement within one configuration.
resource "frostmoln_public_ip" "api" {
  tags = {
    purpose = "api endpoint"
  }

  lifecycle {
    prevent_destroy = true
  }
}

resource "frostmoln_public_ip_association" "api" {
  public_ip_id = frostmoln_public_ip.api.id
  instance_id  = frostmoln_instance.web.id
}

# ORDERING AGAINST THE VPC'S GATEWAY. Declared in its own VPC on purpose: every
# association above would need the same line if it shared this gateway's VPC, and
# an example that quietly omitted it would teach the opposite of what it says.
#
# An attached address depends on the VPC having a gateway, and Terraform cannot
# see that — no attribute here refers to frostmoln_gateway, so the two are
# unordered and run concurrently. On teardown the gateway can go first and
# its delete is refused ("Gateway is still in use", GATEWAY_IN_USE), stopping the
# destroy half way. On create the attachment can land first, and the platform
# attaches a gateway ITSELF to carry it — after which an explicit gateway that
# pins a public_ip_id is refused ("VPC already has a gateway", GATEWAY_EXISTS),
# and one that pins none is NOT refused: it adopts the platform's gateway, and
# the VPC egresses from an address nobody chose.
#
# The depends_on belongs on the resource that makes the ATTACHMENT — this one.
# Not on frostmoln_public_ip.ordered: an address that is merely allocated depends
# on nothing, and where the address comes from the data source above there is no
# allocation resource to hang it on at all. Written the other way about (on the
# gateway, listing the addresses) it reverses both orders and makes the teardown
# fail every time rather than sometimes.
#
# Ordering is not the whole teardown: removing a gateway also needs
# `acknowledge_connectivity_loss = true` applied first — see frostmoln_gateway,
# and remove the line again once the destroy is done.
resource "frostmoln_vpc" "ordered" {
  name = "ordered-example"
  cidr = "10.20.0.0/16"
}

resource "frostmoln_subnet" "ordered" {
  vpc_id = frostmoln_vpc.ordered.id
  name   = "ordered-example"
  cidr   = "10.20.1.0/24"
}

resource "frostmoln_gateway" "ordered" {
  vpc_id = frostmoln_vpc.ordered.id
  mode   = "public_ip"
}

resource "frostmoln_instance" "ordered" {
  name      = "ordered-01"
  flavor_id = data.frostmoln_flavor.medium.id
  image_id  = data.frostmoln_image.ubuntu.id
  vpc_id    = frostmoln_vpc.ordered.id
  subnet_id = frostmoln_subnet.ordered.id
}

resource "frostmoln_public_ip" "ordered" {
  tags = {
    purpose = "ordering example"
  }
}

resource "frostmoln_public_ip_association" "ordered" {
  public_ip_id = frostmoln_public_ip.ordered.id
  instance_id  = frostmoln_instance.ordered.id

  depends_on = [frostmoln_gateway.ordered]
}

Schema

Required

  • instance_id (String) The ID of the instance to attach the public IP to. The address is bound to the instance's FIRST network port unless port_id names another one.
  • public_ip_id (String) The ID of the public IP to attach. The address must already be allocated — this resource never allocates one. Use the frostmoln_public_ip data source to resolve a reserved address to its id, or frostmoln_public_ip.<name>.id when the same configuration allocates it (in which case leave that resource's instance_id unset).

Optional

  • port_id (String) The instance network port to bind the address to. The platform associates an address with a PORT, not with an instance, so this is what the attachment really is — and what frostmoln_public_ip.attachment.resource_id reports for kind = "port".

Leave it unset for a single-homed instance and it resolves to the instance's first network port. Set it for a multi-NIC instance to choose which interface answers on the address — the same choice fm network public-ip associate --port-id offers. It must be one of instance_id's own ports; anything else is refused before a request is sent, because an address bound to a port outside the instance is an attachment this resource would drop from state on its next refresh.

Changing it forces replacement (disassociate, then associate).

Read-Only

  • id (String) The composite identifier of the association ({public_ip_id}/{instance_id}).

Import

Import is supported using the following syntax:

The terraform import command can be used, for example:

shell
# Associations are imported by the composite id "public_ip_id/instance_id".
# Importing records the attachment only; the address itself stays unmanaged
# unless a frostmoln_public_ip resource is imported separately.
terraform import frostmoln_public_ip_association.web <public-ip-id>/<instance-id>