Terraform and OpenTofu on Oracle Cloud (OCI): Getting Started with the OCI Provider

· 3 min read · Terraform & OpenTofu Tutorials

Oracle Cloud with Terraform #

Terraform and OpenTofu manage Oracle Cloud Infrastructure (OCI) with the oci provider, which Oracle publishes. The workflow is the same as for AWS, Azure and Google Cloud. Two OCI ideas explain most of the code:

  • A compartment is a logical container for resources, used for access control and cost tracking. The tenancy itself is the root compartment. Almost every resource needs a compartment_id.
  • Resources are identified by an OCID, a long unique string such as ocid1.compartment.oc1..aaaa.... You will pass OCIDs between resources and as variables.

Authentication with an API key #

The provider uses API key authentication by default. In the OCI console open your profile, User settings, API keys, Add API key, generate a key pair and download the private key. The console shows a configuration snippet with the values you need: the tenancy OCID, the user OCID, the key fingerprint and the region.

The provider accepts these values as Terraform variables, and Terraform reads variables from TF_VAR_ environment variables:

$ export TF_VAR_tenancy_ocid="ocid1.tenancy.oc1..<unique-id>"
$ export TF_VAR_user_ocid="ocid1.user.oc1..<unique-id>"
$ export TF_VAR_fingerprint="<key fingerprint>"
$ export TF_VAR_private_key_path="$HOME/.oci/oci_api_key.pem"
$ export TF_VAR_region="eu-frankfurt-1"
$ export TF_VAR_compartment_ocid="ocid1.compartment.oc1..<unique-id>"

Other methods exist and are better outside your laptop: auth = "SecurityToken" with a config_file_profile uses a session token from the OCI CLI (it expires after one hour), and auth = "InstancePrincipal" lets a pipeline that runs on an OCI instance authenticate without keys. See secrets management: never commit the private key.

Provider #

providers.tf
terraform {
  required_version = ">= 1.6"

  required_providers {
    oci = {
      source  = "oracle/oci"
      version = "~> 9.0"
    }
  }
}

variable "tenancy_ocid" {
  type = string
}

variable "user_ocid" {
  type = string
}

variable "fingerprint" {
  type = string
}

variable "private_key_path" {
  type = string
}

variable "region" {
  type = string
}

variable "compartment_ocid" {
  type = string
}

variable "ssh_public_key_path" {
  type    = string
  default = "~/.ssh/id_ed25519.pub"
}

variable "admin_cidr" {
  type        = string
  description = "Address range that can connect with SSH, for example your public IP as x.x.x.x/32"
}

provider "oci" {
  tenancy_ocid     = var.tenancy_ocid
  user_ocid        = var.user_ocid
  fingerprint      = var.fingerprint
  private_key_path = var.private_key_path
  region           = var.region
}

A virtual cloud network #

A VCN is the equivalent of an AWS VPC and it is regional. The VCN and its subnets are not reachable from the internet until you attach an internet gateway and add a route to it.

network.tf
resource "oci_core_vcn" "main" {
  compartment_id = var.compartment_ocid
  display_name   = "ditwl-demo"
  cidr_blocks    = ["10.0.0.0/16"]
  dns_label      = "ditwl"
}

resource "oci_core_internet_gateway" "main" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
  display_name   = "ditwl-demo-igw"
}

resource "oci_core_route_table" "public" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
  display_name   = "ditwl-demo-public"

  route_rules {
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
    network_entity_id = oci_core_internet_gateway.main.id
  }
}

resource "oci_core_security_list" "public" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.main.id
  display_name   = "ditwl-demo-public"

  egress_security_rules {
    destination = "0.0.0.0/0"
    protocol    = "all"
  }

  ingress_security_rules {
    protocol = "6" # TCP
    source   = var.admin_cidr

    tcp_options {
      min = 22
      max = 22
    }
  }
}

resource "oci_core_subnet" "public" {
  compartment_id    = var.compartment_ocid
  vcn_id            = oci_core_vcn.main.id
  display_name      = "ditwl-demo-public"
  cidr_block        = "10.0.1.0/24"
  dns_label         = "public"
  route_table_id    = oci_core_route_table.public.id
  security_list_ids = [oci_core_security_list.public.id]
}

Notes:

  • Protocols in security lists are IANA protocol numbers as strings: "6" is TCP, "17" is UDP, "1" is ICMP, and "all" means all protocols.
  • A security list applies to every instance in a subnet. A network security group (oci_core_network_security_group) applies to chosen network interfaces and is closer to an AWS security group.
  • A subnet without route_table_id and security_list_ids uses the default ones of the VCN, which are easy to forget. Set them explicitly as above.

A compute instance #

Look up the availability domain and the latest Ubuntu image with data sources, so that no OCID is copied by hand:

compute.tf
data "oci_identity_availability_domains" "ads" {
  compartment_id = var.tenancy_ocid
}

data "oci_core_images" "ubuntu" {
  compartment_id           = var.compartment_ocid
  operating_system         = "Canonical Ubuntu"
  operating_system_version = "24.04"
  shape                    = "VM.Standard.A1.Flex"
  sort_by                  = "TIMECREATED"
  sort_order               = "DESC"
}

resource "oci_core_instance" "web" {
  compartment_id      = var.compartment_ocid
  availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name
  display_name        = "ditwl-demo-web"
  shape               = "VM.Standard.A1.Flex"

  shape_config {
    ocpus         = 1
    memory_in_gbs = 6
  }

  source_details {
    source_type = "image"
    source_id   = data.oci_core_images.ubuntu.images[0].id
  }

  create_vnic_details {
    subnet_id        = oci_core_subnet.public.id
    assign_public_ip = true
  }

  metadata = {
    ssh_authorized_keys = file(pathexpand(var.ssh_public_key_path))
  }
}

output "web_public_ip" {
  value = oci_core_instance.web.public_ip
}

VM.Standard.A1.Flex is an Arm (Ampere) shape whose OCPUs and memory you choose in shape_config. It can be used within the limits of the Always Free tier, which Oracle can change, so check your own limits in the console. Free capacity is sometimes exhausted in a region, and then the apply fails with an "Out of host capacity" error: try another availability domain (change the index [0]) or another shape. Connect with ssh ubuntu@<ip>.

An Object Storage bucket #

Object Storage is the equivalent of S3. The bucket name must be unique within your tenancy's namespace, which a data source returns:

storage.tf
data "oci_objectstorage_namespace" "ns" {
  compartment_id = var.tenancy_ocid
}

resource "oci_objectstorage_bucket" "data" {
  compartment_id = var.compartment_ocid
  namespace      = data.oci_objectstorage_namespace.ns.namespace
  name           = "ditwl-demo-data"
  access_type    = "NoPublicAccess"
  versioning     = "Enabled"
}

Run it #

$ tofu init
$ tofu plan -var admin_cidr=203.0.113.25/32
$ tofu apply -var admin_cidr=203.0.113.25/32
$ tofu destroy -var admin_cidr=203.0.113.25/32

Use your own public IP in admin_cidr. tofu destroy removes everything and avoids charges for resources outside the free tier. Remote state: any backend works, and Oracle also offers Resource Manager, a managed service that runs Terraform configurations inside OCI.

Compare with AWS #

AWS Oracle Cloud
Account / Organizations Tenancy and compartments
VPC VCN
Internet gateway and route tables Internet gateway and route table
Security group Security list (subnet) or network security group
EC2 Compute instance
S3 Object Storage
IAM policy IAM policy on a compartment
EKS OKE

Read next project structure and best practices.

#Terraform #OpenTofu #Oci #Oracle Cloud