Deploy OCI Network and computer instances using terraform

In the modern cloud-driven world, managing and provisioning infrastructure can be a time-consuming task. The Terraform, an Infrastructure as Code (IaC) tool that simplifies defining, provisioning, and managing your Oracle Cloud Infrastructure (OCI). With Terraform, you can automate resource setup, minimize manual errors, and easily recreate or scale your infrastructure as needed.

Recently, I had the opportunities to discuss OCI’s IaC capabilities with the customers. They wanted to quickly test OCI services and delete resources after the test using code rather than manually interacting with the GUI console. Using a free-tier OCI tenancy, I showcased how simple it is to automate infrastructure provisioning using Terraform.

On this blogpost I have covered up, development environment set up, Installed terraform, generated SSH keys that will be used in OCI API access and created required terraform files for the automation.

Section 1: Development Environment Setup

  • On the Windows laptop, enable the Windows Subsystem for Linux (WSL) and install Oracle Linux 9.2.

  • Install Visual Studio Code studio for the code editor and  integrate it with Oracle Linux to create a robust development environment.

Section 2: Install Terraform  generate ssh keys and add the key on OCI console 

  • After setting up Oracle Linux,  install Terraform on the Oracle Linux OS.
  • Generate SSH keys for API and compute access to securely manage resources.
  • Add the public key to OCI user account using OCI console 

Section 3: Create and Deploy and Remove Resources using terraform

  • Create necessary .tf (Terraform) script files to automate the deployment process. And initialize, plan and apply to for the resource creation.  

Final Architecture

Using terraform – it creates, VCN network, public subnet, private subnet, Internet gateway and Linux and windows compute Instance as following diagram

Prerequisites

  • Access to an OCI tenancy, A free-tier tenancy works perfectly for testing and development purposes.
  • Appropriate privileges to access OCI services and resources.

Section 1: Development Environment Setup

Go to Control panel and click on Program and Features.

It will open Turn Windows Feature on or of console and select Virtual Machine Platform and Windows subsystem for Linux and click on OK. To apply the change, it will prompt to reboot the system and reboot to continue

Go to Microsoft Store on your windows machine and choose App icon on the left, choose Oracle Linux and click on Install.

After installation completes click on Open, and supply username and password.

To install Visual code studio- go to Microsoft Store on you windows machine, click on App, Search Visual code studio and click on Install. (You can download installer directly from https://code.visualstudio.com/download and install it)

After the Visual code installation, open it and click on Extension on left panel, choose WSL and install the extension.

Press F1 type WSL and choose Connect to WSL and it will connect the Oracle Linux machine that you install on windows subsystem earlier.

Once it connects, click on Terminal tab you will get access on Oracle Linux

Section 2: Install Terraform  generate ssh keys and add the key on OCI console 

To install Terraform with yum command, you need to add the official HashiCorpLinux repository. Before adding repository install yum-utils package.

sudo yum install -y yum-utils

Next, add the official HashiCorpLinux repository to yum-config-manager. Use yum command and install the terraform.

sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo

sudo yum -y install terraform

Verify the terraform has been installed

terraform –version

terraform –help

To crate and run the terraform code, create project-terraform under the $HOME directory

mkdir $HOME/project-terrraform

Now, from your visual code studio click on File and select Open folder.

choose ‘project-terraform’ folder that you created earlier and click on OK.

Verify that your terminal lands on right directory

Although it is possible to generate API signing key pairs using the console, but in my case, I have created ssh key pairs using openssl and I am storing it under the $HOME/.oci directory. Create .oci folder under the home

mkdir $HOME/.oci

Generate 2048 bit private key using openssl genrsa command.

openssl genrsa -out $HOME/.oci/<rsa-private-key-name>.pem 2048

Generate public key based on the private key that you generated that you generated on previous step

openssl rsa -pubout -in $HOME/.oci/<rsa-public-key-name>.pem -out $HOME/.oci/<rsa-key-name>.pem

We also need .pub format key for the compute access. Use ssh-keygen command and generate 2028 bit .pub format key. I am storing it under the same ($HOME/.oci ) directory

ssh-keygen -b 2048 -t rsa -f <rsa-key-name>

Open the .pem public key file and copy it that we add it on OCI user account that is used for the API access

cat $HOME/.oci/<rsa-key-name>.pem

Once the API signing key pair generation is complete, add the generated public key to the OCI user account, this task will be performed in the OCI GUI console

Log in to the OCI Console and click user profile icon and select My profile.

Go to under the Resources and select API Keys and click on API Keys and Paste a public key that we generated earlier. Click on Add

Copy details provided on configuration file preview and save it somewhere. This will be used in the terraform variables file on later steps.

Configuration file preview, should be like this:

[DEFAULT]

user=ocid1.user.oc1. aaaaaaaaxxxxxxx

fingerprint=66:5f:4a:xx:xx:xx:xx:xx:xx

tenancy=ocid1.tenancy.oc1..aaaaaaaaxxxxxxx

region=ap-osaka-1

key_file=<path to your private keyfile> # TODO

Section 3: Create and Deploy and Remove the Resources

Open visual code studio and under the /home/admin/project-terraform directory and create the following terraform files:

terrafrom.tfvars
veriables.tf
provider.tf
availability_domains.tf
networking.tf
compute_linux.tf
compute_windows.tf
output.tf

Create terraform.tfvar file – define default values for variables in your configuration. It allows you to specify variables without hardcoding them into the configuration. Modify IAM and compartment details that you collected from your environment earlier


########################################################################################################
# OCI Identity and access parameters
########################################################################################################
user_id =   "ocid1.user.oc1..aaaaaaaagp3xxxxxxx"  # Change is your OCI user id   
api_fingerprint =   "66:5f:4a:b6:fa:41:xx:xx:xx:xxx " # Change it with your api signature  
tenancy_id  =   "ocid1.tenancy.oc1..aaaaaaaagp3xxxxxxx" # Change it with your Oci Tenant ID
api_private_key_path    =   "/home/admin/.oci/oci-rsa-private_key.pem"  
region  =   "ap-osaka-1" 

########################################################################################################
# Compartment Id where your resouces will be created 
########################################################################################################
compartment_id  = "ocid1.tenancy.oc1..aaaaaaaagp3xxxxxxx" # Change it with your compartment id where you want to create OCI resouces 

########################################################################################################
# VCN specific variables 
########################################################################################################
create_new_vcn  = true # Set this to true if you want terraform to crearte the network for you, otherwise set it to false.
vcn_cidr_block  = "10.10.0.0/16"   # The list of IPv4 CIDR blocks the VCN will use
vcn_display_name    = "VCN01"      # provide the name that will be displayed in the OCI console
vcn_dns_label   = "vcn01"          # provide a descriptive alphanumeric name for the DNS

#Configure CIDR Blocks, Subnet(Public, Private) OCIDS for an existing VCN
vcn_id  = "REPLACE_BY_YOUR_VCN_OCID"                            #ADD WITH YOUR VCN OCID
private_subnet_id   = "REPLACE_BY_YOUR__PRIVATE_SUBNET_OCID"    #ADD WITH YOUR PRIVATE SUBNET
public_subnet_id = "REPLACE_BY_YOUR_PUBLIC_SUBNET__OCID"        #AA WITH YOUR PUBLIC SUBNET

#Private subnet variables
private_subnet_cidr_block = "10.10.10.0/24"           # OCI private subnet CIDR block range
private_subnet_display_name = "private_subnet_demo"   # provide a descriptive name for the private subnet, that will be displayed in the OCI console
private_subnet_prohibit_public_ip_on_vnic   = false  # Allow public IP address to the VNIC

#Public subnet variables
public_subnet_cidr_block    = "10.10.20.0/24"       # OCI public subnet CIDR block range
public_subnet_display_name  = "public_subnet_demo"  # provide a descriptive name for the public subnets, that will be displayed in the OCI console
public_subnet_prohibit_public_ip_on_vnic    = false # Allow public IP address to the VNIC

########################################################################################################
#Compute variables - Make sure to select a compatible shape VM.Standard.E5.Flex)
########################################################################################################
instance_shape = "VM.Standard.E5.Flex"       # Shape of the compute instance 
instance_flex_memory_in_gbs = 8           #The total amount of memory available to the instance, in gigabytes (you can modify memory size)
instance_flex_ocpus = 1                      #The total number of OCPUs available to the instance. (you can modify OCPU)
instance_create_vnic_details_assign_public_ip = true # To allow compute connectivity from internet
instance_display_name   = "compute_demo" # provide a descriptive name for the compute instance - this is what you will see displayed in the OCI console
public_ssh_key  = "/home/admin/.oci/oci-rsa-compute-key.pub" # Add your public ssh key
private_ssh_key = "home/admin/.oci/oci-rsa-compute-key"     # Add your private ssh key
create_linux_instance = true # if set to true a test linux instance will be created and false no linux instance will be deployed.
create_windows_instance = true # # If set to true a test windows instance will be created and false no windows instance will be deployed.
linux_image_ocid = "ocid1.image.oc1.ap-osaka-1.aaaaaaaafqowh456grk3td6tsd4reg2vyqjvfhqyjx3kyaece4znxj2ydbqq"   # OCID for chosen image (Oracle Linux 9 example) specific to the test region  (in my case ap-osaka-1))
windows_image_ocid = "ocid1.image.oc1.ap-osaka-1.aaaaaaaargxtf5sxr5r3ifegnyjy4s6gsxtqemtwjff4uyes4bmofv2jlwoa"  # OCID for chosen image (Windows example) specific to each region (in my case ap-osaka-1))

#Osaka Region 
# Oracle linux image_id = ocid1.image.oc1.ap-osaka-1.aaaaaaaafqowh456grk3td6tsd4reg2vyqjvfhqyjx3kyaece4znxj2ydbqq
# Windows image_id = ocid1.image.oc1.ap-osaka-1.aaaaaaaargxtf5sxr5r3ifegnyjy4s6gsxtqemtwjff4uyes4bmofv2jlwoa
# for other image OCIDs: https://docs.oracle.com/en-us/iaas/images/

Create provider.tf file: provider is a plugin that helps terraform communicate with cloud services

##Provider
terraform {
  required_providers {
    oci = {
      source = "oracle/oci"
      version = "6.21.0"
    }
  }
}

provider "oci" {
  tenancy_ocid          = var.tenancy_id
  user_ocid             = var.user_id
  fingerprint           = var.api_fingerprint
  private_key_path      = var.api_private_key_path
  region                = var.region
}

Create availability_domain.tf file: defines resources or configurations related to the availability domains in a cloud provider,

#Avaiablity Domain
data "oci_identity_availability_domains" "ad" {
    #Required
    compartment_id = var.tenancy_id
}

Create veriables.tf file: used to define input variables for your configuration

########################################################################################################
#API and IAM realted veriables 
########################################################################################################
variable "api_fingerprint" {
  description = "Fingerprint of OCI API private key for Tenancy"
  type        = string
}
variable "api_private_key_path" {
  description = "Path to OCI API private key used for Tenancy"
  type        = string
}
variable "tenancy_id" {
  description = "Tenancy ID where to create resources for Tenancy"
  type        = string
}
variable "user_id" {
  description = "User ID that Terraform will use to create resources for Tenancy"
  type        = string
}
variable "region" {
  description = "OCI region where resources will be created for Tenancy"
  type        = string
}

########################################################################################################
#VCN Specific variables
########################################################################################################
variable "create_new_vcn" {
  description = "Boolean variable to specify whether to create a new VCN or to reuse an existing one."
  type        = bool
}
variable "compartment_id" {
  description = "OCI compartment where resources will be created"
  type        = string
}
variable "vcn_cidr_block" {
  description = "The list of IPv4 CIDR blocks the VCN will use"
  type        = string
}
variable "vcn_display_name" {
  description = "provide a descriptive name for the VCN - this is what you will see displayed in the OCI console"
  type        = string
}
variable "vcn_dns_label" {
  description = "provide a descriptive alphanumeric name for the DNS - this is what you will see displayed in the OCI console"
  type        = string
}
variable "vcn_id" {
  description = "provide your existing VCN OCID if create_new_vcn = false"
  type = string
}
variable "private_subnet_id" {
  description = "provide existing private subnet OCID"
  type = string
}
variable "public_subnet_id" {
  description = "provide existing public subnet OCID"
  type = string
}

#Private subnet variables
variable "private_subnet_cidr_block" {
  description = "OCI private subnet CIDR block range"
  type        = string
}
variable "private_subnet_display_name" {
  description = "provide a descriptive name for the private subnet - this is what you will see displayed in the OCI console"
  type        = string
}
variable "private_subnet_prohibit_public_ip_on_vnic" {
  description = "Allow public IP address to the VNIC"
  type        = bool
}

#Public subnet variables
variable "public_subnet_cidr_block" {
  description = "OCI public subnet CIDR block range"
  type        = string
}
variable "public_subnet_display_name" {
  description = "provide a descriptive name for the public subnet - this is what you will see displayed in the OCI console"
  type        = string
}
variable "public_subnet_prohibit_public_ip_on_vnic" {
  description = "Allow public IP address to the VNIC"
  type        = bool
}

########################################################################################################
#Compute variables
########################################################################################################

variable "instance_shape" {
  description = "value"
  type        = string
}

variable "instance_flex_memory_in_gbs" {
  description = "The total amount of memory available to the instance, in gigabytes."
  type        = number
}

variable "instance_flex_ocpus" {
  description = "The total number of OCPUs available to the instance."
  type        = number
}

variable "instance_create_vnic_details_assign_public_ip" {
  description = "To allow compute connectivity from internet"
  type        = bool
}

variable "instance_display_name" {
  description = "provide a descriptive name for the compute instance - this is what you will see displayed in the OCI console"
  type        = string
}

variable "public_ssh_key" {
  description = "Add your public ssh key - for provisioning your compute instance"
  type        = string
}

variable "private_ssh_key" {
  description = "Add your private ssh key - for accessing your compute instance after creation"
  type        = string
}

variable "create_linux_instance" {
  description = "Boolean variable to specify whether to provision a Linux instances"
  type        = bool
}

variable "create_windows_instance" {
  description = "Boolean variable to specify whether to provision a Windows instances"
  type        = bool
}

variable "windows_image_ocid" {
  description = "OCID of the Windows image to use"
  type        = string
}

variable "linux_image_ocid" {
  description = "OCID of the Linux image to use"
  type        = string
}

Create networking.tf file: define and configure vcn network resources

resource "oci_core_vcn" "test_vcn" {
  count = (var.create_new_vcn) ? 1 : 0

  #Required
  compartment_id = var.compartment_id
  cidr_block   = var.vcn_cidr_block
  display_name = var.vcn_display_name
  dns_label    = var.vcn_dns_label
}
##Subnet 
resource "oci_core_subnet" "private_subnet" {
  count = (var.create_new_vcn) ? 1 : 0

  #Required
  cidr_block     = var.private_subnet_cidr_block
  compartment_id = var.compartment_id
  vcn_id         = oci_core_vcn.test_vcn.*.id[0]
  display_name               = var.private_subnet_display_name
  prohibit_public_ip_on_vnic = var.private_subnet_prohibit_public_ip_on_vnic
}

resource "oci_core_subnet" "public_subnet" {
  count = (var.create_new_vcn) ? 1 : 0

  #Required
  cidr_block     = var.public_subnet_cidr_block
  compartment_id = var.compartment_id
  vcn_id         = oci_core_vcn.test_vcn.*.id[0]
  display_name               = var.public_subnet_display_name
  prohibit_public_ip_on_vnic = var.public_subnet_prohibit_public_ip_on_vnic
  route_table_id             = oci_core_route_table.test_route_table.*.id[0]
}

#Internet Gateway 
resource "oci_core_internet_gateway" "test_internet_gateway" {
  count = (var.create_new_vcn) ? 1 : 0

  #Required
  compartment_id = var.compartment_id
  display_name   = "igw_for_${var.vcn_display_name}"
  vcn_id         = oci_core_vcn.test_vcn.*.id[0]
 # route_table_id = oci_core_route_table.test_route_table.id
}

## Route the traffice to internet 
resource "oci_core_route_table" "test_route_table" {
  count = (var.create_new_vcn) ? 1 : 0

  #Required
  compartment_id = var.compartment_id
  vcn_id         = oci_core_vcn.test_vcn.*.id[0]
  route_rules {
    #Required
    network_entity_id = oci_core_internet_gateway.test_internet_gateway.*.id[0]
    description      = "route rule internet access for ${var.vcn_display_name}"
    destination      = "0.0.0.0/0"
    destination_type = "CIDR_BLOCK"
  }
}

##NSG
resource "oci_core_network_security_group" "test_nsg" {
  count = (var.create_new_vcn) ? 1 : 0

  #Required
  compartment_id = var.compartment_id
  vcn_id         = oci_core_vcn.test_vcn.*.id[0]
  display_name   = "NETWORK_SECURITY_GROUP_${var.vcn_display_name}"
  freeform_tags  = { "Lab" = "Terraofm 101 Guide" }
}

Create compute_linux.tf file: define compute (linux) related resources

resource "oci_core_instance" "test_linux_instance" {
  #Required Information 
  count = var.create_linux_instance ? 1 : 0

  availability_domain = data.oci_identity_availability_domains.ad.availability_domains[0].name
  compartment_id      = var.compartment_id


  create_vnic_details {
    assign_public_ip       = "true"
    display_name           = var.instance_display_name
    nsg_ids                = []
    skip_source_dest_check = "false"
    subnet_id              = var.create_new_vcn ? oci_core_subnet.public_subnet.*.id[0] : var.public_subnet_id
  }

  display_name = "${var.instance_display_name}_linux"

  metadata = {
    ssh_authorized_keys = "${file(var.public_ssh_key)}"
  }

  shape = var.instance_shape
  shape_config {

    memory_in_gbs = var.instance_flex_memory_in_gbs
    ocpus         = var.instance_flex_ocpus
  }

  launch_options {
    boot_volume_type                    = "PARAVIRTUALIZED"
    firmware                            = "UEFI_64"
    is_consistent_volume_naming_enabled = "true"
    is_pv_encryption_in_transit_enabled = "true"
    network_type                        = "PARAVIRTUALIZED"
    remote_data_volume_type             = "PARAVIRTUALIZED"

  }

  source_details {
    #Required Information
    source_id   = var.linux_image_ocid
    source_type = "image"

  }
  preserve_boot_volume = false
}

Create compute_windows.tf file: define compute (Windows) related resources.

resource "oci_core_instance" "test_windows_instance" {
  #Required Information
  count = var.create_windows_instance ? 1 : 0


  availability_domain = data.oci_identity_availability_domains.ad.availability_domains[0].name
  compartment_id      = var.compartment_id



  create_vnic_details {
    assign_public_ip       = "false"
    display_name           = var.instance_display_name
    nsg_ids                = []
    skip_source_dest_check = "false"
    subnet_id              = var.create_new_vcn ? oci_core_subnet.private_subnet.*.id[0] : var.private_subnet_id


  }

  display_name = "${var.instance_display_name}_windows"

  metadata = {
  }

  shape = var.instance_shape
  shape_config {

    memory_in_gbs = var.instance_flex_memory_in_gbs
    ocpus         = var.instance_flex_ocpus
  }

  launch_options {
    boot_volume_type                    = "PARAVIRTUALIZED"
    firmware                            = "UEFI_64"
    is_pv_encryption_in_transit_enabled = "true"
    network_type                        = "PARAVIRTUALIZED"
    remote_data_volume_type             = "PARAVIRTUALIZED"

  }

  source_details {
    #Required Information
    source_id   = var.windows_image_ocid
    source_type = "image"

  }
  preserve_boot_volume = false
}

Create output.tf file: define outputs that display specific values after the configuration is applied

output "list_ads" {
  value = data.oci_identity_availability_domains.ad.availability_domains
}

# Regions
output "linux_instance_region" {
  value = oci_core_instance.test_linux_instance.*.region
}

output "windows_instance_region" {
  value = oci_core_instance.test_windows_instance.*.region
}

# Networking
output "network_vcn_name" {
  value = oci_core_vcn.test_vcn.*.display_name
}

# Compute: Linux Test Instance
output "output_linux_instance_display_name" {
  value = oci_core_instance.test_linux_instance.*.display_name
}

output "output_linux_instance_public_ip" {
  value = oci_core_instance.test_linux_instance.*.public_ip
}

output "output_linux_instance_state" {
  value = oci_core_instance.test_linux_instance.*.state
}

# Compute: Windows Test Instance
output "output_windows_instance_display_name" {
  value = oci_core_instance.test_windows_instance.*.display_name
}

output "output_windows_instance_public_ip" {
  value = oci_core_instance.test_windows_instance.*.public_ip
}

output "output_windows_instance_state" {
  value = oci_core_instance.test_windows_instance.*.state
}

Run the terraform init command to initialize working directory and it download necessary provider plugins

terraform init -var-file=terrafrom.tfvars 

Execute terraform plan command. It gives you the preview of the changes that terraform will make to your infrastructure

terraform plan -var-file=terrafrom.tfvars

Execute terraform apply command and type Yes if it prompt for the apply action. It apples the changes to your cloud infrastructure

terraform apply -var-file=terrafrom.tfvars

Log on to OCI console and verify that resources are created

Resources have now been deployed using Terraform. Once you verify resource creation in your OCI tenancy use the following command to destroy all resources.

terraform destroy -var-file=terrafrom.tfvars

These steps conclude, how to created remove the OCI resources using terraform

Loading