diff --git a/oci-tf-cookbook/ad-by-number.md b/oci-tf-cookbook/ad-by-number.md new file mode 100644 index 0000000..b58b2a8 --- /dev/null +++ b/oci-tf-cookbook/ad-by-number.md @@ -0,0 +1,70 @@ +--- +title: Get the AD name by user-friendly number +series: oci-tf-cookbook +thumbnail: assets/cookbook.jpg +author: tim-clegg +tags: [open-source, terraform, iac, devops] +solution_names: [Administrative Domain - get AD name by the AD number,AD - get AD name by the AD number,IAM - get AD name by the number] +--- + +## Problem +Rather than use the AD name, it'd be nice to use the AD number. + +## Solution +Use a handy [data source](https://registry.terraform.io/providers/hashicorp/oci/latest/docs/data-sources/identity_availability_domains) to make this happen: +``` +data "oci_identity_availability_domains" "this" { + compartment_id = var.tenancy_ocid +} +``` + +Then within your resource definition(s), use the data source: +``` +resource "oci_core_instance" "this" { + availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name + ... +} +``` + +This could be abbreviated with a local, such as: +``` +locals { + ad_names = { for y in data.oci_identity_availability_domains.this.availability_domains : + index(data.oci_identity_availability_domains.this.availability_domains, y) => y.name + } +} +``` + +This can be used as follows: +``` +resource "oci_core_instance" "this" { + availability_domain = local.ad_names[0] + ... +} +``` + +Note that this uses zero-index array, with index 0 being AD 1, index 1 being AD 2, etc.: + +| Index # | AD # | Example | +|---------|------|---------| +| 0 | 1 | local.ad_names[0] | +| 1 | 2 | local.ad_names[1] | +| 2 | 3 | local.ad_names[2] | + +If you'd like the index to match the AD number, change the local as-follows: + +``` +locals { + ad_names = { for y in data.oci_identity_availability_domains.this.availability_domains : + index(data.oci_identity_availability_domains.this.availability_domains, y) + 1 => y.name + } +} +``` + +Then use it as follows: + +``` +local.ad_names[1] # AD 1 +local.ad_names[2] # AD 2 +local.ad_names[3] # AD 3 +``` \ No newline at end of file diff --git a/oci-tf-cookbook/ad-ocid-by-number.md b/oci-tf-cookbook/ad-ocid-by-number.md new file mode 100644 index 0000000..8c35959 --- /dev/null +++ b/oci-tf-cookbook/ad-ocid-by-number.md @@ -0,0 +1,49 @@ +--- +title: Get the AD OCID by AD number +series: oci-tf-cookbook +thumbnail: assets/cookbook.jpg +author: tim-clegg +tags: [open-source, terraform, iac, devops] +solution_names: [Administrative Domain - getting OCID by AD number, AD - getting OCID by AD number, IAM - getting the AD OCID programmatically] +--- + +## Problem +There are times when it's necessary to get the OCID of an Administrative Domain (AD). How can this be done? + +## Solution +This is similar to the solution for getting the name of an AD by friendly number. Instead of getting the name, we'll get the OCID. + +``` +# Get all availability domains for the region +data "oci_identity_availability_domains" "ads" { + compartment_id = var.tenancy_ocid +} +``` + +Now use it within your resource definition(s): + +``` +# Then use it to get a single AD name based on the index: + ... + availability_domain_id = data.oci_identity_availability_domains.ads.availability_domains[0].id + ... +} +``` + +This can also be refactored into a local: + +``` +locals { + ad_ids = { for y in data.oci_identity_availability_domains.this.availability_domains : + index(data.oci_identity_availability_domains.this.availability_domains, y) => y.id + } +} +``` + +Then used like so: +``` + ... + availability_domain_id = local.ad_ids[0] + ... +} +``` diff --git a/oci-tf-cookbook/compute-image-by-name-most-recent.md b/oci-tf-cookbook/compute-image-by-name-most-recent.md new file mode 100644 index 0000000..b654347 --- /dev/null +++ b/oci-tf-cookbook/compute-image-by-name-most-recent.md @@ -0,0 +1,62 @@ +--- +title: Get the most recent Compute Image OCID +series: oci-tf-cookbook +thumbnail: assets/cookbook.jpg +author: tim-clegg +tags: [open-source, terraform, iac, devops] +solution_names: [Compute - get the latest compute image OCID for a given distro, Compute image - get the latest distro OCID,Image - get latest compute image OCID] +--- + +## Problem +While it's possible to get the OCID of a compute instance image by name, it can often be ideal to always use the latest image (whatever it may be). How can this be done? + +## Solution +``` +data "oci_core_images" "latest_ol8" { + compartment_id = var.tenancy_ocid + operating_system = "Oracle Linux" + operating_system_version = 8.0 + shape = "VM.Standard2.1" + state = "AVAILABLE" + sort_by = "TIMECREATED" + sort_order = "DESC" +} + +resource "oci_core_instance" "my_compute" { + ... + + source_details { + source_id = data.oci_core_images.latest_ol8.images[0].id + source_type = "image" + } + + ... +} +``` + +The shape, OS, etc. can all be customized. The key here is to use the sort attributes to allow you to select the first element in the returned results. This gives you an idea of what you can do to solve this need! + +To make this a bit cleaner, a local could be defined: + +``` +locals { + latest_ol8_image_id = data.oci_core_images.latest_ol8.images[0].id +} +``` + +Then you can use it like: + +``` +resource "oci_core_instance" "my_compute" { + ... + + source_details { + source_id = local.latest_ol8_image_id + source_type = "image" + } + + ... +} +``` + +Just a bit cleaner! \ No newline at end of file diff --git a/oci-tf-cookbook/compute-image-by-name.md b/oci-tf-cookbook/compute-image-by-name.md new file mode 100644 index 0000000..1699102 --- /dev/null +++ b/oci-tf-cookbook/compute-image-by-name.md @@ -0,0 +1,47 @@ +--- +title: Get Compute Image OCID by Name +series: oci-tf-cookbook +thumbnail: assets/cookbook.jpg +author: tim-clegg +tags: [open-source, terraform, iac, devops] +solution_names: [Compute - get compute image OCID by name, Compute image - get OCID by name,Image - get compute image OCID by name] +--- + +## Problem +OCI Compute Instances require the OCID of a Compute Image to be provided. Hard-coding an OCID is rather risky, as Compute Images should be changed/updated rather regularly (and each new image will have a new OCID). How can we get the OCID for a Compute Instance based on its friendly name, as found in [https://docs.oracle.com/en-us/iaas/images/](https://docs.oracle.com/en-us/iaas/images/)? + +## Solution +``` +data "oci_core_images" "this" { + compartment_id = var.compartment_ocid + filter { + name = "state" + values = ["AVAILABLE"] + } +} + +locals { + list_images = { for s in data.oci_core_images.this.images : + s.display_name => + { id = s.id, + operating_system = s.operating_system + } + } +} + +resource "oci_core_instance" "my_compute" { + ... + + source_details { + source_id = local.list_images[var.compute_image_name].id + source_type = "image" + } + + ... +} +``` + +The `var.compute_image_name` can be set to something like `Oracle-Linux-8.4-2021.10.20-0` (found at the time of this writing at [https://docs.oracle.com/en-us/iaas/images/oracle-linux-8x/](https://docs.oracle.com/en-us/iaas/images/oracle-linux-8x/)). + +What have we done here? First, we retrieved the list of compute images (`data.oci_core_images.this`), then coerce this into a tuple, where the key is the name of the image. Lastly, we grabbed the OCID (`id`) from the tuple, based on the name given in the `var.compute_image_name` input variable). + diff --git a/oci-tf-cookbook/get-home-region.md b/oci-tf-cookbook/get-home-region.md new file mode 100644 index 0000000..8e3017c --- /dev/null +++ b/oci-tf-cookbook/get-home-region.md @@ -0,0 +1,56 @@ +--- +title: Find the OCI home region programmatically +series: oci-tf-cookbook +thumbnail: assets/cookbook.jpg +author: tim-clegg +tags: [open-source, terraform, iac, devops] +solution_names: [IAM - get the home region programmatically,Region - get home region] +--- + +## Problem +Many OCI services have to interact with the home region, which might not be the region used by the primary OCI provider definition. Is it possible to programmatically determine the home OCI region of a tenancy? + +## Solution +Yes it is! Typically the primary OCI provider will be a user-specified region, which might not be the home region. In these situations, it's necessary to programmatically determine the home region (which may or may not be the same). + +``` +data "oci_identity_tenancy" "tenant_details" { + tenancy_id = var.tenancy_ocid +} + +data "oci_identity_regions" "home-region" { + filter { + name = "key" + values = [data.oci_identity_tenancy.tenant_details.home_region_key] + } +} + +data "oci_identity_region_subscriptions" "home_region_subscriptions" { + tenancy_id = var.tenancy_ocid + + filter { + name = "is_home_region" + values = [true] + } +} +``` + +Next setup a provider definition for the home region (the following assumes that you already have an OCI provider defined without an alias): + +``` +provider "oci" { + alias = "home" + region = data.oci_identity_region_subscriptions.home_region_subscriptions.region_subscriptions[0].region_name + tenancy_ocid = var.tenancy_ocid + ... +} +``` + +From within the resource blocks that need to talk to the home region, specify this provider, like the following: + +``` +resource "oci_identity_tag_namespace" "this" { + provider = oci.home + ... +} +``` diff --git a/oci-tf-cookbook/index.md b/oci-tf-cookbook/index.md new file mode 100644 index 0000000..96e6e28 --- /dev/null +++ b/oci-tf-cookbook/index.md @@ -0,0 +1,83 @@ +--- +layout: collection +title: OCI Terraform Cookbook +series: oci-tf-cookbook +description: Awesome tips| tricks and techniques for using Terraform with OCI. +thumbnail: assets/cookbook.jpg +author: tim-clegg +tags: [open-source| terraform| iac| devops] +--- +{% img aligncenter assets/cookbook.jpg 400 400 "OCI Terraform Cookbook" "OCI Terraform Cookbook" %} +*(Photo by [Ron Lach](https://www.pexels.com/@ron-lach?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels) from [Pexels](https://www.pexels.com/photo/food-wood-dawn-coffee-8188946/?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels))* + +We hope that you find these solutions of benefit as you build OCI solutions using Terraform. These are organized in a problem/solution format, with all of the problems being in the index (below). Happy coding! + +Ps. These are designed to work with Oracle Cloud Infrastructure (OCI). If you don't have an OCI account get an OCI Always-Free account [here]({{site.urls.always_free }})! + +{%- assign alphabet = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z" | split: "," -%} +{%- assign letters_with_content = "" -%} + +{%- for letter in alphabet -%} + {%- for page in site.pages -%} + {%- if page.path contains 'collections/tutorials/oci-tf-cookbook'" -%} + {%- for name in page.solution_names -%} + {%- assign first_letter = name | slice: 0 | downcase -%} + {%- if first_letter == letter -%} + {%- unless letters_with_content contains letter -%} + {%- if letters_with_content != "" -%} + {%- assign letters_with_content = letters_with_content | append: "," | append: letter -%} + {%- else -%} + {%- assign letters_with_content = letter -%} + {%- endif -%} + {%- endunless -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- endfor -%} +{%- endfor -%} + +{%- assign letters_to_render = letters_with_content | split: "," %} + + + +# Solution Index +{%- for letter in letters_to_render %} +
+

{{ letter }}  top ^

+ + {%- assign solution_names = "" -%} + {%- for page in site.pages -%} + {%- if page.path contains 'collections/tutorials/oci-tf-cookbook'" -%} + {%- for name in page.solution_names -%} + {%- assign fltr = name | slice: 0 | downcase -%} + {%- if fltr == letter -%} + {%- if solution_names != "" -%} + {%- assign solution_names = solution_names | append: "," | append: name -%} + {%- else -%} + {%- assign solution_names = name -%} + {%- endif -%} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- endfor -%} + {%- assign solution_names = solution_names | split: "," | sort_natural -%} + + {%- for name in solution_names -%} + {%- for page in site.pages -%} + {%- if page.path contains 'collections/tutorials/oci-tf-cookbook'" -%} + {%- for page_name in page.solution_names -%} + {%- if name == page_name -%} + {{ name }}
+ {%- endif -%} + {%- endfor -%} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + +
+{%- endfor -%} diff --git a/tf-201/1-dependencies.md b/tf-201/1-dependencies.md new file mode 100644 index 0000000..e7bd471 --- /dev/null +++ b/tf-201/1-dependencies.md @@ -0,0 +1,586 @@ +--- +title: Terraform Dependencies +parent: tf-201 +tags: [open-source, terraform, iac, devops, intermediate] +categories: [iac, opensource] +thumbnail: assets/terraform-201.png +date: 2021-10-05 10:51 +description: Learn how Terraform automatically manages resource dependencies and when (and how) to use explicit (manual) resource dependencies. +toc: true +author: tim-clegg +--- +{% img aligncenter assets/terraform-201.png 400 400 "Terraform 201" "Terraform 201 Tutorial Series" %} + +If you went through the [Terraform 101 tutorial series](/tutorials/tf-101), then you got a chance to see how intelligently Terraform behaved when it came time to destroy all of the resources in the project (the [Destroying resources with Terraform](/tutorials/tf-101/7-destroying) tutorial). This is due to how Terraform automatically manages resource dependencies. There are times when explicit resource dependencies need to be configured. This tutorial takes a brief overview of how resource dependencies are managed in Terraform. + +Make sure that to start with the code at the end of the [Destroying resources with Terraform](/tutorials/tf-101/7-destroying) tutorial. + +## Implicit Dependencies +As you define resources, Terraform automatically tracks resource dependencies. A graph of the topology is maintained by Terraform, allowing Terraform to track dependencies and relationships between different resources. + +This was seen at work in the [Destroying resources with Terraform tutorial](/tutorials/tf-101/7-destroying), when the environment was entirely destroyed. During this process Terraform chose to destroy all of the Subnets first, then destroy the VCN last. Had Terraform tried to delete the VCN while the Subnets were still present, the process would've failed. + +The reverse is true as an environment is provisioned. See this at work by applying the environment that was destroyed in the [Terraform-101 series](/tutorials/tf-101/7-destroying): + +``` +$ terraform apply + +# ... + +Plan: 3 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_vcn.tf_101: Creating... +oci_core_vcn.tf_101: Creation complete after 3s [id=ocid1.vcn.oc1.phx.] +oci_core_subnet.dev: Creating... +oci_core_subnet.test: Creating... +oci_core_subnet.test: Still creating... [10s elapsed] +oci_core_subnet.dev: Still creating... [10s elapsed] +oci_core_subnet.test: Creation complete after 16s [id=ocid1.subnet.oc1.phx.] +oci_core_subnet.dev: Still creating... [20s elapsed] +oci_core_subnet.dev: Creation complete after 27s [id=ocid1.subnet.oc1.phx.] + +Apply complete! Resources: 3 added, 0 changed, 0 destroyed. +``` + +> **NOTE:** If you're prompted to enter a value for the `region` or `tenancy_ocid` variables, it's likely that the environment variables (above) need to be set. Each time you connect to your OCI Cloud Shell session, you'll need to set these, similar to the following: +> ```console +declare -x TF_VAR_tenancy_ocid=`echo $OCI_TENANCY` +declare -x TF_VAR_region=`echo $OCI_REGION` +``` +{:notice} + + +The VCN was built *first*, then the Subnets were added. This goes back to the fact that Terraform understands the relationship between these dependent resources. It knows that the VCN must exist *before* the Subnets can be created. + +For many use-cases, this implicit dependency relationship mapping works just fine. There are situations where Terraform might need some help... this is where explicit dependencies come into play. + +## Explicit Dependencies +The implicit dependency mapping works so well, you might be asking yourself why would you ever want to manually specify an explicit dependency? It turns out that Terraform can't read minds, and there are situations where it cannot infer or see any logical relationship or dependency between two resources. + +For example, pretend that you have an OCI Object Storage Bucket that contains objects (files) that are used by a particular application. This application is deployed and running on an OCI Compute instance. Follow along by adding the following to your `main.tf` file: + +```terraform +# OSS Bucket +data "oci_objectstorage_namespace" "this" { +} + +resource "oci_objectstorage_bucket" "app" { + compartment_id = "" + name = "App_Data" + namespace = data.oci_objectstorage_namespace.this.namespace + access_type = "NoPublicAccess" +} + +# Get all Availability Domains for the region +data "oci_identity_availability_domains" "ads" { + compartment_id = var.tenancy_ocid +} + +# Compute images +data "oci_core_images" "this" { + compartment_id = var.tenancy_ocid + state = "AVAILABLE" + operating_system = "Oracle Linux" + shape = "VM.Standard.E3.Flex" + sort_by = "DISPLAYNAME" + sort_order = "DESC" +} + +# Compute instance +resource "oci_core_instance" "app" { + availability_domain = lookup(data.oci_identity_availability_domains.ads.ailability_domains[0], "name") + compartment_id = var.tenancy_ocid + shape = "VM.Standard.E3.Flex" + display_name = "app" + + shape_config { + memory_in_gbs = 1 + ocpus = 1 + } + create_vnic_details { + assign_public_ip = false + subnet_id = oci_core_subnet.dev.id + } + source_details { + source_id = data.oci_core_images.this.images[0].id + source_type = "image" + } + preserve_boot_volume = false +} +``` + +> If you're wanting to use a specific Compartment, make sure to use that instead of `var.tenancy_ocid`. +{:.notice} + +> **NOTE:** If you're using an Always Free OCI tenancy, you'll need to use a shape of `VM.Standard.E2.1.Micro` (instead of `VM.Standard.E3.Flex`) and get rid of the `shape_config` block. +{:.notice} + +There's a few lines of code in there – some of which you don't need to get mired down with. Some handy code best-practices (like retrieving the latest Compute image OCID programmatically, getting the name of the AD programmatically, etc.) are part of it, but don't get distracted or overwhelmed by it. In short, Terraform is being asked to to create an OCI Object Storage Bucket for the application, then to create an OCI Compute instance. The OCI Compute instance would presumably run the application, although in this example there's nothing actually installed on it (just pretend that it's being used in this way). + +Look at the Terraform plan to see what Terraform proposes be done. + +``` +$ terraform plan +oci_core_vcn.tf_101: Refreshing state... [id=ocid1.vcn.oc1.phx.] +oci_core_subnet.test: Refreshing state... [id=ocid1.subnet.oc1.phx.] +oci_core_subnet.dev: Refreshing state... [id=ocid1.subnet.oc1.phx.] + +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # oci_core_instance.app will be created + + resource "oci_core_instance" "app" { + + availability_domain = "123:PHX-AD-1" + + boot_volume_id = (known after apply) + + compartment_id = "ocid1." + + dedicated_vm_host_id = (known after apply) + + defined_tags = (known after apply) + + display_name = "app" + + fault_domain = (known after apply) + + 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) + + preserve_boot_volume = false + + private_ip = (known after apply) + + public_ip = (known after apply) + + region = (known after apply) + + shape = "VM.Standard.E3.Flex" + + state = (known after apply) + + subnet_id = (known after apply) + + system_tags = (known after apply) + + time_created = (known after apply) + + time_maintenance_reboot_due = (known after apply) + + + agent_config { + + is_management_disabled = (known after apply) + + is_monitoring_disabled = (known after apply) + } + + + availability_config { + + recovery_action = (known after apply) + } + + + create_vnic_details { + + assign_public_ip = "false" + + defined_tags = (known after apply) + + display_name = (known after apply) + + freeform_tags = (known after apply) + + hostname_label = (known after apply) + + private_ip = (known after apply) + + skip_source_dest_check = (known after apply) + + subnet_id = "ocid1.subnet.oc1.phx." + + vlan_id = (known after apply) + } + + + instance_options { + + are_legacy_imds_endpoints_disabled = (known after apply) + } + + + launch_options { + + boot_volume_type = (known after apply) + + firmware = (known after apply) + + is_consistent_volume_naming_enabled = (known after apply) + + is_pv_encryption_in_transit_enabled = (known after apply) + + network_type = (known after apply) + + remote_data_volume_type = (known after apply) + } + + + shape_config { + + gpu_description = (known after apply) + + gpus = (known after apply) + + local_disk_description = (known after apply) + + local_disks = (known after apply) + + local_disks_total_size_in_gbs = (known after apply) + + max_vnic_attachments = (known after apply) + + memory_in_gbs = 1 + + networking_bandwidth_in_gbps = (known after apply) + + ocpus = 1 + + processor_description = (known after apply) + } + + + source_details { + + boot_volume_size_in_gbs = (known after apply) + + kms_key_id = (known after apply) + + source_id = "ocid1.image.oc1.phx." + + source_type = "image" + } + } + + # oci_objectstorage_bucket.app will be created + + resource "oci_objectstorage_bucket" "app" { + + access_type = "NoPublicAccess" + + approximate_count = (known after apply) + + approximate_size = (known after apply) + + bucket_id = (known after apply) + + compartment_id = "ocid1.compartment.oc1..12345ABCDEF" + + created_by = (known after apply) + + defined_tags = (known after apply) + + etag = (known after apply) + + freeform_tags = (known after apply) + + id = (known after apply) + + is_read_only = (known after apply) + + kms_key_id = (known after apply) + + name = "App_Data" + + namespace = "" + + object_events_enabled = (known after apply) + + object_lifecycle_policy_etag = (known after apply) + + replication_enabled = (known after apply) + + storage_tier = (known after apply) + + time_created = (known after apply) + + versioning = (known after apply) + } + +Plan: 2 to add, 0 to change, 0 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. +``` + +Let's proceed forward and ask Terraform to apply (deploy) it: + +``` +$ terraform 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_objectstorage_bucket.app: Creating... +oci_core_instance.app: Creating... +oci_objectstorage_bucket.app: Creation complete after 2s [id=n//b/App_Data] +oci_core_instance.app: Still creating... [10s elapsed] +oci_core_instance.app: Still creating... [20s elapsed] +oci_core_instance.app: Still creating... [30s elapsed] +oci_core_instance.app: Still creating... [40s elapsed] +oci_core_instance.app: Still creating... [50s elapsed] +oci_core_instance.app: Still creating... [1m0s elapsed] +oci_core_instance.app: Still creating... [1m10s elapsed] +oci_core_instance.app: Still creating... [1m20s elapsed] +oci_core_instance.app: Still creating... [1m30s elapsed] +oci_core_instance.app: Creation complete after 1m31s [id=ocid1.instance.oc1.phx.] + +Apply complete! Resources: 2 added, 0 changed, 0 destroyed. +``` + +The bucket and the compute instance were created in parallel. This isn't what is needed, as in our fictitious example, the bucket must exist *before* the compute instance is created. In this fictitious scenario, pretend that there's a cloud-init configuration that references something in the bucket. If the bucket doesn't exist *prior* to the compute instance, provisioning would fail. + +Let's remove the instance just created: + +``` +$ terraform destroy -target=oci_core_instance.app + +# ... + +Plan: 0 to add, 0 to change, 1 to destroy. + +# ... + +Do you really want to destroy all resources? + Terraform will destroy all your managed infrastructure, as shown above. + There is no undo. Only 'yes' will be accepted to confirm. + + Enter a value: yes + +# ... + +Destroy complete! Resources: 1 destroyed. +``` + +Now destroy the bucket: + +``` +$ terraform destroy -target=oci_objectstorage_bucket.app + +# ... + +Plan: 0 to add, 0 to change, 1 to destroy. + +# ... + +Do you really want to destroy all resources? + Terraform will destroy all your managed infrastructure, as shown above. + There is no undo. Only 'yes' will be accepted to confirm. + + Enter a value: yes + +# ... + +Destroy complete! Resources: 1 destroyed. +``` + +Explicit dependencies need to be added to ensure the order-of-precedence for resource creation is correct. Make the beginning of the `oci_core_instance.app` resource look like the following (notice the only addition is the `depends_on` line) in your `main.tf` file: + +```terraform +resource "oci_core_instance" "app" { + depends_on = [oci_objectstorage_bucket.app] + availability_domain = lookup(data.oci_identity_availability_domains.ads.availability_domains[0],"name") +``` + +Run `terraform apply`: + +``` +$ terraform 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_objectstorage_bucket.app: Creating... +oci_objectstorage_bucket.app: Creation complete after 3s [id=n//b/App_Data] +oci_core_instance.app: Creating... +oci_core_instance.app: Still creating... [10s elapsed] +oci_core_instance.app: Still creating... [20s elapsed] +oci_core_instance.app: Still creating... [30s elapsed] +oci_core_instance.app: Still creating... [40s elapsed] +oci_core_instance.app: Still creating... [50s elapsed] +oci_core_instance.app: Still creating... [1m0s elapsed] +oci_core_instance.app: Still creating... [1m10s elapsed] +oci_core_instance.app: Creation complete after 1m15s [id=ocid1.instance.oc1.phx.] + +Apply complete! Resources: 2 added, 0 changed, 0 destroyed. +``` + +Success! Take notice of how Terraform recognized that the Bucket needed to be created *before* the Compute Instance. It is possible to add multiple explicit resource dependencies by simply providing a list of resources (`depends_on = [rez_type.first, rez_type.second]`). + +Making things even better, Terraform's smart enough to know how to destroy resources in the proper order, following the implicit and explicit resource dependencies. See this in action by removing the Bucket with the terraform destroy command: + +``` +$ terraform destroy -target=oci_objectstorage_bucket.app + +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.app will be destroyed + - resource "oci_core_instance" "app" { + - availability_domain = "123:PHX-AD-1" -> null + - boot_volume_id = "ocid1.bootvolume.oc1.phx." -> null + - compartment_id = "ocid1." -> null + - defined_tags = {} -> null + - display_name = "app" -> null + - fault_domain = "FAULT-DOMAIN-2" -> null + - freeform_tags = {} -> null + - hostname_label = "app" -> null + - id = "ocid1.instance.oc1.phx." -> null + - image = "ocid1.image.oc1.phx." -> null + - launch_mode = "NATIVE" -> null + - preserve_boot_volume = false -> null + - private_ip = "172.16.0.172" -> null + - region = "phx" -> null + - shape = "VM.Standard.E3.Flex" -> null + - state = "RUNNING" -> null + - subnet_id = "ocid1.subnet.oc1.phx." -> null + - system_tags = {} -> null + - time_created = "2021-02-09 21:52:02.115 +0000 UTC" -> null + + - agent_config { + - is_management_disabled = false -> null + - is_monitoring_disabled = false -> null + } + + - availability_config { + - recovery_action = "RESTORE_INSTANCE" -> null + } + + - create_vnic_details { + - assign_public_ip = "false" -> null + - defined_tags = {} -> null + - display_name = "app" -> null + - freeform_tags = {} -> null + - hostname_label = "app" -> null + - private_ip = "172.16.0.172" -> null + - skip_source_dest_check = false -> null + - subnet_id = "ocid1.subnet.oc1.phx." -> null + } + + - instance_options { + - are_legacy_imds_endpoints_disabled = false -> null + } + + - launch_options { + - boot_volume_type = "PARAVIRTUALIZED" -> null + - firmware = "UEFI_64" -> null + - is_consistent_volume_naming_enabled = true -> null + - is_pv_encryption_in_transit_enabled = false -> null + - network_type = "VFIO" -> null + - remote_data_volume_type = "PARAVIRTUALIZED" -> null + } + + - shape_config { + - gpus = 0 -> null + - local_disks = 0 -> null + - local_disks_total_size_in_gbs = 0 -> null + - max_vnic_attachments = 2 -> null + - memory_in_gbs = 1 -> null + - networking_bandwidth_in_gbps = 1 -> null + - ocpus = 1 -> null + - processor_description = "2.25 GHz AMD EPYC™ 7742 (Rome)" -> null + } + + - source_details { + - boot_volume_size_in_gbs = "47" -> null + - source_id = "ocid1.image.oc1.phx." -> null + - source_type = "image" -> null + } + } + + # oci_objectstorage_bucket.app will be destroyed + - resource "oci_objectstorage_bucket" "app" { + - access_type = "NoPublicAccess" -> null + - approximate_count = "0" -> null + - approximate_size = "0" -> null + - bucket_id = "ocid1.bucket.oc1.phx." -> null + - compartment_id = "ocid1." -> null + - created_by = "ocid1.user.oc1.." -> null + - defined_tags = {} -> null + - etag = "1234567" -> null + - freeform_tags = {} -> null + - id = "n//b/App_Data" -> null + - is_read_only = false -> null + - name = "App_Data" -> null + - namespace = "" -> null + - object_events_enabled = false -> null + - replication_enabled = false -> null + - storage_tier = "Standard" -> null + - time_created = "2021-02-09 21:51:59.295 +0000 UTC" -> null + - versioning = "Disabled" -> null + } + +Plan: 0 to add, 0 to change, 2 to destroy. + + +Warning: Resource targeting is in effect + +You are creating a plan with the -target option, which means that the result +of this plan may not represent all of the changes requested by the current +configuration. + +The -target option is not for routine use, and is provided only for +exceptional situations such as recovering from errors or mistakes, or when +Terraform specifically suggests to use it as part of an error message. + +Do you really want to destroy all resources? + Terraform will destroy all your managed infrastructure, as shown above. + There is no undo. Only 'yes' will be accepted to confirm. + + Enter a value: yes + +oci_core_instance.app: Destroying... [id=ocid1.instance.oc1.phx.] +oci_core_instance.app: Still destroying... [id=ocid1.instance.oc1.phx., 10s elapsed] +oci_core_instance.app: Still destroying... [id=ocid1.instance.oc1.phx., 20s elapsed] +oci_core_instance.app: Still destroying... [id=ocid1.instance.oc1.phx., 30s elapsed] +oci_core_instance.app: Still destroying... [id=ocid1.instance.oc1.phx., 40s elapsed] +oci_core_instance.app: Still destroying... [id=ocid1.instance.oc1.phx., 50s elapsed] +oci_core_instance.app: Still destroying... [id=ocid1.instance.oc1.phx., 1m0s elapsed] +oci_core_instance.app: Still destroying... [id=ocid1.instance.oc1.phx., 1m10s elapsed] +oci_core_instance.app: Still destroying... [id=ocid1.instance.oc1.phx., 1m20s elapsed] +oci_core_instance.app: Still destroying... [id=ocid1.instance.oc1.phx., 1m30s elapsed] +oci_core_instance.app: Still destroying... [id=ocid1.instance.oc1.phx., 1m40s elapsed] +oci_core_instance.app: Destruction complete after 1m50s +oci_objectstorage_bucket.app: Destroying... [id=n//b/App_Data] +oci_objectstorage_bucket.app: Destruction complete after 3s + +Warning: Applied changes may be incomplete + +The plan was created with the -target option in effect, so some changes +requested in the configuration may have been ignored and the output values may +not be fully updated. Run the following command to verify that no other +changes are pending: + terraform plan + +Note that the -target option is not suitable for routine use, and is provided +only for exceptional situations such as recovering from errors or mistakes, or +when Terraform specifically suggests to use it as part of an error message. + + +Destroy complete! Resources: 2 destroyed. +``` + +Terraform recognized that to destroy the bucket, it must destroy the compute instance first! This is terrific - that means that I can safely create and destroy resources, even when there are carefully crafted resource dependencies. + +It's time to clean-up your code and delete the following lines from your `main.tf` file (you don't need the Bucket or compute instance any longer): + +```terraform +# OSS Bucket +data "oci_objectstorage_namespace" "this" { +} + +resource "oci_objectstorage_bucket" "app" { + compartment_id = var.tenancy_ocid + name = "App_Data" + namespace = data.oci_objectstorage_namespace.this.namespace + access_type = "NoPublicAccess" +} + +# Get all Availability Domains for the region +data "oci_identity_availability_domains" "ads" { + compartment_id = var.tenancy_ocid +} + +# Compute images +data "oci_core_images" "this" { + compartment_id = "" + state = "AVAILABLE" + operating_system = "Oracle Linux" + shape = "VM.Standard.E3.Flex" + sort_by = "DISPLAYNAME" + sort_order = "DESC" +} + +# Compute instance +resource "oci_core_instance" "app" { + depends_on = [oci_objectstorage_bucket.app] + availability_domain = lookup(data.oci_identity_availability_domains.ads.ailability_domains[0], "name") + compartment_id = var.tenancy_ocid + shape = "VM.Standard.E3.Flex" + display_name = "app" + + shape_config { + memory_in_gbs = 1 + ocpus = 1 + } + create_vnic_details { + assign_public_ip = false + subnet_id = oci_core_subnet.dev.id + } + source_details { + source_id = data.oci_core_images.this.images[0].id + source_type = "image" + } + preserve_boot_volume = false +} +``` + +With the above lines deleted, the environment should be back to a single VCN and two Subnets. + +The [next tutorial](/tutorials/tf-201/2-remotes) will cover [Remote state in Terraform](/tutorials/tf-201/2-remotes). \ No newline at end of file diff --git a/tf-201/2-remotes.md b/tf-201/2-remotes.md new file mode 100644 index 0000000..1adc000 --- /dev/null +++ b/tf-201/2-remotes.md @@ -0,0 +1,93 @@ +--- +title: Using remote states with Terraform +parent: tf-201 +tags: [open-source, terraform, iac, devops, intermediate] +categories: [iac, opensource] +thumbnail: assets/terraform-201.png +date: 2021-10-07 6:17 +description: This tutorial shows some of the options for storing your Terraform state remotely. +toc: true +author: tim-clegg +--- +{% img aligncenter assets/terraform-201.png 400 400 "Terraform 201" "Terraform 201 Tutorial Series" %} + +Terraform by default stores the state locally. This works great when a single person is managing an environment. When multiple people are managing an environment, this doesn't scale well. Managing and sharing the Terraform state between team members is an important facet in any Terraform-managed environment. + +## Remote State Storage Options +There are several options available for storing Terraform state remotely on Oracle Cloud Infrastructure (OCI), including (but not limited to): + +* OCI Resource Manager +* OCI Object Storage +* Git repository + +## OCI Resource Manager +OCI Resource Manager (ORM) is a way to effortlessly run Terraform through an Oracle-managed cloud service that's integrated into OCI. ORM takes care of managing the Terraform state and is a great way to quickly and easily manage OCI infrastructure. + +ORM allows for the manual management of OCI infrastructure by the uploading of Terraform stacks, then plan/apply activities which are user-initiatied. The service also might be integrated into an automated pipeline, where the interactions with ORM take place via the ORM API (to upload, plan and apply stacks). + +It's possible to integrate ORM with many popular version control systems (VCS), including GitHub and GitLab, eliminating the need to upload a Terraform stack by hand, but rather have ORM read a Git repository for the Terraform code it should use. ORM makes it possible to achieve a more streamlined CI/CD pipeline between your code and its final implementation in OCI! + +In situations where Terraform will be used to manage pre-existing resources (already in-place, but not currently managed by Terraform), ORM can allow you to quickly discover these existing resources in your OCI tenancy. This is particularly helpful for environments that have not been built and maintained using Terraform, but should be managed by Terraform going forward. The resource discovery functionality can save an enormous amount of time in creating Terraform code based on the resources present at the time of discovery. + +Taking the resource discovery functionality a step further, ORM makes it easy to perform drift detection, allowing you to look for unwanted changes in the environment (those that have taken place outside of the Terraform stack). + +To learn more about ORM, please look at the [OCI ORM documentation](https://docs.oracle.com/en-us/iaas/Content/ResourceManager/Concepts/landing.htm). + +## OCI Object Storage +This is an ideal place to store your state, as it supports versioning (being able to look back at previous versions of the Terraform state) and is highly-available. To use OCI Object Storage, follow these steps. + +1. Create the Bucket in OCI Object Storage +2. Tell Terraform to use the Bucket + +Because you need to create the Bucket *before* you can use it in a Terraform project, you'll need to have two Terraform projects if you take this route: +* One Terraform project to create the Bucket for the Terraform project +* Another Terraform project to manage your other resources + +You could create the Bucket via the Console or CLI, however here’s how you might do it using Terraform. + +```terraform +data "oci_objectstorage_namespace" "this" {} + +resource "oci_objectstorage_bucket" "env1-tfstate" { + compartment_id = var.tenancy_ocid + name = "env1-tfstate" + namespace = data.oci_objectstorage_namespace.this.namespace + access_type = "NoPublicAccess" + object_events_enabled = false + storage_tier = "Standard" + versioning = "Enabled" +} +``` + +Whether you created the Bucket in the Console or via Terraform, here’s how to tell Terraform to use it: + +```terraform +terraform { + backend "s3" { + bucket = "env1-tfstate" + key = "terraform.tfstate" + region = var.region + endpoint = "https://${data.oci_objecstorage_namespace.this.namespace}.compat.objectstorage.${var.region}.oraclecloud.com" + access_key = var.tf_access_key + secret_key = var.tf_secret_key + skip_region_validation = true + skip_credentials_validation = true + skip_metadata_api_check = true + force_path_style = true + } +} +``` + +The `tf_access_key` and `tf_secret_key` variables need to be set (ideally using environment variables). By using variables, you can avoid committing sensitive information to a Git repo, plus in the case of automated pipelines, variables make it easy to integrate with a secret store. + +For more information on using the OCI Object Storage as a backend for the Terraform state, take a look at the [OCI Provider documentation](https://registry.terraform.io/providers/hashicorp/oci/latest/docs/guides/object_store_backend). + +## Git Repository +While it’s possible to use a Git repository for storing Terraform state remotely, it’s not ideal. Git isn’t a real-time shared file system, so if multiple people are working on the same environment at the same time, it’s possible that they’ll end up with two separate versions of the state file, which is not desirable (requiring some manual intervention to resolve the situation). When using a git repository for Terraform state storage, coordination between between team members is typically needed, to ensure that only one person is making changes at any given point in time (who is doing it and when the changes are being made to the environment). + +When using git, it's important to `git pull` (update your local copy) prior to making any changes (to make sure that both the code as well as state is up-to-date). At this point you can typically make your changes (making sure you communicate with your team that you're doing this, effectively simulating a file locking mechanism). After making your changes, commit and push your changes (so your team can pull the changes). This is an overly-simplified view, but gives you an idea of what needs to take place. + +## Conclusion +We've only touched a couple of the many backends available. Check out the [Terraform documentation](https://www.terraform.io/docs/language/settings/backends/index.html) for more information and ideas. + +Because the Terraform state is such a critically important component, finding a good home for it is an important early step to take with any environment. Consider your options, desired workflow and choose accordingly! diff --git a/tf-201/3-modules.md b/tf-201/3-modules.md new file mode 100644 index 0000000..74ad23a --- /dev/null +++ b/tf-201/3-modules.md @@ -0,0 +1,1333 @@ +--- +title: Terraform Modules +parent: tf-201 +tags: [open-source, terraform, iac, devops, intermediate] +categories: [iac, opensource] +thumbnail: assets/terraform-201.png +date: 2021-10-12 14:44 +description: Learn how to wield the power of Terraform modules in your environment! +toc: true +author: tim-clegg +--- +{% img aligncenter assets/terraform-201.png 400 400 "Terraform 201" "Terraform 201 Tutorial Series" %} + +Terraform supports the ability to define a series of "building blocks" (so to speak), that can be easily deployed. This modularity is achieved through the use of... drum roll, please... modules. + +## What is a module? +Modules are made of "normal" Terraform code and consist of what you're already familiar with: inputs (variables/locals), outputs and resource definitions. The key difference is that a module is designed to be used (invoked or "instantiated" in traditional programming speak) from within a Terraform project. They're not designed to operate independently by themselves, but rather to be used as one part of a broader Terraform project (the project using/instantiating the module). + +Most of the time, modules are purpose-built for a specific task/aspect/component of an environment. Due to the extreme flexibility, how purpose-built you decide to go is entirely up to you! + +For instance, some modules are designed from the perspective of a very small aspect of the environment (such as managing a Subnet and its Route Table, Security Lists, DHCP Options, etc.), others comprise *lots* of resources (VCN, Subnets, compute instances, block volumes, object storage buckets, etc.). Here are some of the different ways that modules can be designed around: + +* Resource(s) +* Components within an environment +* Role +* Entire environment + +There's really no limit or one-size-fits-all approach to carving out modules for your environment. It's a good idea to sit down and think through how your organization operates, as well as the type of resources that will be managed. For some organizations, maintaining a clear separation of duty might impact how modules are created and used (more focused/finite in scope), while others might take a "flatter" approach (one module to manage all resources). + +It's possible to construct projects based on nested modules, which can get a bit complex, but can also be powerful. + +> **NOTE:** Nested modules can be very powerful, but can also significantly increase the complexity and difficulty in building/maintaining the environment. Nesting modules is not for the faint of heart! +{:.notice} + +## Module design +A module offers the ability to provide a new, different, abstracted interface for a given use-case. This sounds a bit confusing, right? Whether you're deploying a single resource or many resources, encapsulating this into a Terraform module allows *you* to specify what kind of user interface consumers of the module will experience. + +You control the inputs (variables) as well as the outputs that users consuming the module interacts with. This is pretty powerful, as you can provide a really abstracted, simplified user interface (UI) for very complex and involved topologies (if you want to). Or you can expose every nerd-knob possible. The choice is yours. + +It's important to realize the benefits and drawbacks of having abstracted interfaces. + +**Benefits** +* It's easy to deliver a purpose-built, clean UI. +* Can greatly simplify (DRY) up the UI, by simplifying and distilling the inputs. +* Using "sane defaults" for optional variables, complexity can still be exposed, with some variables being left as optional. +* Great way to create and organization/environment-wide naming/standards, embedding them into the modules, which users use to create/manage their environment. + +**Drawbacks** +* Requires consumers (people using the module) to learn/understand a new UI. Usually they need to know/understand the underlying cloud platform/Terraform provider UIs. Having an abstracted module interface requires yet another UI for consumers to learn/understand to effectively leverage the module(s). +* To simplify things, some level of rigidity can occur in the module UI (out of necessity). +* With rigidity comes lack of usability (unapplicable outside of the narrow, intended use-case the module is designed for). + +## Building your first module +Enough talk, let's get to building a module! From within your Cloud Shell session, run the following: + +```console +cd ~ +mkdir tf-modules +cd tf-modules +``` + +This gives us a brand-new directory for us to work in. + +### Defining the module scope +Let's start by scoping out what the module will do: + +* Deploy a VCN and two Subnets (dev and test). + +### Module UI - inputs +Now let's think about the UI. We're going to keep this really simple and ask for just a few pieces of information: + +| Input | Type | Description | +|-------|------|-------------| +| Tenancy OCID | String | We'll need to know the tenancy OCID to know which tenancy to deploy against. | +| Region | String | We must know which region to deploy to. | +| Compartment OCID | String | Support specifying the compartment to deploy resources into. | +| Environment name | String | We'll have a default value, but will let the user override it if they'd like. | +| Environment CIDR block | String | We'll default to 192.168.0.0/20, but allow the user to specify their own CIDR block if they'd like a different one. | + +Now let's take it a step further and fill-in a few details that we'll need for the actual Terraform code. Let's add the variables and default values to the list: + +| Input | Type | Description | Default Value | Variable name | +|-------|------|-------------|---------------|---------------| +| Tenancy OCID | String | We'll need to know the tenancy OCID to know which tenancy to deploy against. | None - this is required | `tenancy_ocid` | +| Region | String | We must know which region to deploy to. | None - this is required | `region` | +| Compartment OCID | String | Support specifying the compartment to deploy resources into. | None - this is required | `compartment_ocid` | +| Environment name | String | We'll have a default value, but will let the user override it if they'd like. | `awesome_env` | `env_name` | +| Environment CIDR block | String | We'll default to 192.168.0.0/20, but allow the user to specify their own CIDR block if they'd like a different one. | `192.168.0.0/20` | `env_cidr` | + +This is a great start! It's simple, with only the `tenancy_ocid` variable being required (the others have "sane defaults" which make them optional). + +### Module UI - outputs +The last part of our module UI design focuses on the outputs for the module. What might module consumers need *from* this module? A lot of this really comes down to how the environment will be managed. Here are some questions we consider before proceeding: + +* Do we want users to be able to add discrete Subnets to the VCN that will be created, or should this largely be a static, fixed environment? + * This will help determine if we need to export the VCN OCID (or other VCN details). +* Will compute instances be provisioned as part of this module in the future? + * If users will provision Compute instances a-la-carte, we need to export the Subnet OCIDs. +* Since we'll be dynamically determining the Subnet CIDRs, we'll want to provide these values to the user. + +The above are just a few considerations we ponder as we determine the output interface for the module. We'll go with the most flexible approach, giving both the VCN and Subnet OCIDs as outputs, along with the Subnet CIDRs: + +| Name | Value | +|------|-------| +| `vcn_id` | The VCN OCID. | +| `subnet_dev_id` | The OCID of the Development/Dev Subnet. | +| `subnet_dev_cidr` | The dynamically-allocated CIDR block used for the Development Subnet. | +| `subnet_test_id` | The OCID of the Test Subnet. | +| `subnet_test_cidr` | The dynamically-allocated CIDR block used for the Test Subnet. | + +### Coding it +Funny thing, a module is largely just a normal piece of TF code, designed in such a way that it can be used in a generic fashion (called/used many times). + +We'll follow the KISS principle (Keep It Super Simple) and use a local directory for the module: + +```console +mkdir net-mod +cd net-mod +``` + +Now that we're in our "network module" (`net-mod`) directory, let's get busy coding. First we'll put our inputs in the `variables.tf` file: + +```terraform +variable "tenancy_ocid" { + type = string + description = "The tenancy OCID" +} +variable "region" { + type = string + description = "The region to deploy to" +} +variable "compartment_ocid" { + type = string + description = "The OCID of the compartment to deploy resources in." +} +variable "env_name" { + type = string + default = "awesome_env" + description = "The name for the environment" +} +variable "env_cidr" { + type = string + default = "192.168.0.0/20" + description = "The CIDR block to use for the environment." +} +``` + +Let's add some "meat" to this module, giving it something to do. Let's go ahead and add the following to `vcn.tf`: + +```terraform +resource oci_core_vcn "this" { + cidr_block = var.env_cidr + compartment_id = var.compartment_ocid + display_name = var.env_name + dns_label = replace(var.env_name, "/[^A-Za-z0-9]+/", "") +} +``` + +We did a bit of fancy footwork for the `dns_label` attribute. We're essentially stripping out any non-alpha-numeric characters, using the [`replace` Terraform function](https://www.terraform.io/docs/language/functions/replace.html). + +Let's make life simple, by computing the Subnet CIDRs in separate locals. Add this to the `locals.tf` file: + +```terraform +locals { + dev_cidr = cidrsubnets(var.env_cidr, 1, 1)[0] + test_cidr = cidrsubnets(var.env_cidr, 1, 1)[1] +} +``` + +Add the following to `subnets.tf`: + +```terraform +resource oci_core_subnet dev { + cidr_block = local.dev_cidr + compartment_id = var.compartment_ocid + display_name = "dev" + dns_label = "dev" + prohibit_public_ip_on_vnic = "true" + vcn_id = oci_core_vcn.this.id + + lifecycle { + ignore_changes = [ defined_tags["Oracle-Tags.CreatedBy"], defined_tags["Oracle-Tags.CreatedOn"] ] + } +} + +resource oci_core_subnet test { + cidr_block = local.test_cidr + compartment_id = var.compartment_ocid + display_name = "test" + dns_label = "test" + prohibit_public_ip_on_vnic = "true" + vcn_id = oci_core_vcn.this.id + + lifecycle { + ignore_changes = [ defined_tags["Oracle-Tags.CreatedBy"], defined_tags["Oracle-Tags.CreatedOn"] ] + } +} +``` + +And just like that, we have a VCN along with two subnets. The neat thing here is that we're dynamically determining the Subnet CIDRs (based off of the `env_cidr` variable that the user can provide). + +We need to configure the module outputs, so put the following in `outputs.tf`: + +```terraform +output "vcn_id" { + value = oci_core_vcn.this.id +} + +output "subnet_dev_id" { + value = oci_core_subnet.dev.id +} +output "subnet_dev_cidr" { + value = local.dev_cidr +} + +output "subnet_test_id" { + value = oci_core_subnet.test.id +} +output "subnet_test_cidr" { + value = local.test_cidr +} +``` + +The next step is to actually *use* our new module... + +## Invoking (instantiating) modules +Modules can be called from a lot of [sources](https://www.terraform.io/docs/language/modules/sources.html) including local directories, git repos or from the [public Terraform Module Registry](https://registry.terraform.io/browse/modules) (to name just a few). Check out the [Terraform module source documentation](https://www.terraform.io/docs/language/modules/sources.html) for more info. + +> **NOTE:** If you're creating modules that are generic and can be used by most anybody, it's a nice idea to share the goodness and publish them on the [public Terraform Module Registry](https://registry.terraform.io/browse/modules) so others can re-use your awesome work! Check out the directions in the [Terraform documentation](https://www.terraform.io/docs/language/modules/develop/publish.html) for more info on how to do this. + +In our example, we're going to be showing how easy it is to "cookie cutter" roll-out several different environments, side-by-side, using the module we've just created. + +Let's move to our `tf-modules` directory: + +```console +cd ~/tf-modules +``` + +Now place the following in `provider.tf`: + +```terraform +terraform { + required_version = ">= 1.0.0" +} + +provider "oci" { + region = var.region + tenancy_ocid = var.tenancy_ocid +} +``` + +Now we'll place the following in `main.tf` to keep things simple: + +```terraform +module "env_1" { + source = "./net-mod" + + region = var.region + tenancy_ocid = var.tenancy_ocid + compartment_ocid = var.tenancy_ocid +} +``` + +> If you're wanting to use a specific Compartment, make sure to set the `compartment_ocid` attribute to the compartment OCID you wish to use (instead of `var.tenancy_ocid`). +{:.notice} + +Lastly, let's define a few of the variables we'll be using in the Terraform *project* itself. We'll end up having variables declared for the Terraform project as well as in the module(s) we use. Place the following in `variables.tf`: + +```terraform +variable "tenancy_ocid" { + type = string + description = "The tenancy OCID" +} +variable "region" { + type = string + description = "The region to deploy to" +} +``` + +In case this seems a bit confusing, here are the files that we should have at this point: + +``` +. +├── main.tf +├── provider.tf +├── variables.tf +└── net-mod + ├── locals.tf + ├── outputs.tf + ├── subnets.tf + ├── variables.tf + └── vcn.tf +``` + +Remember, the module is completely self-contained, with its own definition of inputs/outputs, as well as resources it'll be managing. The Terraform project that consumes the module has its own set of inputs/outputs and other potential resources (in addition to the module). + +Move forward by initializing the Terraform environment: + +```console +terraform init +``` + +Since we're using the OCI Cloud Shell, the tenancy OCID and region is pre-populated, but we need to put these values in environment variables that Terraform is expecting: + +```console +declare -x TF_VAR_tenancy_ocid=`echo $OCI_TENANCY` +declare -x TF_VAR_region=`echo $OCI_REGION` +``` + +> **NOTE:** If you're prompted to enter a value for the `region` or `tenancy_ocid` variables, it's likely that the environment variables (above) need to be set. Each time you connect to your OCI Cloud Shell session, you'll need to set these. +{:notice} + +Now check what Terraform proposes be done: + +```console +terraform plan + +Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # module.env_1.oci_core_subnet.dev will be created + + resource "oci_core_subnet" "dev" { + + availability_domain = (known after apply) + + cidr_block = "192.168.0.0/21" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "dev" + + dns_label = "dev" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.env_1.oci_core_subnet.test will be created + + resource "oci_core_subnet" "test" { + + availability_domain = (known after apply) + + cidr_block = "192.168.8.0/21" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "test" + + dns_label = "test" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.env_1.oci_core_vcn.this will be created + + resource "oci_core_vcn" "this" { + + cidr_block = "192.168.0.0/20" + + cidr_blocks = (known after apply) + + compartment_id = "ocid1.tenancy.oc1.." + + default_dhcp_options_id = (known after apply) + + default_route_table_id = (known after apply) + + default_security_list_id = (known after apply) + + defined_tags = (known after apply) + + display_name = "awesome_env" + + dns_label = "awesomeenv" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_blocks = (known after apply) + + is_ipv6enabled = (known after apply) + + state = (known after apply) + + time_created = (known after apply) + + vcn_domain_name = (known after apply) + } + +Plan: 3 to add, 0 to change, 0 to destroy. + +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" +now. +$ +``` + +Let's go ahead and deploy these resources: + +```console +terraform apply + + +Plan: 3 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 + +module.env_1.oci_core_vcn.this: Creating... +module.env_1.oci_core_vcn.this: Creation complete after 1s [id=ocid1.vcn.oc1.phx.] +module.env_1.oci_core_subnet.test: Creating... +module.env_1.oci_core_subnet.dev: Creating... +module.env_1.oci_core_subnet.test: Still creating... [10s elapsed] +module.env_1.oci_core_subnet.dev: Still creating... [10s elapsed] +module.env_1.oci_core_subnet.test: Creation complete after 15s [id=ocid1.subnet.oc1.phx.] +module.env_1.oci_core_subnet.dev: Creation complete after 16s [id=ocid1.subnet.oc1.phx.] + +Apply complete! Resources: 3 added, 0 changed, 0 destroyed. +``` + +That's it! Ok, so far, we've not saved much time. In fact, this seems like we've added a bit of complexity (using modules) and wasted time. Let's see why this is so great... + +Go back into `main.tf` and add a few more environments: + +```terraform +module "env_2" { + source = "./net-mod" + + region = var.region + tenancy_ocid = var.tenancy_ocid + compartment_ocid = var.tenancy_ocid + env_cidr = "10.0.0.0/24" + env_name = "Another env" +} + +module "env_3" { + source = "./net-mod" + + region = var.region + tenancy_ocid = var.tenancy_ocid + compartment_ocid = var.tenancy_ocid + env_cidr = "172.16.2.0/24" + env_name = "Env 3" +} +``` + +Now re-initialize Terraform (this is needed because we're calling the module again): + +```console +$ terraform init +Initializing modules... +- env_2 in net-mod +- env_3 in net-mod + +Initializing the backend... + +Initializing provider plugins... +- Reusing previous version of hashicorp/oci from the dependency lock file +- Using previously-installed hashicorp/oci v4.46.0 + +Terraform has been successfully initialized! + +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your working directory. If you forget, other +commands will detect it and remind you to do so if necessary. +$ +``` + +> **NOTE:** Any time you have a new module definition, or update the module code, you should re-initialize Terraform (`terraform init`). If you don't do this, when trying to call a new module definition, you'll get an error like the following: +> ``` +$ terraform apply +╷ +│ Error: Module not installed +│ +│ on main.tf line 9: +│ 9: module "env_2" { +│ +│ This module is not yet installed. Run "terraform init" to install all modules required by this configuration. +╵ +╷ +│ Error: Module not installed +│ +│ on main.tf line 19: +│ 19: module "env_3" { +│ +│ This module is not yet installed. Run "terraform init" to install all modules required by this configuration. +╵ +$ +``` +{:notice} + +Now apply it (yes, we're taking a shortcut here, bypassing running `terraform plan`): + +```console +$ terraform apply +module.env_1.oci_core_vcn.this: Refreshing state... [id=ocid1.vcn.oc1.phx.] +module.env_1.oci_core_subnet.test: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.env_1.oci_core_subnet.dev: Refreshing state... [id=ocid1.subnet.oc1.phx.] + +Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # module.env_2.oci_core_subnet.dev will be created + + resource "oci_core_subnet" "dev" { + + availability_domain = (known after apply) + + cidr_block = "10.0.0.0/25" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "dev" + + dns_label = "dev" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.env_2.oci_core_subnet.test will be created + + resource "oci_core_subnet" "test" { + + availability_domain = (known after apply) + + cidr_block = "10.0.0.128/25" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "test" + + dns_label = "test" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.env_2.oci_core_vcn.this will be created + + resource "oci_core_vcn" "this" { + + cidr_block = "10.0.0.0/24" + + cidr_blocks = (known after apply) + + compartment_id = "ocid1.compartment.oc1.." + + default_dhcp_options_id = (known after apply) + + default_route_table_id = (known after apply) + + default_security_list_id = (known after apply) + + defined_tags = (known after apply) + + display_name = "Another env" + + dns_label = "Anotherenv" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_blocks = (known after apply) + + is_ipv6enabled = (known after apply) + + state = (known after apply) + + time_created = (known after apply) + + vcn_domain_name = (known after apply) + } + + # module.env_3.oci_core_subnet.dev will be created + + resource "oci_core_subnet" "dev" { + + availability_domain = (known after apply) + + cidr_block = "172.16.2.0/25" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "dev" + + dns_label = "dev" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.env_3.oci_core_subnet.test will be created + + resource "oci_core_subnet" "test" { + + availability_domain = (known after apply) + + cidr_block = "172.16.2.128/25" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "test" + + dns_label = "test" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.env_3.oci_core_vcn.this will be created + + resource "oci_core_vcn" "this" { + + cidr_block = "172.16.2.0/24" + + cidr_blocks = (known after apply) + + compartment_id = "ocid1.compartment.oc1.." + + default_dhcp_options_id = (known after apply) + + default_route_table_id = (known after apply) + + default_security_list_id = (known after apply) + + defined_tags = (known after apply) + + display_name = "Env 3" + + dns_label = "Env3" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_blocks = (known after apply) + + is_ipv6enabled = (known after apply) + + state = (known after apply) + + time_created = (known after apply) + + vcn_domain_name = (known after apply) + } + +Plan: 6 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 + +module.env_3.oci_core_vcn.this: Creating... +module.env_2.oci_core_vcn.this: Creating... +module.env_3.oci_core_vcn.this: Creation complete after 2s [id=ocid1.vcn.oc1.phx.] +module.env_3.oci_core_subnet.test: Creating... +module.env_3.oci_core_subnet.dev: Creating... +module.env_2.oci_core_vcn.this: Creation complete after 1s [id=ocid1.vcn.oc1.phx.] +module.env_2.oci_core_subnet.test: Creating... +module.env_2.oci_core_subnet.dev: Creating... +module.env_3.oci_core_subnet.dev: Creation complete after 4s [id=ocid1.subnet.oc1.phx.] +module.env_2.oci_core_subnet.dev: Creation complete after 5s [id=ocid1.subnet.oc1.phx.] +module.env_3.oci_core_subnet.test: Creation complete after 5s [id=ocid1.subnet.oc1.phx.] +module.env_2.oci_core_subnet.test: Creation complete after 5s [id=ocid1.subnet.oc1.phx.] + +Apply complete! Resources: 6 added, 0 changed, 0 destroyed. +$ +``` + +Ok, now we're able to see the value of modules! By simply adding a few lines of code, we were able to create multiple environments. Each new environment would involve adding a few lines of code (referencing the module we created). It's that easy! The value increases as you have modules that manage more resources. + +### Reading values from modules +So far we've seen how to create modules as well as how to "call" them. I'd like to see the Subnet CIDRs it's selected, wouldn't you? Let's do this now! Add the following to `outputs.tf` (this will be a new file at this point): + +```terraform +output "env_1_dev_cidr" { + value = module.env_1.subnet_dev_cidr +} +output "env_1_test_cidr" { + value = module.env_1.subnet_test_cidr +} + +output "env_2_dev_cidr" { + value = module.env_2.subnet_dev_cidr +} +output "env_2_test_cidr" { + value = module.env_2.subnet_test_cidr +} + +output "env_3_dev_cidr" { + value = module.env_3.subnet_dev_cidr +} +output "env_3_test_cidr" { + value = module.env_3.subnet_test_cidr +} +``` + +See these values by re-running `terraform apply`: + +```console +$ terraform apply -auto-approve +module.env_1.oci_core_vcn.this: Refreshing state... [id=ocid1.vcn.oc1.phx.] +module.env_1.oci_core_subnet.dev: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.env_1.oci_core_subnet.test: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.env_2.oci_core_vcn.this: Refreshing state... [id=ocid1.vcn.oc1.phx.] +module.env_3.oci_core_vcn.this: Refreshing state... [id=ocid1.vcn.oc1.phx.] +module.env_3.oci_core_subnet.test: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.env_3.oci_core_subnet.dev: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.env_2.oci_core_subnet.test: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.env_2.oci_core_subnet.dev: Refreshing state... [id=ocid1.subnet.oc1.phx.] + +Changes to Outputs: + + env_1_dev_cidr = "192.168.0.0/21" + + env_1_test_cidr = "192.168.8.0/21" + + env_2_dev_cidr = "10.0.0.0/25" + + env_2_test_cidr = "10.0.0.128/25" + + env_3_dev_cidr = "172.16.2.0/25" + + env_3_test_cidr = "172.16.2.128/25" + +You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure. + +Apply complete! Resources: 0 added, 0 changed, 0 destroyed. + +Outputs: + +env_1_dev_cidr = "192.168.0.0/21" +env_1_test_cidr = "192.168.8.0/21" +env_2_dev_cidr = "10.0.0.0/25" +env_2_test_cidr = "10.0.0.128/25" +env_3_dev_cidr = "172.16.2.0/25" +env_3_test_cidr = "172.16.2.128/25" +$ +``` + +> **NOTE:** We used the `-auto-approve` parameter above, as a shortcut. This argument tells Terraform to *not* prompt you to continue - it just runs, no questions asked. This is great for automated pipelines (where user input cannot be obtained), but can be dangerous. Use it with care. I used it here as the only change we made was the outputs, otherwise it wouldn't be safe to use this parameter. +{:alert} + +The `outputs` area is what we're looking for. Sure enough, there are the Subnet CIDRs that our module decided upon! We'd likewise be able to get the Subnet (and VCN) OCIDs if we so desire. + +## Iterating modules +So far we've only called a single module. It is possible to instantiate several modules at one time, iterating through them. Terraform supports using the [`count`](https://www.terraform.io/docs/language/meta-arguments/count.html) and [`for_each`](https://www.terraform.io/docs/language/meta-arguments/for_each.html) arguments in module definitions. + +Here's how we might use the `for_each` attribute in our scenario. Before we proceed, let's go ahead and destroy all of the existing resources: + +```console +$ terraform destroy + + + +Plan: 0 to add, 0 to change, 9 to destroy. + +Changes to Outputs: + - env_1_dev_cidr = "192.168.0.0/21" -> null + - env_1_test_cidr = "192.168.8.0/21" -> null + - env_2_dev_cidr = "10.0.0.0/25" -> null + - env_2_test_cidr = "10.0.0.128/25" -> null + - env_3_dev_cidr = "172.16.2.0/25" -> null + - env_3_test_cidr = "172.16.2.128/25" -> null + +Do you really want to destroy all resources? + Terraform will destroy all your managed infrastructure, as shown above. + There is no undo. Only 'yes' will be accepted to confirm. + + Enter a value: yes + +module.env_2.oci_core_subnet.test: Destroying... [id=ocid1.subnet.oc1.phx.] +module.env_2.oci_core_subnet.dev: Destroying... [id=ocid1.subnet.oc1.phx.] +module.env_3.oci_core_subnet.dev: Destroying... [id=ocid1.subnet.oc1.phx.] +module.env_3.oci_core_subnet.test: Destroying... [id=ocid1.subnet.oc1.phx.] +module.env_1.oci_core_subnet.test: Destroying... [id=ocid1.subnet.oc1.phx.] +module.env_1.oci_core_subnet.dev: Destroying... [id=ocid1.subnet.oc1.phx.] +module.env_3.oci_core_subnet.dev: Destruction complete after 1s +module.env_2.oci_core_subnet.test: Destruction complete after 2s +module.env_1.oci_core_subnet.dev: Destruction complete after 1s +module.env_3.oci_core_subnet.test: Destruction complete after 1s +module.env_3.oci_core_vcn.this: Destroying... [id=ocid1.vcn.oc1.phx.] +module.env_2.oci_core_subnet.dev: Destruction complete after 2s +module.env_2.oci_core_vcn.this: Destroying... [id=ocid1.vcn.oc1.phx.] +module.env_3.oci_core_vcn.this: Destruction complete after 0s +module.env_1.oci_core_subnet.test: Destruction complete after 1s +module.env_1.oci_core_vcn.this: Destroying... [id=ocid1.vcn.oc1.phx.] +module.env_2.oci_core_vcn.this: Destruction complete after 1s +module.env_1.oci_core_vcn.this: Destruction complete after 1s + +Destroy complete! Resources: 9 destroyed. +$ +``` + +Let's modify things a bit, making `main.tf` look like the following: + +```terraform +locals { + envs = { + "awesome_1" = "10.0.0.0/20", + "env_2" = "192.168.10.0/24", + "team_3" = "10.10.10.0/25" + } +} +module "envs" { + for_each = local.envs + source = "./net-mod" + + region = var.region + tenancy_ocid = var.tenancy_ocid + compartment_ocid = var.compartment_ocid + env_name = each.key + env_cidr = each.value +} +``` + +Modify the contents of `outputs.tf` to look like: + +```terraform +output "env_1_dev_cidr" { + value = module.envs[keys(local.envs)[0]].subnet_dev_cidr +} +output "env_1_test_cidr" { + value = module.envs[keys(local.envs)[0]].subnet_test_cidr +} + +output "env_2_dev_cidr" { + value = module.envs[keys(local.envs)[1]].subnet_dev_cidr +} +output "env_2_test_cidr" { + value = module.envs[keys(local.envs)[1]].subnet_test_cidr +} + +output "env_3_dev_cidr" { + value = module.envs[keys(local.envs)[2]].subnet_dev_cidr +} +output "env_3_test_cidr" { + value = module.envs[keys(local.envs)[2]].subnet_test_cidr +} +``` + +Re-initialize Terraform (this is needed anytime we add/remove a module to a Terraform project): + +```console +$ terraform init + + +``` + +Look at the plan: + +```console +$ terraform plan + +Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # module.envs["awesome_1"].oci_core_subnet.dev will be created + + resource "oci_core_subnet" "dev" { + + availability_domain = (known after apply) + + cidr_block = "10.0.0.0/21" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "dev" + + dns_label = "dev" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.envs["awesome_1"].oci_core_subnet.test will be created + + resource "oci_core_subnet" "test" { + + availability_domain = (known after apply) + + cidr_block = "10.0.8.0/21" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "test" + + dns_label = "test" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.envs["awesome_1"].oci_core_vcn.this will be created + + resource "oci_core_vcn" "this" { + + cidr_block = "10.0.0.0/20" + + cidr_blocks = (known after apply) + + compartment_id = "ocid1.compartment.oc1.." + + default_dhcp_options_id = (known after apply) + + default_route_table_id = (known after apply) + + default_security_list_id = (known after apply) + + defined_tags = (known after apply) + + display_name = "awesome_1" + + dns_label = "awesome1" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_blocks = (known after apply) + + is_ipv6enabled = (known after apply) + + state = (known after apply) + + time_created = (known after apply) + + vcn_domain_name = (known after apply) + } + + # module.envs["env_2"].oci_core_subnet.dev will be created + + resource "oci_core_subnet" "dev" { + + availability_domain = (known after apply) + + cidr_block = "192.168.10.0/25" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "dev" + + dns_label = "dev" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.envs["env_2"].oci_core_subnet.test will be created + + resource "oci_core_subnet" "test" { + + availability_domain = (known after apply) + + cidr_block = "192.168.10.128/25" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "test" + + dns_label = "test" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.envs["env_2"].oci_core_vcn.this will be created + + resource "oci_core_vcn" "this" { + + cidr_block = "192.168.10.0/24" + + cidr_blocks = (known after apply) + + compartment_id = "ocid1.compartment.oc1.." + + default_dhcp_options_id = (known after apply) + + default_route_table_id = (known after apply) + + default_security_list_id = (known after apply) + + defined_tags = (known after apply) + + display_name = "env_2" + + dns_label = "env2" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_blocks = (known after apply) + + is_ipv6enabled = (known after apply) + + state = (known after apply) + + time_created = (known after apply) + + vcn_domain_name = (known after apply) + } + + # module.envs["team_3"].oci_core_subnet.dev will be created + + resource "oci_core_subnet" "dev" { + + availability_domain = (known after apply) + + cidr_block = "10.10.10.0/26" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "dev" + + dns_label = "dev" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.envs["team_3"].oci_core_subnet.test will be created + + resource "oci_core_subnet" "test" { + + availability_domain = (known after apply) + + cidr_block = "10.10.10.64/26" + + compartment_id = "ocid1.compartment.oc1.." + + defined_tags = (known after apply) + + dhcp_options_id = (known after apply) + + display_name = "test" + + dns_label = "test" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_block = (known after apply) + + ipv6virtual_router_ip = (known after apply) + + prohibit_internet_ingress = (known after apply) + + prohibit_public_ip_on_vnic = true + + route_table_id = (known after apply) + + security_list_ids = (known after apply) + + state = (known after apply) + + subnet_domain_name = (known after apply) + + time_created = (known after apply) + + vcn_id = (known after apply) + + virtual_router_ip = (known after apply) + + virtual_router_mac = (known after apply) + } + + # module.envs["team_3"].oci_core_vcn.this will be created + + resource "oci_core_vcn" "this" { + + cidr_block = "10.10.10.0/25" + + cidr_blocks = (known after apply) + + compartment_id = "ocid1.compartment.oc1.." + + default_dhcp_options_id = (known after apply) + + default_route_table_id = (known after apply) + + default_security_list_id = (known after apply) + + defined_tags = (known after apply) + + display_name = "team_3" + + dns_label = "team3" + + freeform_tags = (known after apply) + + id = (known after apply) + + ipv6cidr_blocks = (known after apply) + + is_ipv6enabled = (known after apply) + + state = (known after apply) + + time_created = (known after apply) + + vcn_domain_name = (known after apply) + } + +Plan: 9 to add, 0 to change, 0 to destroy. + +Changes to Outputs: + + env_1_dev_cidr = "10.0.0.0/21" + + env_1_test_cidr = "10.0.8.0/21" + + env_2_dev_cidr = "192.168.10.0/25" + + env_2_test_cidr = "192.168.10.128/25" + + env_3_dev_cidr = "10.10.10.0/26" + + env_3_test_cidr = "10.10.10.64/26" + +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" +now. +$ +``` + +This looks *really* good! Talk about compressing the amount of Terraform code we have to write and maintain now!!! Wow - we've found a way to scale to many environments with just a few lines of code. It almost seems too good to be true. + +Let's apply it: + +```console +$ terraform apply + + + +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 + +module.envs["team_3"].oci_core_vcn.this: Creating... +module.envs["env_2"].oci_core_vcn.this: Creating... +module.envs["awesome_1"].oci_core_vcn.this: Creating... +module.envs["env_2"].oci_core_vcn.this: Creation complete after 1s [id=ocid1.vcn.oc1.phx.] +module.envs["env_2"].oci_core_subnet.dev: Creating... +module.envs["env_2"].oci_core_subnet.test: Creating... +module.envs["team_3"].oci_core_vcn.this: Creation complete after 1s [id=ocid1.vcn.oc1.phx.] +module.envs["team_3"].oci_core_subnet.test: Creating... +module.envs["team_3"].oci_core_subnet.dev: Creating... +module.envs["awesome_1"].oci_core_vcn.this: Creation complete after 1s [id=ocid1.vcn.oc1.phx.] +module.envs["awesome_1"].oci_core_subnet.test: Creating... +module.envs["awesome_1"].oci_core_subnet.dev: Creating... +module.envs["env_2"].oci_core_subnet.dev: Creation complete after 4s [id=ocid1.subnet.oc1.phx.] +module.envs["env_2"].oci_core_subnet.test: Creation complete after 4s [id=ocid1.subnet.oc1.phx.] +module.envs["awesome_1"].oci_core_subnet.test: Creation complete after 7s [id=ocid1.subnet.oc1.phx.] +module.envs["awesome_1"].oci_core_subnet.dev: Creation complete after 8s [id=ocid1.subnet.oc1.phx.] +module.envs["team_3"].oci_core_subnet.test: Still creating... [10s elapsed] +module.envs["team_3"].oci_core_subnet.dev: Still creating... [10s elapsed] +module.envs["team_3"].oci_core_subnet.test: Still creating... [20s elapsed] +module.envs["team_3"].oci_core_subnet.dev: Still creating... [20s elapsed] +module.envs["team_3"].oci_core_subnet.test: Still creating... [30s elapsed] +module.envs["team_3"].oci_core_subnet.dev: Still creating... [30s elapsed] +module.envs["team_3"].oci_core_subnet.test: Still creating... [40s elapsed] +module.envs["team_3"].oci_core_subnet.dev: Still creating... [40s elapsed] +module.envs["team_3"].oci_core_subnet.test: Creation complete after 45s [id=ocid1.subnet.oc1.phx.] +module.envs["team_3"].oci_core_subnet.dev: Creation complete after 45s [id=ocid1.subnet.oc1.phx.] + +Apply complete! Resources: 9 added, 0 changed, 0 destroyed. + +Outputs: + +env_1_dev_cidr = "10.0.0.0/21" +env_1_test_cidr = "10.0.8.0/21" +env_2_dev_cidr = "192.168.10.0/25" +env_2_test_cidr = "192.168.10.128/25" +env_3_dev_cidr = "10.10.10.0/26" +env_3_test_cidr = "10.10.10.64/26" +$ +``` + +Let's refactor the outputs a bit further, making `outputs.tf` look like the following: + +```terraform +output "env_cidrs" { + value = join("\n", concat( + [ + for k,v in local.envs: + join("\n", [ + "${k} dev CIDR: ${module.envs[k].subnet_dev_cidr}", + "${k} test CIDR: ${module.envs[k].subnet_test_cidr}" + ]) + ] + )) +} +``` + +Let's re-apply to see how this looks now: + +```console +$ terraform apply -auto-approve +module.envs["awesome_1"].oci_core_vcn.this: Refreshing state... [id=ocid1.vcn.oc1.phx.] +module.envs["env_2"].oci_core_vcn.this: Refreshing state... [id=ocid1.vcn.oc1.phx.] +module.envs["team_3"].oci_core_vcn.this: Refreshing state... [id=ocid1.vcn.oc1.phx.] +module.envs["awesome_1"].oci_core_subnet.test: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.envs["env_2"].oci_core_subnet.test: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.envs["awesome_1"].oci_core_subnet.dev: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.envs["env_2"].oci_core_subnet.dev: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.envs["team_3"].oci_core_subnet.dev: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.envs["team_3"].oci_core_subnet.test: Refreshing state... [id=ocid1.subnet.oc1.phx.] + +Changes to Outputs: + + env_cidrs = <<-EOT + awesome_1 dev CIDR: 10.0.0.0/21 + awesome_1 test CIDR: 10.0.8.0/21 + env_2 dev CIDR: 192.168.10.0/25 + env_2 test CIDR: 192.168.10.128/25 + team_3 dev CIDR: 10.10.10.0/26 + team_3 test CIDR: 10.10.10.64/26 + EOT + +You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure. + +Apply complete! Resources: 0 added, 0 changed, 0 destroyed. + +Outputs: + +env_1_dev_cidr = "10.0.0.0/21" +env_1_test_cidr = "10.0.8.0/21" +env_2_dev_cidr = "192.168.10.0/25" +env_2_test_cidr = "192.168.10.128/25" +env_3_dev_cidr = "10.10.10.0/26" +env_3_test_cidr = "10.10.10.64/26" +env_cidrs = <] +module.envs["awesome_1"].oci_core_vcn.this: Refreshing state... [id=ocid1.vcn.oc1.phx.] +module.envs["env_2"].oci_core_vcn.this: Refreshing state... [id=ocid1.vcn.oc1.phx.] +module.envs["env_2"].oci_core_subnet.test: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.envs["env_2"].oci_core_subnet.dev: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.envs["awesome_1"].oci_core_subnet.dev: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.envs["team_3"].oci_core_subnet.dev: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.envs["team_3"].oci_core_subnet.test: Refreshing state... [id=ocid1.subnet.oc1.phx.] +module.envs["awesome_1"].oci_core_subnet.test: Refreshing state... [id=ocid1.subnet.oc1.phx.] + +Changes to Outputs: + - env_1_dev_cidr = "10.0.0.0/21" -> null + - env_1_test_cidr = "10.0.8.0/21" -> null + - env_2_dev_cidr = "192.168.10.0/25" -> null + - env_2_test_cidr = "192.168.10.128/25" -> null + - env_3_dev_cidr = "10.10.10.0/26" -> null + - env_3_test_cidr = "10.10.10.64/26" -> null + +You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure. + +Apply complete! Resources: 0 added, 0 changed, 0 destroyed. + +Outputs: + +env_cidrs = < + +Plan: 3 to add, 0 to change, 0 to destroy. + +Changes to Outputs: + ~ env_cidrs = <<-EOT + awesome_1 dev CIDR: 10.0.0.0/21 + awesome_1 test CIDR: 10.0.8.0/21 + env_2 dev CIDR: 192.168.10.0/25 + env_2 test CIDR: 192.168.10.128/25 + + env_4 dev CIDR: 10.1.2.0/25 + + env_4 test CIDR: 10.1.2.128/25 + team_3 dev CIDR: 10.10.10.0/26 + team_3 test CIDR: 10.10.10.64/26 + EOT + +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 + +module.envs["env_4"].oci_core_vcn.this: Creating... +module.envs["env_4"].oci_core_vcn.this: Creation complete after 1s [id=ocid1.vcn.oc1.phx.] +module.envs["env_4"].oci_core_subnet.test: Creating... +module.envs["env_4"].oci_core_subnet.dev: Creating... +module.envs["env_4"].oci_core_subnet.test: Creation complete after 5s [id=ocid1.subnet.oc1.phx.] +module.envs["env_4"].oci_core_subnet.dev: Creation complete after 6s [id=ocid1.subnet.oc1.phx.] + +Apply complete! Resources: 3 added, 0 changed, 0 destroyed. + +Outputs: + +env_cidrs = <