{"id":2212,"date":"2024-05-21T13:29:24","date_gmt":"2024-05-21T13:29:24","guid":{"rendered":"https:\/\/nicktailor.com\/tech-blog\/?p=2212"},"modified":"2026-01-21T13:30:39","modified_gmt":"2026-01-21T13:30:39","slug":"deploying-production-grade-systems-on-oracle-cloud-infrastructure-oci-with-terraform","status":"publish","type":"post","link":"https:\/\/nicktailor.com\/tech-blog\/deploying-production-grade-systems-on-oracle-cloud-infrastructure-oci-with-terraform\/","title":{"rendered":"Deploying Production-Grade Systems on Oracle Cloud Infrastructure (OCI) with Terraform"},"content":{"rendered":"\n<p>Launching a virtual machine is easy. Running <strong>secure, reliable, production-grade systems<\/strong> is not. This guide shows how to deploy <strong>enterprise-ready compute infrastructure on Oracle Cloud Infrastructure (OCI)<\/strong> using <strong>Terraform<\/strong>, with a focus on security, fault tolerance, and long-term operability.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What &#8220;Production-Grade&#8221; Actually Means<\/h2>\n\n\n\n<p>A production environment is defined by <em>predictability<\/em>, not convenience. Production systems must survive failures, scale safely, and be observable at all times.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Private networking by default<\/li>\n\n\n\n<li>No public SSH access<\/li>\n\n\n\n<li>Replaceable compute instances<\/li>\n\n\n\n<li>Persistent storage separated from OS<\/li>\n\n\n\n<li>Infrastructure defined as code<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Target Architecture Overview<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Private VCN and subnet with NAT gateway for outbound access<\/li>\n\n\n\n<li>Network Security Groups (NSGs) with explicit rules<\/li>\n\n\n\n<li>Flex compute shape<\/li>\n\n\n\n<li>Detached block storage with iSCSI attachment<\/li>\n\n\n\n<li>SSH key authentication only<\/li>\n<\/ul>\n\n\n\n<p>This architecture is suitable for:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>SaaS backends<\/li>\n\n\n\n<li>Internal APIs<\/li>\n\n\n\n<li>Databases<\/li>\n\n\n\n<li>AI \/ ML inference nodes<\/li>\n\n\n\n<li>HPC control or login nodes<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Terraform: Provider Configuration<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\nterraform {\n  required_version = \"&gt;= 1.6\"\n\n  required_providers {\n    oci = {\n      source  = \"oracle\/oci\"\n      version = \"&gt;= 5.0.0\"\n    }\n  }\n}\n\nprovider \"oci\" {\n  tenancy_ocid     = var.tenancy_ocid\n  user_ocid        = var.user_ocid\n  fingerprint      = var.fingerprint\n  private_key_path = var.private_key_path\n  region           = var.region\n}\n<\/code><\/pre>\n\n\n\n<p>This ensures reproducible deployments and enforces secure API-based authentication.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Variables<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\nvariable \"tenancy_ocid\" {\n  description = \"OCID of the tenancy\"\n  type        = string\n}\n\nvariable \"user_ocid\" {\n  description = \"OCID of the user calling the API\"\n  type        = string\n}\n\nvariable \"fingerprint\" {\n  description = \"Fingerprint of the API signing key\"\n  type        = string\n}\n\nvariable \"private_key_path\" {\n  description = \"Path to the private key for API authentication\"\n  type        = string\n}\n\nvariable \"region\" {\n  description = \"OCI region identifier\"\n  type        = string\n}\n\nvariable \"compartment_ocid\" {\n  description = \"OCID of the compartment for resources\"\n  type        = string\n}\n\nvariable \"image_ocid\" {\n  description = \"OCID of the compute image (e.g., Oracle Linux 8)\"\n  type        = string\n}\n\nvariable \"ssh_public_key\" {\n  description = \"Path to SSH public key file\"\n  type        = string\n}\n\nvariable \"allowed_cidr\" {\n  description = \"CIDR block allowed to access instances (e.g., VPN range)\"\n  type        = string\n  default     = \"10.0.0.0\/16\"\n}\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Data Sources<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\ndata \"oci_identity_availability_domains\" \"ads\" {\n  compartment_id = var.tenancy_ocid\n}\n\nlocals {\n  availability_domain = data.oci_identity_availability_domains.ads.availability_domains&#91;0].name\n}\n<\/code><\/pre>\n\n\n\n<p>This retrieves the list of availability domains in your region. We select the first AD for simplicity, but production deployments should consider multi-AD placement.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Virtual Cloud Network (VCN)<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\nresource \"oci_core_vcn\" \"prod_vcn\" {\n  cidr_blocks    = &#91;\"10.0.0.0\/16\"]\n  display_name   = \"prod-vcn\"\n  dns_label      = \"prodvcn\"\n  compartment_id = var.compartment_ocid\n}\n<\/code><\/pre>\n\n\n\n<p>A \/16 CIDR allows future expansion without redesign. VCNs act as the first isolation boundary for production systems.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Internet Gateway<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\nresource \"oci_core_internet_gateway\" \"igw\" {\n  compartment_id = var.compartment_ocid\n  vcn_id         = oci_core_vcn.prod_vcn.id\n  display_name   = \"prod-igw\"\n  enabled        = true\n}\n<\/code><\/pre>\n\n\n\n<p>The internet gateway enables outbound connectivity for the NAT gateway. It does not expose private instances directly.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">NAT Gateway<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\nresource \"oci_core_nat_gateway\" \"nat_gw\" {\n  compartment_id = var.compartment_ocid\n  vcn_id         = oci_core_vcn.prod_vcn.id\n  display_name   = \"prod-nat-gw\"\n  block_traffic  = false\n}\n<\/code><\/pre>\n\n\n\n<p>The NAT gateway allows private subnet instances to reach the internet for package updates and external API calls without exposing inbound access.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Route Tables<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\nresource \"oci_core_route_table\" \"private_rt\" {\n  compartment_id = var.compartment_ocid\n  vcn_id         = oci_core_vcn.prod_vcn.id\n  display_name   = \"private-route-table\"\n\n  route_rules {\n    destination       = \"0.0.0.0\/0\"\n    destination_type  = \"CIDR_BLOCK\"\n    network_entity_id = oci_core_nat_gateway.nat_gw.id\n  }\n}\n<\/code><\/pre>\n\n\n\n<p>All outbound traffic from the private subnet routes through the NAT gateway. This ensures instances can reach external resources without being directly accessible.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Private Subnet (No Public IPs)<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\nresource \"oci_core_subnet\" \"private_subnet\" {\n  cidr_block                 = \"10.0.1.0\/24\"\n  vcn_id                     = oci_core_vcn.prod_vcn.id\n  compartment_id             = var.compartment_ocid\n  display_name               = \"private-subnet\"\n  prohibit_public_ip_on_vnic = true\n  route_table_id             = oci_core_route_table.private_rt.id\n  dns_label                  = \"private\"\n}\n<\/code><\/pre>\n\n\n\n<p>Instances in this subnet are never reachable from the internet. Access must go through a bastion, VPN, or private load balancer.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Network Security Groups (NSG)<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\nresource \"oci_core_network_security_group\" \"app_nsg\" {\n  compartment_id = var.compartment_ocid\n  vcn_id         = oci_core_vcn.prod_vcn.id\n  display_name   = \"app-nsg\"\n}\n\n# Allow SSH from internal network only\nresource \"oci_core_network_security_group_security_rule\" \"allow_ssh\" {\n  network_security_group_id = oci_core_network_security_group.app_nsg.id\n  direction                 = \"INGRESS\"\n  protocol                  = \"6\" # TCP\n\n  source      = var.allowed_cidr\n  source_type = \"CIDR_BLOCK\"\n\n  tcp_options {\n    destination_port_range {\n      min = 22\n      max = 22\n    }\n  }\n}\n\n# Allow HTTPS from internal network\nresource \"oci_core_network_security_group_security_rule\" \"allow_https\" {\n  network_security_group_id = oci_core_network_security_group.app_nsg.id\n  direction                 = \"INGRESS\"\n  protocol                  = \"6\" # TCP\n\n  source      = var.allowed_cidr\n  source_type = \"CIDR_BLOCK\"\n\n  tcp_options {\n    destination_port_range {\n      min = 443\n      max = 443\n    }\n  }\n}\n\n# Allow all outbound traffic\nresource \"oci_core_network_security_group_security_rule\" \"allow_egress\" {\n  network_security_group_id = oci_core_network_security_group.app_nsg.id\n  direction                 = \"EGRESS\"\n  protocol                  = \"all\"\n\n  destination      = \"0.0.0.0\/0\"\n  destination_type = \"CIDR_BLOCK\"\n}\n\n# Allow ICMP for path MTU discovery\nresource \"oci_core_network_security_group_security_rule\" \"allow_icmp\" {\n  network_security_group_id = oci_core_network_security_group.app_nsg.id\n  direction                 = \"INGRESS\"\n  protocol                  = \"1\" # ICMP\n\n  source      = \"10.0.0.0\/16\"\n  source_type = \"CIDR_BLOCK\"\n\n  icmp_options {\n    type = 3\n    code = 4\n  }\n}\n<\/code><\/pre>\n\n\n\n<p>NSGs provide service-level firewalling and are preferred over subnet-wide security lists. These rules allow SSH and HTTPS only from your internal network, while permitting all outbound traffic.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Compute Instance (Flex Shape)<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\nresource \"oci_core_instance\" \"prod_instance\" {\n  availability_domain = local.availability_domain\n  compartment_id      = var.compartment_ocid\n  display_name        = \"prod-app-01\"\n  shape               = \"VM.Standard.E4.Flex\"\n\n  shape_config {\n    ocpus         = 2\n    memory_in_gbs = 16\n  }\n\n  create_vnic_details {\n    subnet_id        = oci_core_subnet.private_subnet.id\n    assign_public_ip = false\n    nsg_ids          = &#91;oci_core_network_security_group.app_nsg.id]\n    hostname_label   = \"prod-app-01\"\n  }\n\n  source_details {\n    source_type             = \"image\"\n    source_id               = var.image_ocid\n    boot_volume_size_in_gbs = 50\n  }\n\n  metadata = {\n    ssh_authorized_keys = file(var.ssh_public_key)\n  }\n\n  preserve_boot_volume = true\n}\n<\/code><\/pre>\n\n\n\n<p>Flex shapes allow independent scaling of CPU and memory, ensuring predictable performance without overpaying for unused resources. Setting <code>preserve_boot_volume = true<\/code> protects the boot volume if the instance is accidentally terminated.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Persistent Block Storage<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\nresource \"oci_core_volume\" \"data_volume\" {\n  availability_domain = local.availability_domain\n  compartment_id      = var.compartment_ocid\n  display_name        = \"prod-data-vol\"\n  size_in_gbs         = 200\n  vpus_per_gb         = 10 # Balanced performance tier\n}\n\nresource \"oci_core_volume_attachment\" \"data_attach\" {\n  attachment_type = \"paravirtualized\"\n  instance_id     = oci_core_instance.prod_instance.id\n  volume_id       = oci_core_volume.data_volume.id\n  display_name    = \"prod-data-attachment\"\n}\n<\/code><\/pre>\n\n\n\n<p>Separating OS and data ensures instances are disposable while data remains protected. Paravirtualized attachments are simpler than iSCSI and work automatically on Oracle Linux.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Post-Deployment: Mounting the Block Volume<\/h3>\n\n\n\n<p>After Terraform applies, SSH into the instance and mount the volume:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\n# Find the attached volume (usually \/dev\/sdb)\nlsblk\n\n# Create filesystem (first time only)\nsudo mkfs.xfs \/dev\/sdb\n\n# Create mount point and mount\nsudo mkdir -p \/data\nsudo mount \/dev\/sdb \/data\n\n# Add to fstab for persistence across reboots\necho '\/dev\/sdb \/data xfs defaults,_netdev,nofail 0 2' | sudo tee -a \/etc\/fstab\n<\/code><\/pre>\n\n\n\n<p>The <code>_netdev<\/code> and <code>nofail<\/code> options ensure the system boots even if the volume is temporarily unavailable.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Outputs<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\noutput \"instance_private_ip\" {\n  description = \"Private IP address of the compute instance\"\n  value       = oci_core_instance.prod_instance.private_ip\n}\n\noutput \"instance_id\" {\n  description = \"OCID of the compute instance\"\n  value       = oci_core_instance.prod_instance.id\n}\n\noutput \"vcn_id\" {\n  description = \"OCID of the VCN\"\n  value       = oci_core_vcn.prod_vcn.id\n}\n\noutput \"volume_id\" {\n  description = \"OCID of the data volume\"\n  value       = oci_core_volume.data_volume.id\n}\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Security &amp; Operational Checklist<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u2713 No public SSH access<\/li>\n\n\n\n<li>\u2713 Key-based authentication only<\/li>\n\n\n\n<li>\u2713 Private networking with NAT for outbound<\/li>\n\n\n\n<li>\u2713 Explicit NSG rules (no default allow)<\/li>\n\n\n\n<li>\u2713 Persistent storage with separate lifecycle<\/li>\n\n\n\n<li>\u2713 Infrastructure fully defined in code<\/li>\n\n\n\n<li>\u2713 Boot volume preservation enabled<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What to Add Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Bastion Service<\/strong> &#8211; OCI&#8217;s managed bastion for secure SSH access without VPN<\/li>\n\n\n\n<li><strong>Site-to-Site VPN<\/strong> &#8211; Connect to on-premises networks<\/li>\n\n\n\n<li><strong>OCI Load Balancer<\/strong> &#8211; For multi-instance deployments<\/li>\n\n\n\n<li><strong>Monitoring and Alerting<\/strong> &#8211; OCI Monitoring service with custom alarms<\/li>\n\n\n\n<li><strong>Dynamic Groups and IAM policies<\/strong> &#8211; Instance principals for secure API access<\/li>\n\n\n\n<li><strong>Cloud-init or Ansible<\/strong> &#8211; OS hardening and application deployment<\/li>\n\n\n\n<li><strong>CI\/CD pipelines<\/strong> &#8211; GitOps workflow for Terraform changes<\/li>\n\n\n\n<li><strong>Volume backups<\/strong> &#8211; Scheduled backup policies for data protection<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Example terraform.tfvars<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>\ntenancy_ocid     = \"ocid1.tenancy.oc1..aaaaaaaaexample\"\nuser_ocid        = \"ocid1.user.oc1..aaaaaaaaexample\"\nfingerprint      = \"aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99\"\nprivate_key_path = \"~\/.oci\/oci_api_key.pem\"\nregion           = \"eu-frankfurt-1\"\ncompartment_ocid = \"ocid1.compartment.oc1..aaaaaaaaexample\"\nimage_ocid       = \"ocid1.image.oc1.eu-frankfurt-1.aaaaaaaaexample\"\nssh_public_key   = \"~\/.ssh\/id_rsa.pub\"\nallowed_cidr     = \"10.0.0.0\/16\"\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Nick Tailor&#8217;s Thoughts<\/h2>\n\n\n\n<p>Production infrastructure is not about clicking faster. It is about <strong>repeatability, security, and recovery<\/strong>. OCI combined with Terraform provides an extremely strong foundation when engineered correctly from day one.<\/p>\n\n\n\n<p>If you treat infrastructure as software, production becomes predictable.<\/p>\n\n\n\n<p>The complete code from this guide is available as a ready-to-use Terraform module. Clone it, update your variables, and run <code>terraform apply<\/code> to deploy.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Launching a virtual machine is easy. Running secure, reliable, production-grade systems is not. This guide shows how to deploy enterprise-ready compute infrastructure on Oracle Cloud Infrastructure (OCI) using Terraform, with a focus on security, fault tolerance, and long-term operability. What &#8220;Production-Grade&#8221; Actually Means A production environment is defined by predictability, not convenience. Production systems must survive failures, scale safely, and<a href=\"https:\/\/nicktailor.com\/tech-blog\/deploying-production-grade-systems-on-oracle-cloud-infrastructure-oci-with-terraform\/\" class=\"read-more\">Read More &#8230;<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[141,130],"tags":[],"class_list":["post-2212","post","type-post","status-publish","format-standard","hentry","category-oci","category-terraform"],"_links":{"self":[{"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/posts\/2212","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/comments?post=2212"}],"version-history":[{"count":1,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/posts\/2212\/revisions"}],"predecessor-version":[{"id":2213,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/posts\/2212\/revisions\/2213"}],"wp:attachment":[{"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/media?parent=2212"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/categories?post=2212"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nicktailor.com\/tech-blog\/wp-json\/wp\/v2\/tags?post=2212"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}