Using Terraform on Oracle Cloud Infrastructure (OCI)
I was just coding with Terraform to manage and configure an OCI compartment. This took me almost fifteen years ago.... Yes, I am talking about prehistoric times for some. Back then, bash (shell) scripting was a true ally for me. I was able to code most of the operational/daily tasks. That was quite helpful to avoid human errors. In addition to that, it was helping me to have a standard architecture. 'Infrastructure as Code' or 'DevOps' are pretty new terms and during these times, we didn't have the right toolkit. Therefore, all IT personnel was in search of holy grail for automization. We were creating customized solutions or using some automization tools. There was no way to predict the instruments we have today.
I truly believe the power of knowlege as Francis Bacon said. But if you can combine the knowledge with programming, that'd definitely open a door to infinite possibilities.
This is why I'd like to share a few examples about Terraform and its integration with Oracle Cloud. Maybe in another article, I can write a step-by-step installation document. But for the moment, I will not go into the details about the installation of oci cli and Terraform. I'll cut to the chase and show how you can create an infrastructure as follows:
In the architecture, I have a virtual cloud network (VCN) with 3 different Subnets. In each subnet, we have an Internet Gateway. In addition to that, we have a Linux Virtual Machine in every subnet. I can add more items such as Security Lists (or Network Security Groups), File Storage Systems, Object Storages, etc... But for keeping it simple, I'm not going into those additional components.
The example code will create a compartment, then create a VCN and 3 different subnets, called Subnet-A, Subnet-B and Subnet-C. After that, 3 different virtual machines will be provisioned. But before we begin, I'd like to underline a few things: My goal is to give an idea about Terraform and OCI (Oracle Cloud Infrastructure) integration and also how you can use terraform modules. Please try to learn the code, do not just copy and paste. And depending on your needs, customize as you wish. In addition to that, even though the code works well, it might not be at production level. You should take enough time to test and be sure of that, it works for you, too. Simply put, use at your own risk :)
Now it's play time! Create two folders to keep your Terraform files. TerraSample will be your root folder for the project. Subfolder, which is create-vms-with-subnet will be a 'module' for the project. We use modules for repeatitive operations. (There's a similarity between functions and modules.)
$ mkdir -p /mnt/TerraSample/create-vms-with-subnet
After that, we create two (plain text) files inside TerraSample directory. 'main.tf' will be the main entry point for your code.
# main.tf FILE # Variables variable compartment_name { type = string } variable compartment_desc { type = string } variable root_compartment_id { type = string } variable img_source_id { default = "ocid1.image.oc1.eu-frankfurt-1.aaaaaa...." } variable availability_domain { default = "cAmo:EU-FRANKFURT-1-AD-3" } variable region { default = "eu-frankfurt-1" } variable fault_domain { default = "FAULT-DOMAIN-1" } # Resources # CREATE A NEW COMPARTMENT resource "oci_identity_compartment" "tf_compartment" { # Required compartment_id = var.root_compartment_id description = var.compartment_desc name = var.compartment_name } # EACH CALL WILL CREATE A NEW SUBNET AND A NEW VM module "create-vms-with-subnetA" { source = "./create-vms-with-subnet" compartment_id = oci_identity_compartment.tf_compartment.id vcn_cidr_block = "160.16.0.0/16" vcn_display_name = "Demo-VCN-A" vcn_dns_label = "demovcna" subnet_cidr_block = "160.16.1.0/24" subnet_display_name = "VCN-A-Subnet-01" subnet_dns_label = "vcnasubnet01" img_source_id = "ocid1.image.oc1.eu-frankfurt-1.aaaaaa...." availability_domain = "cAmo:EU-FRANKFURT-1-AD-3" fault_domain = "FAULT-DOMAIN-1" vm_hostname = "testvm99" igw_display_name = "VCN-A-igw01" } module "create-vms-with-subnetB" { source = "./create-vms-with-subnet" compartment_id = oci_identity_compartment.tf_compartment.id vcn_cidr_block = "10.16.0.0/16" vcn_display_name = "Demo-VCN-B" vcn_dns_label = "demovcnb" subnet_cidr_block = "10.16.1.0/24" subnet_display_name = "VCN-B-Subnet-01" subnet_dns_label = "vcnbsubnet01" img_source_id = "ocid1.image.oc1.eu-frankfurt-1.aaaaaa...." availability_domain = "cAmo:EU-FRANKFURT-1-AD-1" fault_domain = "FAULT-DOMAIN-2" vm_hostname = "testvm90" igw_display_name = "VCN-B-igw01" } module "create-vms-with-subnetC" { source = "./create-vms-with-subnet" compartment_id = oci_identity_compartment.tf_compartment.id vcn_cidr_block = "30.16.0.0/16" vcn_display_name = "Demo-VCN-C" vcn_dns_label = "demovcnb" subnet_cidr_block = "30.16.1.0/24" subnet_display_name = "VCN-C-Subnet-01" subnet_dns_label = "vcncsubnet01" img_source_id = "ocid1.image.oc1.eu-frankfurt-1.aaaaaa...." availability_domain = "cAmo:EU-FRANKFURT-1-AD-1" fault_domain = "FAULT-DOMAIN-2" vm_hostname = "testvm90" igw_display_name = "VCN-C-igw01" }
Then create another file for your variables. In our example, its name is oci_compartment_variables.auto.tfvars. You don't need to use variables, basically your terraform code could be static. But in order to have an efficient code, we need to provide re-usability. This is why I strongly recommend to use variables and also modules. Here's the content of oci_compartment_variables.auto.tfvars:
# oci_compartment_variables.auto.tfvars FILE compartment_name = "DummyComp" compartment_desc = "Dummy Comp by CCEBI" root_compartment_id = "ocid1.compartment.oc1..aaaaaaaa7b..."
Now let's go into the subfolder. Under create-vms-with-subnet directory, create another file called main.tf:
# main.tf FILE UNDER create-vms-with-subnet FOLDER # Variables variable compartment_id { } variable vcn_cidr_block { } variable vcn_display_name { } variable vcn_dns_label { } variable subnet_cidr_block { } variable subnet_display_name { } variable subnet_dns_label { } variable igw_display_name { } variable img_source_id { } variable vm_hostname { } variable availability_domain { } variable fault_domain { } # Resources # CREATE VCN resource "oci_core_vcn" "tf_vcn" { #cidr_block = <<Optional value not found in discovery>> cidr_blocks = [ var.vcn_cidr_block, ] # USE THE NEW COMPARTMENT ID compartment_id = var.compartment_id defined_tags = { } display_name = var.vcn_display_name dns_label = var.vcn_dns_label freeform_tags = { } #is_ipv6enabled = <<Optional value not found in discovery>> } # CREATE SUBNET resource oci_core_subnet "tf_subnet" { #availability_domain = <<Optional value not found in discovery>> cidr_block = var.subnet_cidr_block compartment_id = var.compartment_id defined_tags = { } dhcp_options_id = oci_core_vcn.tf_vcn.default_dhcp_options_id display_name = var.subnet_display_name dns_label = var.subnet_dns_label freeform_tags = { } #ipv6cidr_block = <<Optional value not found in discovery>> prohibit_internet_ingress = "false" prohibit_public_ip_on_vnic = "false" route_table_id = oci_core_vcn.tf_vcn.default_route_table_id security_list_ids = [ oci_core_vcn.tf_vcn.default_security_list_id, ] vcn_id = oci_core_vcn.tf_vcn.id } # CREATE INTERNET GATEWAY resource oci_core_internet_gateway "tf_igw" { compartment_id = var.compartment_id defined_tags = { } display_name = var.igw_display_name enabled = "true" freeform_tags = { } vcn_id = oci_core_vcn.tf_vcn.id } # DEFINE A ROUTE TABLE resource oci_core_default_route_table "tf_route_table" { defined_tags = { } display_name = "Default Route Table for ${var.vcn_display_name}" freeform_tags = { } manage_default_resource_id = oci_core_vcn.tf_vcn.default_route_table_id route_rules { #description = <<Optional value not found in discovery>> destination = "0.0.0.0/0" destination_type = "CIDR_BLOCK" network_entity_id = oci_core_internet_gateway.tf_igw.id } } # CREATE A VM INSTANCE resource oci_core_instance "tf_compute" { agent_config { are_all_plugins_disabled = "false" is_management_disabled = "false" is_monitoring_disabled = "false" plugins_config { desired_state = "DISABLED" name = "Vulnerability Scanning" } plugins_config { desired_state = "ENABLED" name = "OS Management Service Agent" } plugins_config { desired_state = "ENABLED" name = "Custom Logs Monitoring" } plugins_config { desired_state = "ENABLED" name = "Compute Instance Run Command" } plugins_config { desired_state = "ENABLED" name = "Compute Instance Monitoring" } } availability_config { #is_live_migration_preferred = <<Optional value not found in discovery>> recovery_action = "RESTORE_INSTANCE" } availability_domain = var.availability_domain #capacity_reservation_id = <<Optional value not found in discovery>> compartment_id = var.compartment_id create_vnic_details { #assign_private_dns_record = <<Optional value not found in discovery>> assign_public_ip = "true" defined_tags = { } display_name = var.vm_hostname freeform_tags = { } hostname_label = var.vm_hostname nsg_ids = [ ] skip_source_dest_check = "false" subnet_id = oci_core_subnet.tf_subnet.id #vlan_id = <<Optional value not found in discovery>> } #dedicated_vm_host_id = <<Optional value not found in discovery>> defined_tags = { } display_name = var.vm_hostname extended_metadata = { } fault_domain = var.fault_domain freeform_tags = { } instance_options { are_legacy_imds_endpoints_disabled = "false" } #ipxe_script = <<Optional value not found in discovery>> #is_pv_encryption_in_transit_enabled = <<Optional value not found in discovery>> launch_options { boot_volume_type = "PARAVIRTUALIZED" firmware = "UEFI_64" is_consistent_volume_naming_enabled = "true" #is_pv_encryption_in_transit_enabled = "false" network_type = "VFIO" remote_data_volume_type = "PARAVIRTUALIZED" } metadata = { "ssh_authorized_keys" = "<< USE_YOUR_OWN_SSH_KEY >>" } #preserve_boot_volume = <<Optional value not found in discovery>> shape = "VM.Standard.E3.Flex" shape_config { baseline_ocpu_utilization = "" memory_in_gbs = "16" ocpus = "1" } source_details { #boot_volume_size_in_gbs = <<Optional value not found in discovery>> #kms_key_id = <<Optional value not found in discovery>> source_id = var.img_source_id source_type = "image" } state = "RUNNING" }
The final folder/file structure should look like this:
$ tree TerraSample/ TerraSample/ |-- create-vms-with-subnet | `-- main.tf |-- main.tf `-- oci_compartment_variables.auto.tfvars
We are ready to go. Terraform has three easy steps. First, we use 'terraform init' command to prepare working directory for other commands. Then we ask Terraform to show changes required by the current configuration. For this, we use 'terraform plan' command. And finaly, if everything is OK, then we ask Terraform to create or update infrastructure. (By the way, if you create a new terraform file and want to validate it, you can use 'terraform validate'. This is optional.)
Go to root folder (TerraSample) and prepare the environment for Terraform:
$ cd /mnt/TerraSample/ $ terraform init
Run plan command to see steps:
$ 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: # oci_identity_compartment.tf_compartment will be created + resource "oci_identity_compartment" "tf_compartment" { + compartment_id = "ocid1.compartment.oc1..aaaaaaaa7by..." + defined_tags = (known after apply) + description = "Dummy Comp by CCEBI" + freeform_tags = (known after apply) + id = (known after apply) + inactive_state = (known after apply) + is_accessible = (known after apply) + name = "DummyComp" + state = (known after apply) + time_created = (known after apply) } ... + defined_tags = (known after apply) + display_name = "Demo-VCN-C" + dns_label = "demovcnb" + 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: 16 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.
If you believe, you're good to go, then the real magic kicks in. With just a simple command, we will create several components. Our Abracadabra is 'terraform apply'
$ terraform apply 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: # oci_identity_compartment.tf_compartment will be created + resource "oci_identity_compartment" "tf_compartment" { + compartment_id = "ocid1.compartment.oc1..aaaaaaaa7byjsmcipnzqdd3cbhgu2qin56g2xs4gxknctpnfrg37ldfjumhq" + defined_tags = (known after apply) + description = "Dummy Comp by CCEBI" + freeform_tags = (known after apply) + id = (known after apply) + inactive_state = (known after apply) + is_accessible = (known after apply) + name = "DummyComp" + state = (known after apply) + time_created = (known after apply) } ... module.create-vms-with-subnetA.oci_core_instance.tf_compute: Still creating... [20s elapsed] module.create-vms-with-subnetB.oci_core_instance.tf_compute: Still creating... [20s elapsed] module.create-vms-with-subnetC.oci_core_instance.tf_compute: Still creating... [20s elapsed] module.create-vms-with-subnetA.oci_core_instance.tf_compute: Still creating... [30s elapsed] module.create-vms-with-subnetB.oci_core_instance.tf_compute: Still creating... [30s elapsed] module.create-vms-with-subnetC.oci_core_instance.tf_compute: Still creating... [30s elapsed] module.create-vms-with-subnetB.oci_core_instance.tf_compute: Creation complete after 36s [id=ocid1.instance.oc1.eu-frankfurt-1.antheljtzf3wq...] module.create-vms-with-subnetA.oci_core_instance.tf_compute: Still creating... [40s elapsed] module.create-vms-with-subnetC.oci_core_instance.tf_compute: Still creating... [40s elapsed] module.create-vms-with-subnetA.oci_core_instance.tf_compute: Creation complete after 47s [id=ocid1.instance.oc1.eu-frankfurt-1.antheljszf3wq...] module.create-vms-with-subnetC.oci_core_instance.tf_compute: Still creating... [50s elapsed] module.create-vms-with-subnetC.oci_core_instance.tf_compute: Still creating... [1m0s elapsed] module.create-vms-with-subnetC.oci_core_instance.tf_compute: Creation complete after 1m1s [id=ocid1.instance.oc1.eu-frankfurt-1.antheljtzf3wq...] Apply complete! Resources: 16 added, 0 changed, 0 destroyed.
Perfect! Now we have 3 VMs, residing in 3 different subnets. Those subnets are under a new VCN, which is created under a new compartment. If that doesn't amaze you, I'd like to share another feature of Terraform. Terraform can compare your code with the real world and depending on those differences, it can easily implement. Let's say you'd like to have another subnet to host another VM. You don't have to re-write everything from scratch. If you want a new one, just add this to your main.tf file (under TerraSample):
# ADD BELOW LINES TO THE main.cf UNDER TerraForm (main) FOLDER ... module "create-vms-with-subnetD" { source = "./create-vms-with-subnet" compartment_id = oci_identity_compartment.tf_compartment.id vcn_cidr_block = "40.16.0.0/16" vcn_display_name = "Demo-VCN-D" vcn_dns_label = "demovdnb" subnet_cidr_block = "40.16.1.0/24" subnet_display_name = "VCN-D-Subnet-01" subnet_dns_label = "vcndsubnet01" img_source_id = "ocid1.image.oc1.eu-frankfurt-1.aaaaaaaa4pnql6wpr..." availability_domain = "cAmo:EU-FRANKFURT-1-AD-1" fault_domain = "FAULT-DOMAIN-2" vm_hostname = "testvm60" igw_display_name = "VCN-D-igw01" }
This time, when you use 'terraform plan', you'll see what actions will be carried out. These are incremental changes.
$ terraform plan oci_identity_compartment.tf_compartment: Refreshing state... [id=ocid1.compartment.oc1..aaaaaaaazbvvu7cw...] module.create-vms-with-subnetC.oci_core_vcn.tf_vcn: Refreshing state... [id=ocid1.vcn.oc1.eu-frankfurt-1.amaaaaaazf3wqhia33dt...] module.create-vms-with-subnetA.oci_core_vcn.tf_vcn: Refreshing state... [id=ocid1.vcn.oc1.eu-frankfurt-1.amaaaaaazf3wqhiajsd2...] module.create-vms-with-subnetB.oci_core_vcn.tf_vcn: Refreshing state... [id=ocid1.vcn.oc1.eu-frankfurt-1.amaaaaaazf3wqhiafbxh...] ... # module.create-vms-with-subnetD.oci_core_vcn.tf_vcn will be created + resource "oci_core_vcn" "tf_vcn" { + cidr_block = (known after apply) + cidr_blocks = [ + "40.16.0.0/16", ] + compartment_id = "ocid1.compartment.oc1..aaaaaaaazbvvu7cwh..." + 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 = "Demo-VCN-D" + dns_label = "demovdnb" + 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) }
You see? Only new added lines will have an effect on your Cloud Infrastructure.
$ 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 ... module.create-vms-with-subnetD.oci_core_instance.tf_compute: Still creating... [20s elapsed] module.create-vms-with-subnetD.oci_core_instance.tf_compute: Still creating... [30s elapsed] module.create-vms-with-subnetD.oci_core_instance.tf_compute: Creation complete after 37s [id=ocid1.instance.oc1.eu-frankfurt-1.antheljtzf3wqhicdkffgs...] Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Did you change your idea and want to get rid of the 4th Subnet? That's easy... just delete the recently added lines.
$ terraform plan ... # module.create-vms-with-subnetD.oci_core_vcn.tf_vcn will be destroyed - resource "oci_core_vcn" "tf_vcn" { - cidr_block = "40.16.0.0/16" -> null - cidr_blocks = [ - "40.16.0.0/16", ] -> null - compartment_id = "ocid1.compartment.oc1..aaaaaaaazbvvu7cwheeozzc5kccsksni7..." -> null ... $ terraform apply ... module.create-vms-with-subnetD.oci_core_instance.tf_compute: Still destroying... [id=ocid1.instance.oc1.eu-frankfurt-1.anthe...2prefmiuhjmzxyoyu3...., 1m30s elapsed] module.create-vms-with-subnetD.oci_core_instance.tf_compute: Destruction complete after 1m36s module.create-vms-with-subnetD.oci_core_subnet.tf_subnet: Destroying... [id=ocid1.subnet.oc1.eu-frankfurt-1.aaaaaaaaijh6rxjrcuhtje6dl6ehbt7h.....] module.create-vms-with-subnetD.oci_core_subnet.tf_subnet: Destruction complete after 1s module.create-vms-with-subnetD.oci_core_vcn.tf_vcn: Destroying... [id=ocid1.vcn.oc1.eu-frankfurt-1.amaaaaaazf3wqhiajad7tgyrhfux...] module.create-vms-with-subnetD.oci_core_vcn.tf_vcn: Destruction complete after 1s Apply complete! Resources: 0 added, 0 changed, 5 destroyed.
I kept the most powerful command to the end. Let's say, you developed a Terraform code and used it for production. But then you decided to use the same architecture for creating Test/DEV and QA environments. (By the way, that's a very reasonable request. Replicating the same architecture will help you to have identitcal test platforms.)
Thanks to Terraform, we can use the same blueprint (code), and change variables. So, new variables can shape your Test/Dev platforms. But at a moment, if there's a need to destroy everything, Terraform can also do this with 'terraform destroy' command. This will destroy everything depending on your input file.
$ terraform destroy oci_identity_compartment.tf_compartment: Refreshing state... [id=ocid1.compartment.oc1..aaaaaaaazbvvu7cwheeozzc5kccsksni7x5...] module.create-vms-with-subnetB.oci_core_vcn.tf_vcn: Refreshing state... [id=ocid1.vcn.oc1.eu-frankfurt-1.amaaaaaazf3wqhiafbxh5y5et...] ... # module.create-vms-with-subnetB.oci_core_instance.tf_compute will be destroyed - resource "oci_core_instance" "tf_compute" { - availability_domain = "cAmo:EU-FRANKFURT-1-AD-1" -> null - boot_volume_id = "ocid1.bootvolume.oc1.eu-frankfurt-1.abtheljt4vy3axsgqwfzteoupom3xbl..." -> null - compartment_id = "ocid1.compartment.oc1..aaaaaaaazbvvu7cwheeozzc5kccsksni7x5vty..." -> null - defined_tags = {} -> null - display_name = "testvm90" -> null ... Plan: 0 to add, 0 to change, 16 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 ... module.create-vms-with-subnetA.oci_core_subnet.tf_subnet: Destruction complete after 1s module.create-vms-with-subnetA.oci_core_vcn.tf_vcn: Destroying... [id=ocid1.vcn.oc1.eu-frankfurt-1.amaaaaaazf3wqhiajsd2hv7...] module.create-vms-with-subnetB.oci_core_vcn.tf_vcn: Destruction complete after 1s module.create-vms-with-subnetA.oci_core_vcn.tf_vcn: Destruction complete after 1s oci_identity_compartment.tf_compartment: Destroying... [id=ocid1.compartment.oc1..aaaaaaaazbvvu7cwheeozzc...] oci_identity_compartment.tf_compartment: Destruction complete after 0s Destroy complete! Resources: 16 destroyed.
Is that it? Of course not. What I shared is a simple fraction of Terraform. When you go into more details, you start to see the beauty of it. Cloud is not just about optimizing infrastructure costs. It gives you a great potential to get jobs done, in timely manner and standard approach. Terraform is just a tool inside our toolbox. You will have many more tools within Oracle Cloud.
If you like the idea of using Terraform, and looking for a way to practice it, you can use Oracle's free tier services. Just visit Oracle cloud website and get familiar with the environment: Oracle Cloud Free Tier
P.S. - 1: Most types of Oracle Cloud Infrastructure resources have an Oracle-assigned unique ID called an Oracle Cloud Identifier (OCID). In order to avoid mistakes, I deliberately put only a portion of my OCIDs. Because you need to replace them with your OCIDs.
P.S. - 2: I'm a command line guy. If you want to do the same things in GUI, Oracle Cloud provides you a service called Stacks under Resource Manager. You can also take a look at that.
Master Principal Solution Architect - Oracle UAE
3 年Thanks Cagatay. Terraform and Resource Manager are very usable technologies for OCI infrastructure coding.
Cloud Compute Leader - EMEA | CISSP
3 年Thanks Cagatay!
Help organizations modernize their applications, platform and integrations with automation while considering finops practices for the hybrid cloud.
3 年OCI rocks with Terraform, Thanks Cagatay very useful blog ??