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

> Deploy your first resources on Oracle Cloud (OCI) with Terraform or OpenTofu: API key authentication, a VCN, a compute instance and an Object Storage bucket.

- Source: https://www.itwonderlab.com/terraform-oci-getting-started/
- Published: 2026-09-09
- Updated: 2026-09-09
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## Oracle Cloud with Terraform

Terraform and OpenTofu manage [Oracle Cloud](https://www.itwonderlab.com/oracle-cloud/) Infrastructure (OCI) with the `oci` [provider](https://www.itwonderlab.com/terraform-provider/), which Oracle publishes. The workflow is the same as for [AWS](https://www.itwonderlab.com/aws-terraform-tutorial-terraform-basics/), [Azure](https://www.itwonderlab.com/terraform-azure-getting-started/) and [Google Cloud](https://www.itwonderlab.com/terraform-gcp-getting-started/). 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:

```shell
$ 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](https://www.itwonderlab.com/terraform-secrets-management/): never commit the private key.

### Provider

```hcl title="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](https://www.itwonderlab.com/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.

```hcl title="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:

```hcl title="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](https://www.itwonderlab.com/aws-s3/). The bucket name must be unique within your tenancy's **namespace**, which a data source returns:

```hcl title="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

```shell
$ 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](https://www.itwonderlab.com/terraform-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](https://www.itwonderlab.com/aws-organizations/) | Tenancy and compartments |
| [VPC](https://www.itwonderlab.com/aws-vpc/) | VCN |
| Internet gateway and [route tables](https://www.itwonderlab.com/aws-route-tables/) | Internet gateway and route table |
| Security group | Security list (subnet) or network security group |
| [EC2](https://www.itwonderlab.com/aws-ec2/) | Compute instance |
| [S3](https://www.itwonderlab.com/aws-s3/) | Object Storage |
| [IAM](https://www.itwonderlab.com/aws-iam/) policy | IAM policy on a compartment |
| [EKS](https://www.itwonderlab.com/aws-eks/) | OKE |

Read next [project structure](https://www.itwonderlab.com/terraform-project-structure/) and [best practices](https://www.itwonderlab.com/terraform-best-practices/).
