OpenTofu


opentofu logo

Introduction

OpenTofu is open-source fork of Terraform from HashiCorp.

OpenTofu is an "Infrastructure-as-code" software tool. Users can use it to define and provide data center infrastructure using a declarative configuration language known as HashiCorp Configuration Language (HCL), or optionally JSON.

I first started to explore OpenTofu because I wanted to migrate some hosted VMs from the OpenTelekomCloud. Back then, I was using a custom script to deploy infrastructure to the cloud from description files written in YAML.

I found the approach of deploying cloud infrastructure from description files much more convenient to doing the same from the Web UI.

While the Web UI is very useful to explore the capabilities of a cloud service, for a more "production-like" infrastructure, I would consider that approach very poor. Deploying several VMs, would require multiple clicks, depending on good memory skills to make sure that deployment were consistent.

When I was contemplating migrating this VMs from OTC to Oracle Cloud I felt doing it via the Web UI very unapealing. And while there is an API available, I did not find the inclination to write a custom script.

Recently, speaking to some co-workers, I was reminded of Terraform. And decided to try that this time. Of course, at the time, HashiCorp had recently switched licenses, so switched to OpenTofu instead.

My expectation is that migrating between OpenTofu and Terraform and back should be a simple experience, and any learned skills in OpenTofu should translate directly into Terraform. In fact, except for the installation of OpenTofu, I have been following tutorials for Terraform instead. So far everything has been essentially the same.

Origins

In August 2023 Terraform (along with several of HashiCorp's products) switched from an Open Source license (MPL2.0) to non Open-Source Business Source License. This change prompted a group of users to fork the last available version of Terraform, v1.5.5, as OpenTofu.

Why use OpenTofu

OpenTofu is an "Infrastructure-as-code" software, this allows you to build, change, and manage your infrastructure in a safe, consistent, trackable, and repeatable way by defining resource configurations that you can version (in a version control system like GitHub), reuse, and share.

This yields to more reliable infrastructure, and at the same time allow you to quickly make/implement changes to it in response of changing requirements.

Using OpenTofu

Installation

The official installation instructions can be found in their Documentation Webpage.

Whenever possible using your native package manager (e.g. apt on Debian Linux, dnf on Fedora, etc) would be the preferred option. In my case, I am using Void Linux, so that option was not available to me.

Luckly, OpenTofu is implemented as a single binary executable (It is developed in go language), so simply downloading this from their release page and adding it somewhere in your executable path to be sufficient.

There even is a Windows version which can be installed in a similar way.

For your convenience they provide with an "installation" script, but being it a single binary, I felt that this was irrelevant.

Configuring for OCI

  1. Sign up for Oracle Cloud
  2. You can skip these steps if using an Administrator user. However, it is highly recommended to use a "non-Administrator" account when available.
    • Create Compartment:
      • Go to Identity, Compartments.
      • Click Create Compartment. Complete the form and create the compartment.
      • Wait a couple of minutes for the compartment to be create.
      • Note and record the compartment name and ID (which will be needed later)
    • Create Group:
      • Go to Identity, Domains.
      • Select the desired domain. Either create a new domain or use the default.
      • On the sidebar, click on Groups.
      • Click on Create group and complete the form. You can leave users empty and create the API user later.
    • Create a Policy:
      • Go to Identity, Policies.
      • Click Create policy. Enter Name and Description.
      • Note: The form can select a Compartment. I don't know how this works as I only tested on the root compartment.
      • Select Show manual editor and enter this as the policy:
        allow group <group-name> to read all-resources in tenancy
        allow group <group-name> to manage all-resources in compartment <compartment>
    • Create API user:
      • Go to Identity, Domains.
      • Select the desired domain. Either create a new domain or use the default.
      • On the sidebar, click on Users.
      • Click on Create user and complete the form. Add the user to the relevant group.
        This way the scope of access for a given user/group is limited to a compartment.
  3. Create RSA keys, this can be done either via command line or using the Web Console.
    • From the command line:
      • openssl genrsa -out <your-home-directory>/.oci/<your-rsa-key-name>.pem 2048
      • chmod 600 <your-home-directory>/.oci/<your-rsa-key-name>.pem
      • openssl rsa -pubout -in <your-home-directory>/.oci/<your-rsa-key-name>.pem -out $HOME/.oci/<your-rsa-key-name>_public.pem
      • Assign key to user account:
        • Go to Identity, Domains.
        • Select the desired domain. Either create a new domain or use the default.
        • On the sidebar, click on Users.
        • Select the API user.
        • On the left sidebar click on API Keys.
        • Click Add API Key. Select Paste Public Keys and paste the value of the contents of the public key file. Click Add
    • From the Web Console:
      • Go to Identity, Domains.
      • Select the desired domain. Either create a new domain or use the default.
      • On the sidebar, click on Users.
      • Select the API user.
      • On the left sidebar click on API Keys. Select Generate API Key pair.
      • Download Private key. You may download the public key, but that is strictly optional.
      • Click Add
    • After adding the key, save the sample config information. You will need it later.

Setting up API Key-Based authentication

Create a working directory to be used in our OpenTofu project.

  1. Create a file named provider.tf with the following contents:
    provider "oci" {
     tenancy_ocid = "<tenancy-ocid>"
     user_ocid = "<user-ocid"
     fingerprint = "<fingerprint>"
     region = var.region_name
     private_key_path = "<private-key-path>"
    }

    The tenancy_ocid, user_ocid and fingerprint would have come from the config settings in the last step of "Configuring OCI". The region name is used in more locations so we will record it as variable so we don't specify it here.
    The private-key-path is the path to the private RSA key from an earlier step. Use Linux/UNIX style "slash" (/) directory separators here even under MS-Windows.

  2. Create a vars.tf with contents:
    #
    # Define variables
    #
    variable "compartment_ocid" {
      type        = string
      description = "ID of the compartment in OCI to use"
      default     = "<compartment-ocid>"
    }
    variable "region_name" {
      type        = string
      description = "Region to deploy resources"
      default     = "<region>"
    }

    The compartment-ocid comes from the compartment that was created during "Configuring OCI".
    The region comes from the sample config from "Configuring OCI".

  3. Create a file availability-domains.tf with contents:
    data "oci_identity_availability_domains" "tutorial_ads" {
     compartment_id = var.compartment_ocid
    }

Declare networking

  1. Create a file vcn-module.tf with contents:

    module "vcn" {
      source  = "oracle-terraform-modules/vcn/oci"
      version = "3.1.0"
      #
      # Required Inputs
      compartment_id = var.compartment_ocid
      region = var.region_name
      internet_gateway_route_rules = null
      local_peering_gateways = null
      nat_gateway_route_rules = null
      #
      # Optional Inputs
      vcn_name = "<vcn-name>"
      vcn_dns_label = "<vcn-dns-label>"
      vcn_cidrs = ["10.0.0.0/16"]
      #
    
      create_internet_gateway = true
      create_nat_gateway = false
      create_service_gateway = false
    }
    

    NOTE: In this example, we are fixing the module version. Don't know if this is needed or not.
    Select a suitable name for virtual cloud's vcn-name.
    Similarly, select a suitable vcn-dns-label. However, in Oracle Free tiers there are no DNS zones, so this setting is useless.
    vcn_cidrs in this example is set to 10.0.0.0/16. Feel free to modify as needed.
    We are only setting create_internet_gateway to true. This allows VMs in the public subnets to be reachable from the Internet. This is the only gateway allowed in the Oracle Free tier.
    create_nat_gateway is set to false as this is not supported by the Oracle Free tier. This gateway allows VMs in the private subnets to communicate to the Internet. There is no in-bound access from the Internet to the private subnets.
    create_service_gateway is set to false as this is not supported by the Oracle Free tier. This gateway allows VMs to communicate to the Oracle service end-points without going over the Internet.

  2. create a file pub-seclst.tf with contents:

     resource "oci_core_security_list" "public-security-list" {
    
       # Required
       compartment_id = var.compartment_ocid
       vcn_id = module.vcn.vcn_id
    
       # Optional
       display_name = "security-list-for-public-subnet"
    
       ingress_security_rules { 
           stateless = false
           source = "0.0.0.0/0"
           source_type = "CIDR_BLOCK"
           # Get protocol numbers from https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml TCP is 6
           protocol = "6"
           tcp_options { 
               min = 22
               max = 22
           }
         }
       ingress_security_rules { 
           stateless = false
           source = "0.0.0.0/0"
           source_type = "CIDR_BLOCK"
           # Get protocol numbers from https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml ICMP is 1  
           protocol = "1"
    
           # For ICMP type and code see: https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml
           icmp_options {
             type = 3
             code = 4
           } 
         }   
    
       ingress_security_rules { 
           stateless = false
           source = "10.0.0.0/16"
           source_type = "CIDR_BLOCK"
           # Get protocol numbers from https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml ICMP is 1  
           protocol = "1"
    
           # For ICMP type and code see: https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml
           icmp_options {
             type = 3
           } 
         }
    
       egress_security_rules {
           stateless = false
           destination = "0.0.0.0/0"
           destination_type = "CIDR_BLOCK"
           protocol = "all" 
       }
    
     }

    This defines the security list protecting the public subnet.

  3. Create pub-subnet.tf with contents:

     resource "oci_core_subnet" "vcn-public-subnet"{
    
       # Required
       compartment_id = var.compartment_ocid
       vcn_id = module.vcn.vcn_id
       cidr_block = "10.0.255.0/24"
    
       # Optional
       route_table_id = module.vcn.ig_route_id
       security_list_ids = [oci_core_security_list.public-security-list.id]
       display_name = "public-subnet"
     }
    

    Change cidr_block, display_name as needed.

Declare compute instance

  1. Create login keys:
    ssh-keygen -t rsa -N "" -b 2048 -C <your-ssh-key-name> -f <your-ssh-key-name>

    The command generates some random text art used to generate the keys. When complete, you have two files:

    • The private key file: your-ssh-key-name
    • The public key file: your-ssh-key-name.pub
  2. Create cloud-init.yaml with contents:
    #cloud-config
    runcmd:
    - echo 'Hello world' >> /etc/motd

    This is a quick example to demonstrate that it works. See cloud-init for more examples.

  3. Create compute.tf with contents:

     resource "oci_core_instance" "<vm-name>" {
         # Required
         availability_domain = data.oci_identity_availability_domains.tutorial_ads.availability_domains[0].name
         compartment_id = var.compartment_ocid
         shape = "VM.Standard.A1.Flex"
         shape_config {
             memory_in_gbs = 2
             ocpus = 1
         }
         source_details {
             source_id = "ocid1.image.oc1.eu-amsterdam-1.aaaaaaaa7o2ilw6qsabd7qgnfjxncygvy442pzxkzcmsogkxeqhtwsgwlnwq"
             source_type = "image"
         }
         #~ shape = "VM.Standard.E2.1.Micro"
         #~ source_details {
             #~ source_id = "ocid1.image.oc1.eu-amsterdam-1.aaaaaaaa57ipifc7jj3m7nskxw66czipcrf4hpehsbx473uauvxot2im67dq"
             #~ source_type = "image"
         #~ }
    
         # Optional
         display_name = "<vm-name>"
         create_vnic_details {
             assign_public_ip = true
             subnet_id = oci_core_subnet.vcn-public-subnet.id
         }
         metadata = {
             ssh_authorized_keys = file("<you-sh-key-name>,pub")
             user_data = "${base64encode(file("cloud-init.yaml"))}"
    
         } 
         preserve_boot_volume = false
     }
    
    • availability_domain is configured from availability-domains.tf which retrieves the list of availability domains and selects the first one.
    • In this example, we are setting vm-name as resource-id and also as display_name. This is not necessary, but it makes things simpler this way. The resource-id is used internally by OpenTofu to refer created resources, while the display_name is shown in the Oracle web console.
    • source_id can be looked up from the documentation. Simply find the image you want, and locate the region to use, and get the ID from there.
    • shape and shape_config are used to configure the VM, in the Oracle Free tier, you can use: VM.Standard.A1.Flex or VM.Standard.E2.1.Micro.
      The Flex shape requires further configuration with shape_config which requires memory_in_gbs to configure memory and ocpus to configure CPU count.
    • metadata.ssh_authorized_keys : Configure authorized ssh keys. The default user for the Oracle Ubuntu images is ubuntu.
    • metadata.user_data : User data for initialization. This must be a base64 encoded script. NOTE that because it is base64 encoded under Microsoft Windows, scripts may not be recognized properly as including a file (as in the example) will contain Windows style line terminations which will make the #cloud-config test fail. The following cloud-init formats are supported:
      • #cloud-config Cloud Config Data
      • #! User-Data Script (e.g. #!/bin/bash)
      • #include Include file
      • #cloud-boothook Cloud boothook

Run Scripts

  1. Initialize
    tf init

    This initializes a working directory. Specifically it would download from the Terraform repository any providers and/or modules.

  2. Create an execution plan
    tf plan

    use this to preview what will OpenTofu will eventually execute.

  3. Run your terraform scripts:
    tf apply

    This will execute changes to the infrastructure.

Destroying Infrastructure

Run:

 tf destroy

This will destroy any created resources.

The given example creates a ARM server. At the time of this writing, the Oracle Free tier does not have any Arm servers available. Running these scripts will generate error:

Error: 500-InternalError, Out of host capacity.

Unfortunately, there is not much that can be done here. You try running things later hopping that some capacity may be freed-up.