I have an old post how to create three instances in OCI with modules using Terraform 0.11 but since 0.12 came out I’ve been wanting to rewrite it to show what we can achieve with new features introduced in TF 0.12.

In this post I will once again create those instances but now the modules used are dynamic so they will create as many resources I need based on variables I send.

Terraform 0.12 improvements

You can download the full source code with modules from https://github.com/svilmune/tf-012-create-three-instances-demo. As you can see the root folder contains the files main.tf, variables.tf and outputs.tf. In the main.tf I reference always the module by using module directory which has their own .tf files inside. If I would want I could use also the git link to reference the module but in this example I’ll use local folders.

module "CreateCompartment" {
  source                  = "./module-compartment"
  tenancy_ocid            = var.tenancy_ocid
  compartment_name        = var.compartment_name
  compartment_description = var.compartment_description
}

First visible change with Terraform 0.12 is that we no longer need to set brackets around variables, makes writing code much easier! Also if I take a peak in the Compartment module I can see change in the outputs.tf.

output "compartment" {
  value = oci_identity_compartment.CreateCompartment
}

I can now return all data values from created resource by above code, no longer do I need to return values one by one and can use it easily by defining value of output in the code and picking the correct variable. OCI provider documentation here has these defined.

 compartment_ocid                   = module.CreateCompartment.compartment.id

Route Table and Security List modules

Few notable changes also with route table and security list modules. Sometimes route rules require us to define multiple values, earlier you couldn’t really easily do this within a module but now there are few possibilities. The way I’ve solved it in this example is by using a list and then combining it to a map in the module. From the route table:

module "CreateRouteRule" {

  source                             = "./module-routetable"
  tenancy_ocid                       = var.tenancy_ocid
  compartment_ocid                   = module.CreateCompartment.compartment.id
  vcn_id                             = module.CreateVCN.vcn.id
  network_id                         = [module.CreateIGW.internetgateway.id]
  route_table_display_name           = var.route_table_display_name
  route_table_route_rules_cidr_block = [var.igw_route_table_rules_cidr_block]

}

I’m sending now only one variable to route table module inside the list but could modify that to contain multiple. Let’s take a peak inside the route table module itself!

resource "oci_core_route_table" "CreateRouteTable" {

  compartment_id = var.compartment_ocid
  vcn_id         = var.vcn_id
  display_name   = var.route_table_display_name

  dynamic "route_rules" {

    for_each = zipmap(var.network_id, var.route_table_route_rules_cidr_block)

    content {
      network_entity_id = route_rules.key
      destination       = route_rules.value

    }
  }
}

Very simple with two variables! I’m defining in the route_rules part that the resource creation is dynamic and I create a key/value pair to a new map from the lists I’m sending to the module using zipmap. After that I’m assigning each key/value pair to resource values. If there would be bigger map the route rules would be updated accordingly.

In the security list module I’m doing similar thing with a slightly different approach. In variables.tf I’ve defined map with ingress_ports:

variable "ingress_ports" {
  description = "all the ports for security list"
  default = [
    { minport     = 22
      maxport     = 22
      source_cidr = "0.0.0.0/0"
    },
    { minport     = 0
      maxport     = 0
      source_cidr = "172.27.0.0/16"
  }]
}

If I would need more ports I would now just add new item in the map without need to edit the module itself at all, this has been one of the biggest things which helped us reducing amount of work with Terraform. As you might have noticed Security Lists get updated every now and then..

In the module I’m using the variable like this.

dynamic "ingress_security_rules" {
    iterator = port
    for_each = [for y in var.ingress_ports : {
      minport     = y.minport
      maxport     = y.maxport
      source_cidr = y.source_cidr
    }]
    content {
      protocol  = var.ingress_protocol
      source    = port.value.source_cidr
      stateless = var.ingress_stateless
      tcp_options {
        // These values correspond to the destination port range.
        min = port.value.minport
        max = port.value.maxport
      }
    }
  }

Lot of similarities to route table but as we now have three items in the map we can’t use simply key+value but instead have multiple key/value pairs. I’ve simply assigned iterator and go through the map with for_each and in the content section notice how the value is defined with port.value.value_name always. Now regardless of amount of items we get all done in one loop. For example one of our lists has already 30 different ports which are easily managed through similar module.

Creating those instances

We still need to look on the creation of instances. With Terraform 0.11 we used count variable to create multiple resources but now we can again utilize for_each, only this time we use it for the “whole” resource.

I’ve created variable instance_variables which has following:

variable "instance_variables" {
  description = "Map instance name to hostname"
  default = {
    "ForEach1" = "fe-1"
    "ForEach2" = "fe-2"
    "ForEach3" = "fe-3"
  }
}

I’m using this key/value pair for display name and hostname label when creating the instance. Perhaps you would need to define private ip’s so this is one value also which could be added etc. In the instance module I can just simply define to loop this variable through using for_each and assign values where needed.

resource "oci_core_instance" "CreateInstance" {
  for_each            = var.instance_variables
  availability_domain = var.instance_availability_domain
  compartment_id      = var.compartment_id
  shape               = var.shape_id
  source_details {
    source_id   = var.image_id
    source_type = "image"
  }

  create_vnic_details {
    subnet_id              = var.subnet_id
    display_name           = each.key
    hostname_label         = each.value
    skip_source_dest_check = var.instance_create_vnic_details_skip_source_dest_check
    assign_public_ip       = var.assign_public_ip
  }

Running Terraform

Now that all the new features have been gone through I’m ready to run Terraform and create these resources. I’ll start by running terraform init which will initialize modules and provider.

terraform init
Initializing modules...

Initializing the backend...

Initializing provider plugins...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.oci: version = "~> 3.39"

Terraform has been successfully initialized!

Once this has been done I will run terraform plan to see what resources will get created. I can see each of my three instances being in the plan with specific names which I assigned to them. Since I’m happy with the plan I will run terraform apply.

While running apply I can observe all three instances are being created in parallel by using the module:

module.CreateInstances.oci_core_instance.CreateInstance["ForEach2"]: Creating...
module.CreateInstances.oci_core_instance.CreateInstance["ForEach3"]: Creating...
module.CreateInstances.oci_core_instance.CreateInstance["ForEach1"]: Creating...

After bit over minute I have the instances running by using new Terraform 0.12 features and I produce the output of all instance ip’s in the end. See also how you can use for loop in output with Terraform 0.12.

output "instance_private_and_public_ips" {
  value = {
    for instance in module.CreateInstances.instances:
    instance.private_ip => instance.public_ip
  }
}
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

Outputs:

instance_private_and_public_ips = {
  "172.27.0.2" = "130.61.82.84"
  "172.27.0.3" = "130.61.114.168"
  "172.27.0.4" = "130.61.38.163"
}

Summary

In my opinion getting Terraform 0.12 available has been a huge factor on our goal to write code which can be reused and reduce time on writing Terraform. There are many new features and I’ve only displayed a few but perhaps this give you an idea what can be done with it. At this point in time OCI Resource Manager doesn’t yet support Terraform 0.12, once it does I will update git repository to contain also version which can be run in Resource Manager.

Modules and code is freely used, just let me know if you find any bugs or have any major improvements, it’s always great to learn new things!

7 thoughts on “Create three instances with Terraform 0.12 and dynamic modules in Oracle Cloud Infrastructure”

  1. Hi, I am getting the following error during planning phase:

    $ terraform plan
    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.GetAds: Refreshing state…
    data.oci_core_images.oraclelinux: Refreshing state…

    Error: Invalid index

    on main.tf line 104, in module “CreateInstances”:
    104: image_id = lookup(data.oci_core_images.oraclelinux.images[0], “id”)
    |—————-
    | data.oci_core_images.oraclelinux.images is empty list of object

    The given key does not identify an element in this collection value.

    1. Actually please ignore this, I immediately realized that the image was actually removed from the console ( linux 7.6 ), so updating the variables fixed that…thanks!

      1. Good finding! I should probably add note about that since I’ve ran into that same myself few times when the image isn’t available anymore.

  2. Really useful article, thank you.

    How do/would you go about incorporating:

    “output “compartment” {
    value = oci_identity_compartment.CreateCompartment
    }”

    into a module that creates multiple computer instances using a ‘for_each’ and then reference one of them in a volume attachment (as an example), please?

    1. Thanks Robert! If you are just referencing that one compartment which was created then good example how to pass is from the project’s main.tf:

      module “CreateVCN” {
      source = “./module-vcn”
      compartment_ocid = module.CreateCompartment.compartment.id
      vcn_cidr_block = var.vcn_cidr_block
      display_name = var.vcn_display_name
      dns_label = var.vcn_dns_label
      }

      As what comes to block volumes and attachments it’s tricky one to make decision how to do it in my opinion. I would probably do it as a combined module so block volume and attachment are in the same module and I pass specific instance id + amount of block volumes to be created to the module. If I try to make same module to create block volumes for all compute instances at the same time it might become quite complicated since you could have:

      Compute1: 50GB LUN, 100 GB LUN
      Compute2: 100GB LUN
      Compute3: 50GB LUN, 50GB LUN, 200GB LUN

      If you want to pass specific value from the output list it would be:

      module.CreateCompartment.compartment[0].id

      But this is a good topic for another blog post so I will combine this into wider post for the block volumes!

Leave a Reply to Christoph Cancel reply

Your email address will not be published.