Blog

Assigning reserved public IPs to guests with Oracle Cloud and Terraform

Share this article

When I started one (of the many things) that struggled with was how to assign a reserved public ip to a guest when using Terraform.
And as I don’t seem to be to only one, I decided to write a quick blog post to explain how to do it.
But first, some background.

Terraform

Terraform is an infrastructure as code software tool created by HashiCorp
It allows you to create cloud resources using a high level declarative language, which is great as you can reuse code and put it in version control.
Different cloud vendors have written a provider for it, which allows you to use Terraform on their cloud platform.

Public IPs

Within Oracle Cloud Infrastructure (OCI), you have 2 different types of public IP addresses.
You have ephemeral addresses, which exist for the lifetime of your guest (they are destroyed when your guest is destroyed or recreated). And you have reserved addresses, which are persistent and exist beyond the lifetime of your guest. These can also be unassigned from a guest and assigned to another one.

How to create and attach them using Terraform is what I want to show you in this post using a step by step example.

Step by step example

I have an existing environment, in which I already have created a virtual cloud network, public subnet, routing rules and security lists.
I now want to reserve a public IP address (but not yet attach it to a guest).

The Terraform code used is valid for version 0.12, which uses a different syntax than Terraform 0.11.

To reserve a public IP address on OCI, you use the oci_core_public_ip resource:

resource "oci_core_public_ip" "pubip1" {
  compartment_id = var.compartment_ocid
  display_name   = "reserved public ip"
  lifetime       = "RESERVED"
  private_ip_id  = ""
  defined_tags   = var.common_tags
}

output "pubip1_address" {
  value = oci_core_public_ip.pubip1.ip_address
}

Note the private_ip_id attribute which is set to an empty string.

When I now run the terraform apply command it will reserve a public IP address for me and shows me the address:

oci_core_public_ip.pubip1: Creating...
oci_core_public_ip.pubip1: Creation complete after 1s [id=ocid1.publicip.oc1.eu-frankfurt-1.aaaaaaaa4xp5ftu2vfmd5ajagyplp5xupbe7ptqxofiwocyj33inr2d7mdzq]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

pubip1_address = 132.145.251.84

Next I’m going to create a guest within my public subnet, but I’m not requesting a public ip for it, by setting the assign_public_ip attribute to false.
Doing so, would create and assign an ephemeral address

resource "oci_core_instance" "pubiptest" {
  compartment_id      = var.compartment_ocid
  availability_domain = data.oci_identity_availability_domains.ADs.availability_domains["0"]["name"]
  fault_domain        = data.oci_identity_fault_domains.FDs.fault_domains["0"]["name"]
  display_name        = "pubiptest"
  shape               = "VM.Standard2.1"
  defined_tags        = var.common_tags

  create_vnic_details {
    display_name     = "primaryvnic"
    hostname_label   = "pubiptest"
    assign_public_ip = false
    subnet_id        = oci_core_subnet.pub.id
  }

  source_details {
    source_type = "image"
    source_id   = var.instance_image_ocid[var.region]
  }

  metadata = {
    ssh_authorized_keys = var.ssh_public_key
  }

  timeouts {
    create = "60m"
  }
}

After running terraform apply again, I now have a reserved unassigned public IP and a guest in a public subnet without a public IP address.
To bind the two together I need to update the private_ip_id attribute of the oci_core_public_ip resource with the private IP id of the guest.

To get that information I can use the data resource oci_core_private_ips.
When passing it the ip_address of my guest and the id of the subnet it is in, it will retrieve the id of that IP address.

data "oci_core_private_ips" "pubiptestIps" {
  ip_address = oci_core_instance.pubiptest.private_ip
  subnet_id  = oci_core_subnet.pub.id
}

With that piece of information I can now update the private_ip_id attribute:

resource "oci_core_public_ip" "pubip1" {
  compartment_id = var.compartment_ocid
  display_name   = "reserved public ip"
  lifetime       = "RESERVED"
  private_ip_id  = data.oci_core_private_ips.pubiptestIps.private_ips[0]["id"]
  defined_tags   = var.common_tags
}

Running terraform apply again, will now attach my public IP to my guest:

Terraform will perform the following actions:

  # oci_core_public_ip.pubip1 will be updated in-place
  ~ resource "oci_core_public_ip" "pubip1" {
        compartment_id = "ocid1.compartment.oc1..aaaaaaaaclogtbidd....6bpjndhcute37temrqq"
        defined_tags   = {
            "Exitas.lifecycle"   = "TEST"
            "Exitas.responsible" = "dhoogfr"
        }
        display_name   = "reserved public ip"
        freeform_tags  = {}
        id             = "ocid1.publicip.oc1.eu-frankfurt-1.aaaaaaaa4xp5ftu2vfmd5ajagyplp5xupbe7ptqxofiwocyj33inr2d7mdzq"
        ip_address     = "132.145.251.84"
        lifetime       = "RESERVED"
      + private_ip_id  = "ocid1.privateip.oc1.eu-frankfurt-1.abtheljsqxdtbns67lsyhwzclxjoyimhq2l6efj5tghtw47fl47kqjdesetq"
        scope          = "REGION"
        state          = "AVAILABLE"
        time_created   = "2019-08-09 09:49:13.724 +0000 UTC"
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

oci_core_public_ip.pubip1: Modifying... [id=ocid1.publicip.oc1.eu-frankfurt-1.aaaaaaaa4xp5ftu2vfmd5ajagyplp5xupbe7ptqxofiwocyj33inr2d7mdzq]
oci_core_public_ip.pubip1: Modifications complete after 1s [id=ocid1.publicip.oc1.eu-frankfurt-1.aaaaaaaa4xp5ftu2vfmd5ajagyplp5xupbe7ptqxofiwocyj33inr2d7mdzq]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Outputs:

pubip1_address = 132.145.251.84

Proof of the pudding:

dhoogfr@dhoogfr-lpt1 ~ $ ssh -i ~/.ssh/id_dba4  opc@132.145.251.84
Last login: Fri Aug  9 11:14:30 2019 from 78-22-90-127.access.telenet.be
[opc@pubiptest ~]$ hostname -f
pubiptest.pub.tlvcn.oraclevcn.com

Bringing it all together

In the above example I did the creation and assigning in different steps, but that is not necessary.
You can reserve the public IP, create the guest and assign the IP address to it in one apply.

dhoogfr@dhoogfr-lpt1 ~ $ terraform apply -var-file="/home/dhoogfr/tf_env_vars/exitaslab.tf"
data.oci_identity_availability_domains.ADs: Refreshing state...
oci_core_vcn.tlvcn: Refreshing state... [id=ocid1.vcn.oc1.eu-frankfurt-1.aaaa....gayhcpyobpfg4a7v4wlmq]
oci_core_internet_gateway.ig: Refreshing state... [id=ocid1.internetgateway.oc1.eu-frankfurt-1.aaaaaaaa7a.....3op4fhha5svqajq]
oci_core_nat_gateway.ng: Refreshing state... [id=ocid1.natgateway.oc1.eu-frankfurt-1.aaaaaaaanh2gj7j....xpzdwqwn3kyvwq]
oci_core_security_list.priv_sl1: Refreshing state... [id=ocid1.securitylist.oc1.eu-frankfurt-1.aa....5x3256clsiptiqeqwrtzna]
oci_core_security_list.pub_sl1: Refreshing state... [id=ocid1.securitylist.oc1.eu-frankfurt-1.aaa....6odir53a7ja5mcyn6fmskq]
data.oci_identity_fault_domains.FDs: Refreshing state...
oci_core_route_table.priv_rt1: Refreshing state... [id=ocid1.routetable.oc1.eu-frankfurt-1.aaaaaaaaoy....hgmiia4chxggwofddwem7pxvq]
oci_core_route_table.pub_rt1: Refreshing state... [id=ocid1.routetable.oc1.eu-frankfurt-1.a....dktqogwapunzvoibx4oyq]
oci_core_subnet.priv: Refreshing state... [id=ocid1.subnet.oc1.eu-frankfurt-1.aaaaaaaah5dtkao4h6....6vks6twpej2ve2qqaajq]
oci_core_subnet.pub: Refreshing state... [id=ocid1.subnet.oc1.eu-frankfurt-1.aaaaaaaayskwtimwgqf...pw6vtpbi462ivuquivonq]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # data.oci_core_private_ips.pubiptestIps will be read during apply
  # (config refers to values not yet known)
 <= data "oci_core_private_ips" "pubiptestIps"  {
      + id          = (known after apply)
      + ip_address  = (known after apply)
      + private_ips = (known after apply)
      + subnet_id   = "ocid1.subnet.oc1.eu-frankfurt-1.aaaaaaaayskwtimwgqfos.....vtpbi462ivuquivonq"
    }

  # oci_core_instance.pubiptest will be created
  + resource "oci_core_instance" "pubiptest" {
      + availability_domain                 = "Wkpu:EU-FRANKFURT-1-AD-1"
      + boot_volume_id                      = (known after apply)
      + compartment_id                      = "ocid1.compartment.oc1..aaaaaaaaclogtbiddqh7....acopa6bpjndhcute37temrqq"
      + defined_tags                        = {
          + "Exitas.lifecycle"   = "TEST"
          + "Exitas.responsible" = "dhoogfr"
        }
      + display_name                        = "pubiptest"
      + fault_domain                        = "FAULT-DOMAIN-1"
      + freeform_tags                       = (known after apply)
      + hostname_label                      = (known after apply)
      + id                                  = (known after apply)
      + image                               = (known after apply)
      + ipxe_script                         = (known after apply)
      + is_pv_encryption_in_transit_enabled = (known after apply)
      + launch_mode                         = (known after apply)
      + launch_options                      = (known after apply)
      + metadata                            = {
          + "ssh_authorized_keys" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8z1Lj....nP9r9ElP"
        }
      + private_ip                          = (known after apply)
      + public_ip                           = (known after apply)
      + region                              = (known after apply)
      + shape                               = "VM.Standard2.1"
      + state                               = (known after apply)
      + subnet_id                           = (known after apply)
      + time_created                        = (known after apply)
      + time_maintenance_reboot_due         = (known after apply)

      + agent_config {
          + is_monitoring_disabled = (known after apply)
        }

      + create_vnic_details {
          + assign_public_ip       = "false"
          + defined_tags           = (known after apply)
          + display_name           = "primaryvnic"
          + freeform_tags          = (known after apply)
          + hostname_label         = "pubiptest"
          + private_ip             = (known after apply)
          + skip_source_dest_check = (known after apply)
          + subnet_id              = "ocid1.subnet.oc1.eu-frankfurt-1.aaaaaaaayskwtimwg...uivonq"
        }

      + source_details {
          + boot_volume_size_in_gbs = (known after apply)
          + kms_key_id              = (known after apply)
          + source_id               = "ocid1.image.oc1.eu-frankfurt-1.aaaaaaaa527xpybx2az....24pgva"
          + source_type             = "image"
        }

      + timeouts {
          + create = "60m"
        }
    }

  # oci_core_public_ip.pubip1 will be created
  + resource "oci_core_public_ip" "pubip1" {
      + assigned_entity_id   = (known after apply)
      + assigned_entity_type = (known after apply)
      + availability_domain  = (known after apply)
      + compartment_id       = "ocid1.compartment.oc1..aaaaaaaaclogtbid.....37temrqq"
      + defined_tags         = {
          + "Exitas.lifecycle"   = "TEST"
          + "Exitas.responsible" = "dhoogfr"
        }
      + display_name         = "reserved public ip"
      + freeform_tags        = (known after apply)
      + id                   = (known after apply)
      + ip_address           = (known after apply)
      + lifetime             = "RESERVED"
      + private_ip_id        = (known after apply)
      + scope                = (known after apply)
      + state                = (known after apply)
      + time_created         = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

oci_core_instance.pubiptest: Creating...
oci_core_instance.pubiptest: Still creating... [10s elapsed]
oci_core_instance.pubiptest: Still creating... [20s elapsed]
oci_core_instance.pubiptest: Still creating... [30s elapsed]
oci_core_instance.pubiptest: Still creating... [40s elapsed]
oci_core_instance.pubiptest: Still creating... [50s elapsed]
oci_core_instance.pubiptest: Still creating... [1m0s elapsed]
oci_core_instance.pubiptest: Creation complete after 1m6s [id=ocid1.instance.oc1.eu-frankfurt-1.abtheljshvi......a7ksuywr6zmjmfq]
data.oci_core_private_ips.pubiptestIps: Refreshing state...
oci_core_public_ip.pubip1: Creating...
oci_core_public_ip.pubip1: Creation complete after 1s [id=ocid1.publicip.oc1.eu-frankfurt-1.aaaaaaaaz4dq6mb2dcklx....bjm7sgrbyxa]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

pubip1_address = 130.61.198.76

dhoogfr@dhoogfr-lpt1 ~ $ ssh -i ~/.ssh/id_dba4  opc@130.61.198.76
The authenticity of host '130.61.198.76 (130.61.198.76)' can't be established.
ECDSA key fingerprint is SHA256:nELsNQ3StSy67KL/eQ/Ibpb7HvlxHNy4lK3iDdT+z7g.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '130.61.198.76' (ECDSA) to the list of known hosts.
[opc@pubiptest ~]$ hostname -f
pubiptest.pub.tlvcn.oraclevcn.com

Warning!

When you now destroy the guest, Terraform will also destroy the attached public IP

dhoogfr@dhoogfr-lpt1 ~ $ terraform plan -var-file="/home/dhoogfr/tf_env_vars/exitaslab.tf" -destroy -target oci_core_instance.pubiptest
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

data.oci_identity_availability_domains.ADs: Refreshing state...
oci_core_vcn.tlvcn: Refreshing state... [id=ocid1.vcn.oc1.eu-frankfurt-1.aaaa....gayhcpyobpfg4a7v4wlmq]
oci_core_internet_gateway.ig: Refreshing state... [id=ocid1.internetgateway.oc1.eu-frankfurt-1.aaaaaaaa7a.....3op4fhha5svqajq]
oci_core_security_list.pub_sl1: Refreshing state... [id=ocid1.securitylist.oc1.eu-frankfurt-1.aaa....6odir53a7ja5mcyn6fmskq]
data.oci_identity_fault_domains.FDs: Refreshing state...
oci_core_route_table.pub_rt1: Refreshing state... [id=ocid1.routetable.oc1.eu-frankfurt-1.aaaaaaaacfsu4eluwxqyltw2zmvnet6jfnv7jktdktqogwapunzvoibx4oyq]
oci_core_route_table.pub_rt1: Refreshing state... [id=ocid1.routetable.oc1.eu-frankfurt-1.a....dktqogwapunzvoibx4oyq]
oci_core_instance.pubiptest: Refreshing state... [id=ocid1.instance.oc1.eu-frankfurt-1.abtheljshvivmt....ag4gva7ksuywr6zmjmfq]

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # oci_core_instance.pubiptest will be destroyed
  - resource "oci_core_instance" "pubiptest" {
      - availability_domain = "Wkpu:EU-FRANKFURT-1-AD-1" -> null
      - boot_volume_id      = "ocid1.bootvolume.oc1.eu-frankfurt-1.abtheljshetbh5wu4jjwyroxbn6ymg2kc76nu73vzddjzfupkvs66d5vhk3a" -> null
      - compartment_id      = "ocid1.compartment.oc1..aaaaaaaaclogtbiddq....ute37temrqq" -> null
      - defined_tags        = {
          - "Exitas.lifecycle"   = "TEST"
          - "Exitas.responsible" = "dhoogfr"
        } -> null
      - display_name        = "pubiptest" -> null
      - fault_domain        = "FAULT-DOMAIN-1" -> null
      - freeform_tags       = {} -> null
      - hostname_label      = "pubiptest" -> null
      - id                  = "ocid1.instance.oc1.eu-frankfurt-1.abtheljshvivmtcde2i7ysb4q2ggb46cnlqewbz7ag4gva7ksuywr6zmjmfq" -> null
      - image               = "ocid1.image.oc1.eu-frankfurt-1.aaaaaaaa527xpybx2azyhcz2oyk6f4lsvokyujajo73zuxnnhcnp7p24pgva" -> null
      - launch_mode         = "NATIVE" -> null
      - launch_options      = [
          - {
              - boot_volume_type                    = "PARAVIRTUALIZED"
              - firmware                            = "UEFI_64"
              - is_consistent_volume_naming_enabled = true
              - is_pv_encryption_in_transit_enabled = true
              - network_type                        = "VFIO"
              - remote_data_volume_type             = "PARAVIRTUALIZED"
            },
        ] -> null
      - metadata            = {
          - "ssh_authorized_keys" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8z1Lj1XBL0iXkfwmhXZmbNsZboSBcH...6hU/DAzZmA2X1hFkMaF...wIUnP9r9ElP"
        } -> null
      - private_ip          = "192.168.8.2" -> null
      - public_ip           = "130.61.198.76" -> null
      - region              = "eu-frankfurt-1" -> null
      - shape               = "VM.Standard2.1" -> null
      - state               = "RUNNING" -> null
      - subnet_id           = "ocid1.subnet.oc1.eu-frankfurt-1.aaaaaaaayskwtimwgqfosq...pw6vtpbi462ivuquivonq" -> null
      - time_created        = "2019-08-09 12:38:59.539 +0000 UTC" -> null

      - agent_config {
          - is_monitoring_disabled = false -> null
        }

      - create_vnic_details {
          - assign_public_ip       = "false" -> null
          - defined_tags           = {} -> null
          - display_name           = "primaryvnic" -> null
          - freeform_tags          = {} -> null
          - hostname_label         = "pubiptest" -> null
          - nsg_ids                = [] -> null
          - private_ip             = "192.168.8.2" -> null
          - skip_source_dest_check = false -> null
          - subnet_id              = "ocid1.subnet.oc1.eu-frankfurt-1.aaaaaaaayskwti....6vtpbi462ivuquivonq" -> null
        }

      - source_details {
          - boot_volume_size_in_gbs = "47" -> null
          - source_id               = "ocid1.image.oc1.eu-frankfurt-1.aaaaaaaa527xpybx....ajo73zuxnnhcnp7p24pgva" -> null
          - source_type             = "image" -> null
        }

      - timeouts {
          - create = "60m" -> null
        }
    }

  # oci_core_public_ip.pubip1 will be destroyed
  - resource "oci_core_public_ip" "pubip1" {
      - assigned_entity_id   = "ocid1.privateip.oc1.eu-frankfurt-1.abtheljslfynzj5nqfvl527hoel3enqz7hjs63oxmr6oynsumd4tjmckh4vq" -> null
      - assigned_entity_type = "PRIVATE_IP" -> null
      - compartment_id       = "ocid1.compartment.oc1..aaaaaaaaclogtbiddq...jndhcute37temrqq" -> null
      - defined_tags         = {
          - "Exitas.lifecycle"   = "TEST"
          - "Exitas.responsible" = "dhoogfr"
        } -> null
      - display_name         = "reserved public ip" -> null
      - freeform_tags        = {} -> null
      - id                   = "ocid1.publicip.oc1.eu-frankfurt-1.aaaaaaaaz4dq6mb2dcklxkqwfwn2x4hvoligqhs36fxhiurzxbjm7sgrbyxa" -> null
      - ip_address           = "130.61.198.76" -> null
      - lifetime             = "RESERVED" -> null
      - private_ip_id        = "ocid1.privateip.oc1.eu-frankfurt-1.abtheljslfynzj5nqfvl527hoel3enqz7hjs63oxmr6oynsumd4tjmckh4vq" -> null
      - scope                = "REGION" -> null
      - state                = "ASSIGNED" -> null
      - time_created         = "2019-08-09 12:40:04.219 +0000 UTC" -> null
    }

Plan: 0 to add, 0 to change, 2 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

To avoid this, you need to first detach the public IP from the guest.
This is simply done by updating the private_ip_id attribute of the oci_core_public_ip resource to an empty string:

resource "oci_core_public_ip" "pubip1" {
  compartment_id = var.compartment_ocid
  display_name   = "reserved public ip"
  lifetime       = "RESERVED"
  #private_ip_id  = data.oci_core_private_ips.pubiptestIps.private_ips[0]["id"]
  private_ip_id  = ""
  defined_tags   = var.common_tags
}

Running destroy now will no longer destroy the reserved public ip

Plan: 0 to add, 0 to change, 1 to destroy.

------------------------------------------------------------------------

A quick tip

You can use the prevent_destroy attribute from the lifecycle clause, to prevent Terraform you to accidentally destroy a resource:

resource "oci_core_public_ip" "pubip1" {
  compartment_id = var.compartment_ocid
  display_name   = "reserved public ip"
  lifetime       = "RESERVED"
  private_ip_id  = data.oci_core_private_ips.pubiptestIps.private_ips[0]["id"]
  #private_ip_id = ""
  defined_tags  = var.common_tags
  lifecycle {
    prevent_destroy = true
  }
}
------------------------------------------------------------------------

Error: Instance cannot be destroyed

  on demo_env.tf line 223:
 223: resource "oci_core_public_ip" "pubip1" {

Resource oci_core_public_ip.pubip1 has lifecycle.prevent_destroy set, but the
plan calls for this resource to be destroyed. To avoid this error and continue
with the plan, either disable lifecycle.prevent_destroy or reduce the scope of
the plan using the -target flag.

Conclusion

To assign a reserved public IP address to a guest, you set the private_ip_id attribute of the oci_core_public_ip resource to the id of the private IP address of the guest and you set the assign_public_ip attribute of the oci_core_instance resource to false.
To detach the public IP address, set the private_ip_id attribute of the oci_core_public_ip resource to an empty string.

Destroying a guest with an attached public IP address will destroy the reserved public IP address too.
To prevent this, you must detach the public IP before destroying the guest.
Using the prevent_destroy attribute in the lifecycle clause can help you to avoid accidental destroys.

Tags: Blog
Oracle Database Firewall
DBAs in the mist

You May Also Like