OpenTofu
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
- Sign up for Oracle Cloud
- 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.
- Create Compartment:
- 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.
- From the command line:
Setting up API Key-Based authentication
Create a working directory to be used in our OpenTofu project.
- 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
andfingerprint
would have come from theconfig
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. - 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 sampleconfig
from "Configuring OCI". - Create a file
availability-domains.tf
with contents:data "oci_identity_availability_domains" "tutorial_ads" { compartment_id = var.compartment_ocid }
Declare networking
-
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 to10.0.0.0/16
. Feel free to modify as needed.
We are only settingcreate_internet_gateway
totrue
. 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 tofalse
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 tofalse
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. -
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.
-
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
- 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
- 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.
-
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 fromavailability-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 thedisplay_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
andshape_config
are used to configure the VM, in the Oracle Free tier, you can use:VM.Standard.A1.Flex
orVM.Standard.E2.1.Micro
.
TheFlex
shape requires further configuration withshape_config
which requiresmemory_in_gbs
to configure memory andocpus
to configure CPU count.metadata.ssh_authorized_keys
: Configure authorized ssh keys. The default user for the Oracle Ubuntu images isubuntu
.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
- Initialize
tf init
This initializes a working directory. Specifically it would download from the Terraform repository any providers and/or modules.
- Create an execution plan
tf plan
use this to preview what will OpenTofu will eventually execute.
- 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.