How to Deploy Multi-Region Resources with Terraform: example(OCI Public IPs)

This image has an empty alt attribute; its file name is terraform_multiregion.png

Intro


As with any software, terraform also has hidden gems waiting to be discovered, even after you’ve obtained your associate certification. Some features aren’t always known until you need them, which is why we still a a lot to learn from the product. Today is one of those days!  In this post, I will show how to deploy Multi-Region Resources using something called provider aliases.



Why multi region deploy isn’t that common ?

The reason why the provider alias feature is not commonly used is that most users typically deploy resources in a single region at a time. Unless you have a setup that requires a DR configuration with regional failover or a distributed workload across several regions. The provider block, which is placed in the root module of a Terraform configuration, dictates the default location where all resources will be created.

Understanding Provider Aliases


To support multi region deployment, you can include multiple configurations for a given provider by including multiple provider blocks with the same provider name, but different alias meta-argument for each additional configuration. see Hashicorp’s example below

# Default provider configuration #region1 (un-aliased)

provider "aws" {
region = "
us-east-1" } }
# Extra configuration for #region2 (β€œus-west-2”), reference this as `aws.west`.

provider "aws" {
alias = "west" <<--------------- our identifier
region = "us-west-2"
}

 How to reference it from a resource block 
To use extra provider configuration for a resource or data source, set its provider argument to a <PROVIDER NAME>.<ALIAS> defined earlier:

resource "aws_instance" "my_instance" {
provider = aws.west <<---- reference allowing the instance creation in us-west-2
…
}


Practical Scenario: Deploying Public IPs in Multiple Regions in OCI

  

Let’s consider a scenario where a HA firewall setup (active-active) requires 4 public IP addresses in two different regions. We’ll leverage provider aliases to achieve this multi-region deployment.

  • Toronto => primary site (default) while Montreal (aliased)  => failover region

  • 4 IPs per region will be deployed

    • Public IP for Firewall Primary VM management Interface

    • Public IP for Firewall Secondary VM management Interface

    • Floating Public IP for Firewall Untrust Interface

    • Floating Public IP for Firewall Untrust Interface inbound flow (frontend cluster ip)

Clone the repository

  • This is my own github repo, Pick an area on your file system and run the clone command

$ git clone https://github.com/brokedba/terraform-examples.git

You will find our configuration under a subdirectory called terraform-provider-oci/publicIPs


  • Cd Into the subdirectory where our configuration resides and run the init

$ cd   ~/terraform-examples/terraform-provider-oci/publicIPs
$ terraform init

  • Here’s a tree of the files composing our configuration

$ tree . |-- variables.tf ---> Resource variables needed for the deploy including locals
|-- publicip.tf ---> Our main public IP resource declaration
|-- output.tf ---> displays the IP resources detail at the end of the deploy
|-- terraform.tfvars.template ---> environment_variables needed to authenticate to OCI

Now let’s check how and where the aliases are defined and referenced  

Provider block

Here, I explicitly set an alias for the default configuration β€˜primary  but it’s not necessary. Only dr alias is needed. 

# vi ./terraform-provider-oci/publicIPs/variables.tf

provider "oci" { # OPtional since it’s the default config
alias            = "primary" <<--- Default region Toronto
tenancy_ocid     = var.tenancy_ocid
user_ocid        = var.user_ocid
fingerprint      = var.fingerprint
private_key_path = var.private_key_path
region           = var.region <<---- "ca-toronto-1"

}

…
provider "oci" {

alias            = "dr" <<--- Alternative region Montreal

…
}

Resource based reference

By using local variables, I stored the display names of all my public IPs. This allows me to leverage a single dynamic block and a for_each loop to create all the public IPs per region efficiently.

This image has an empty alt attribute; its file name is image-5.png
As explained before, alias reference easy through a simple provider argument.

resource "oci_core_public_ip" "dr_firewall_public_ip" {    

provider = oci.dr <<---- reference allowing IP creation in Montreal region
    for_each = local.ips.dr_site
    compartment_id = var.tenancy_ocid
    lifetime = "RESERVED"
    #Optional
    display_name = each.key }

1. Execution plan (plan)

  • Under the working directory (terraform-provider-oci/publicIPs) update terraform.tfvars file

  • Run terraform plan (see below example of a public IP resource block  referencing Montreal region).

#Adjust terraform.tfvars.template with authentication parameters & rename it to terraform.tfvars

$ terraform plan .Terraform will perform the following actions:

# oci_core_public_ip.dr_firewall_public_ip["dr-mgmt-public_ip-vm-c"] will be created
  + resource "oci_core_public_ip" "dr_firewall_public_ip" {
      + assigned_entity_id   = (known after apply)
      + assigned_entity_type = (known after apply)
      + availability_domain  = (known after apply)
      + compartment_id       = "ocid1.tenancy.oc1..aaaaaaaavxxxxxxxxxxxxx"
      + defined_tags         = (known after apply)
      + display_name         = "dr-mgmt-public_ip-vm-c"
      + freeform_tags        = (known after apply)
      + id                   = (known after apply)
      + ip_address           = (known after apply)
      + lifetime             = "RESERVED"
      + public_ip_pool_id    = (known after apply)
      + scope                = (known after apply)
      + state                = (known after apply)
      + time_created         = (known after apply)
    }

..
# oci_core_public_ip.dr_firewall_public_ip["dr-mgmt-public_ip-vm-d"] will be created  + resource "oci_core_public_ip" "dr_firewall_public_ip" {
...
# oci_core_public_ip.dr_firewall_public_ip["dr-untrust-floating-public_ip"]..
+ resource "oci_core_public_ip" "dr_firewall_public_ip" {
...
# oci_core_public_ip.dr_firewall_public_ip["dr-untrust-floating-public_ip_frontend_1"] ..
+ resource "oci_core_public_ip" "dr_firewall_public_ip" {
... --- OTHER Primary region resources

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

Changes to Outputs:
  + Montreal_public_ips = {
      + dr-mgmt-public_ip-vm-c                   = (known after apply)
      + dr-mgmt-public_ip-vm-d                   = (known after apply)
      + dr-untrust-floating-public_ip            = (known after apply)
      + dr-untrust-floating-public_ip_frontend_1 = (known after apply)
    }
   + Toronto_public_ips  = {
      + mgmt-public_ip-vm-a                   = (known after apply)
      + mgmt-public_ip-vm-b                   = (known after apply)
      + untrust-floating-public_ip            = (known after apply)
      + untrust-floating-public_ip_frontend_1 = (known after apply)
    }

2. deployment (Apply)

  

And here you 8 resources created among which 4 public IPs in both regions (Toronto/Montreal) with one config.   

#original output was truncated for more visibility

$ terraform apply -–auto-approve

oci_core_public_ip.primary_firewall_public_ip["mgmt-public_ip-vm-a"]: Creating...
oci_core_public_ip.primary_firewall_public_ip[untrust-floating-public_ip_frontend_1]:Creating..
oci_core_public_ip.primary_firewall_public_ip["mgmt-public_ip-vm-b"]: Creating...
oci_core_public_ip.primary_firewall_public_ip["untrust-floating-public_ip"]: Creating...
oci_core_public_ip.dr_firewall_public_ip["dr-mgmt-public_ip-vm-c"]: Creating...
oci_core_public_ip.dr_firewall_public_ip["dr-mgmt-public_ip-vm-d"]: Creating...
oci_core_public_ip.dr_firewall_public_ip["dr-untrust-floating-public_ip"]: Creating...
oci_core_public_ip.dr_firewall_public_ip["dr-untrust-floating-public_ip_frontend_1"]:Creating..

Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
  + Montreal_public_ips = {
   + dr-mgmt-public_ip-vm-c   = "name: dr-mgmt-public_ip-vm-c IP:155… OCID:xx"
   + dr-mgmt-public_ip-vm-d   = "name: dr-mgmt-public_ip-vm-d IP:155…OCID:xx"
   + dr-untrust-floating-public_ip  = name: dr-untrust-floating-public_ip IP:155…
+ dr-untrust-floating-public_ip_frontend_1 = "name: dr-untrust-floating-public_ip_frontend_1 …
    }
   + Toronto_public_ips  = {
   + mgmt-public_ip-vm-a                   = "name: mgmt-public_ip-vm-a...
   + mgmt-public_ip-vm-b                   = "name: mgmt-public_ip-vm-b ...
   + untrust-floating-public_ip            = "name: untrust-floating-public_ip...
   + untrust-floating-public_ip_frontend_1 = "name: untrust-floating-public_ip_frontend_1..
    }


How about terraform modules?

To declare a configuration alias within a module in order to receive an alternate provider configuration from the parent module, you can add the aliases using the Configuration_aliases argument to the r provider’s required_providers entry.  

terraform {
required_version = ">= 1.0.3"    
required_providers {
  oci = {
   source  = "oracle/oci"
version = "4.105.0"
       configuration_aliases =  [ oci.primary, oci.dr ]
    }  } }


Conclusion:

  • Provider aliases in Terraform provide a powerful capability to deploy resources across multiple regions.

  • This allows you to simplify your Terraform configuration and avoid duplicating code for each region.

  • Provider aliases can also be used for targeting multiple Docker hosts, multiple Consul hosts, etc..

  • Sometimes a non popular feature doesn’t mean hard to implement as a quick look at the doc can get you going.  Hope this was helpful

Thanks for reading