I’ve been a big fan of Terraform for quite some time but one of the things I have been complaining about has been some things which have not been possible when you use Terraform modules.

Why do we use modules? It gives us a way to reuse our code so that each resource creation is always done using the same modules which create necessary resources. This usually has worked without an issue but OCI security list has been a pain to work with.

Problem with the security list module has been that the amount of ports you need to open per security list changes. If our database security list has required two ports to be opened and our applications security list required five ports you couldn’t dynamically define the ports.

This has lead our security list module to be a rude hack where Git has had different versions based on the amount of ports you required.

For example our Applications Security List used version 1.4:

module "CreatePrivateSecurityListApplications" {
  source               = "git::https://gitlab.local/OCI/module-securitylist.git?ref=v1.4"

Where as Exadata Security List used version 1.2:

module "CreatePrivateSecurityListExadata" {
  source               = "git::https://gitlab.local/OCI/module-securitylist.git?ref=v1.2"

While this worked it has been hard to maintain and each new port addition required lot of code updates. If you are interested on using versioning with modules you can check it from Terraform documentation.

Starting point

Old code had lot of static variables and when a port for a security list was added it was always a new entry into a list in the resource creation. OCI provider documentation shows how it’s done, we are now looking specially on the tcp_options part.

Part of incoming variables for the module:

#Added for v1.1 to support 1521 and 1522 sql*net ports on static opening
variable "ingress_sql1_tcp_min" {}
variable "ingress_sql1_tcp_max" {}
variable "ingress_sql2_tcp_min" {}
variable "ingress_sql2_tcp_max" {}

#Added for v1.2 to support 8001 as HTTP port for E-Business Suite
variable "ingress_ebs_http_tcp_min" {}
variable "ingress_ebs_http_tcp_max" {}

And how they were handled in the code:

    protocol = "${var.ingress_protocol}" // tcp = 6
    source = "${var.ingress_source}"
    stateless = "${var.ingress_stateless}"
 tcp_options {

      // These values correspond to the destination port range.
      "min" = "${var.ingress_sql1_tcp_min}"
      "max" = "${var.ingress_sql1_tcp_max}"
    protocol = "${var.ingress_protocol}" // tcp = 6
    source = "${var.ingress_source}"
    stateless = "${var.ingress_stateless}"

    tcp_options {

      // These values correspond to the destination port range.
      "min" = "${var.ingress_sql2_tcp_min}"
      "max" = "${var.ingress_sql2_tcp_max}"

In the end we had 20 different ports defined each having their own tcp_options so you can imagine how fun keeping it updated was..

Switching to Terraform 0.12

Now to the interesting part! I’ve been searching a way to do this with Terraform 0.12 and I found a way to do it with dynamic blocks. To use them we need a variable which is a list or a map, in my case I need a map with defined values for port ranges and cidr range. Instead of defining each min and max port as a variable I can do this with 0.12:

variable "ingress_ports_applications" {
  description = "all the ports for security list"
  default = [
              {minport = 22
               maxport = 22
               source_cidr = null
               {minport = 30500
                maxport = 30600
                source_cidr = null

And in my project I just call the module with necessary variables including this new one on line 12:

module "CreatePrivateSecurityListApplications" {
  source               = "git::https://gitlab.local/OCI/module-securitylist.git?ref=v1.5"
  compartment_ocid     = "${lookup(data.oci_identity_compartments.GetCompartments.compartments[0],"id")}"
  vcn_id               = "${lookup(data.oci_core_vcns.GetVCN.virtual_networks[0],"id")}"
  tenancy_ocid         = "${var.tenancy_ocid}"
  sl_display_name      = "${var.private_security_list_name_applications}"
  egress_destination   = "${var.egress_destination_applications}"
  egress_protocol      = "${var.egress_protocol_applications}"
  ingress_protocol     = "${var.ingress_protocol_applications}"
  ingress_source       = "${var.ingress_source_applications}"
  ingress_stateless    = "${var.ingress_stateless_applications}"
  ingress_ports      = "${var.ingress_ports_applications}"

Now when looking our newly rewritten module the number of lines in the code has dropped from 150 to 40.

variable "vcn_id" {}
variable "tenancy_ocid" {}
variable "compartment_ocid" {}
variable "sl_display_name" {}
variable "egress_destination" {}
variable "egress_protocol" {}
variable "ingress_protocol" {}
variable "ingress_source" {}
variable "ingress_stateless" {}
variable "ingress_ports" {}

resource "oci_core_security_list" "CreateSecurityList" {
  compartment_id = var.compartment_ocid
  vcn_id = var.vcn_id
  display_name = var.sl_display_name

  egress_security_rules {
    destination = var.egress_destination
    protocol = var.egress_protocol
  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 != "" ? 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

I’m defining dynamic block starting from line 21 and assigning the values to variables on lines 24, 25 and 26. Finally on lines 34 and 35 I’m using these variables as min and max port range. The variables inside “content” is looped through based on what I defined in my map.

I have also a conditional for source on line 30. I open specific ports to CIDR block (so for all) as we can access this only through corporate network, but as Exadata networks require all ports to be opened we define source_cidr for those networks and open all ports. So unless source cidr is defined it will default to

We also had to create new module to handle security list with UDP. It felt easier to separate these to a new module rather than put everything the same, the module is 1:1 with this one but just had udp_options instead of tcp_options in the resource creation.


This should give you an overview what you can achieve with Terraform 0.12, there are other good new features as well. Take a look them if you are starting to build something new!

If nothing else there are now some possibilities to reduce amount of code you’ve stacked over time.

Leave a Reply

Your email address will not be published. Required fields are marked *