OCI GoldenGate: Building a Real-Time CDC Pipeline from On-Premises Oracle Database to Autonomous Database

Database migration projects fail in predictable ways. The team runs an export, copies the data, imports it, and then discovers the source database kept taking writes during the transfer. The delta needs to be applied. The delta takes longer than expected. The maintenance window closes. The project slips. The next attempt includes a longer maintenance window, which the business rejects.

Change Data Capture solves this at the architecture level. Instead of taking a point-in-time copy, you stream every committed transaction from the source database to the target in real time. The target stays current continuously. When you are ready to cut over, the gap between source and target is seconds, not hours. You flip the connection string, drain in-flight requests, and the migration is done.

OCI GoldenGate is Oracle’s managed CDC and replication service. It runs the GoldenGate engine inside OCI as a fully managed deployment, handles the Extract process on the source side, moves transactions across the network, and applies them to the target using a Replicat process. In this post I will walk through configuring a full CDC pipeline from an on-premises Oracle Database 19c to OCI Autonomous Database using Terraform for the infrastructure layer and the GoldenGate REST API for pipeline configuration.

Architecture

The pipeline has three components running in sequence.

The Extract process connects to the source Oracle Database, reads the redo logs, and captures committed DML and DDL operations. It writes these to trail files inside the GoldenGate deployment.

The Distribution Path moves trail files from the source-side Extract to the OCI GoldenGate deployment over an encrypted connection. In a hybrid setup where the source database is on-premises, this path crosses the network via FastConnect or IPSec VPN.

The Replicat process reads trail files inside OCI GoldenGate and applies the transactions to Autonomous Database using the native OCI Autonomous Database connection.

Prerequisites

Before starting you need the following in place.

On the source Oracle Database: supplemental logging must be enabled at the database level and at the table level for all tables being replicated. The GoldenGate Extract user needs SELECT on the tables, access to VLOGandVLOG and VLOGandVLOGFILE, and EXECUTE on DBMS_FLASHBACK.

On the network side: a FastConnect private peering or IPSec VPN between your on-premises network and your OCI VCN. The GoldenGate deployment sits in a private subnet and reaches the source database over this private connection.

On OCI: an Autonomous Database instance already provisioned, with the wallet downloaded and the admin password stored in OCI Vault.

Step 1: Source Database Preparation

Connect to the source Oracle Database as SYSDBA and run:

-- Enable supplemental logging at the database level
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA;
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (PRIMARY KEY) COLUMNS;
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (UNIQUE) COLUMNS;
-- Verify supplemental logging is active
SELECT SUPPLEMENTAL_LOG_DATA_MIN,
SUPPLEMENTAL_LOG_DATA_PK,
SUPPLEMENTAL_LOG_DATA_UI
FROM V$DATABASE;
-- Create the GoldenGate capture user
CREATE USER ggadmin IDENTIFIED BY "YourStrongPassword123!";
GRANT CREATE SESSION TO ggadmin;
GRANT SELECT ANY DICTIONARY TO ggadmin;
GRANT SELECT ANY TABLE TO ggadmin;
GRANT FLASHBACK ANY TABLE TO ggadmin;
GRANT EXECUTE ON DBMS_FLASHBACK TO ggadmin;
GRANT SELECT ON SYS.V_$DATABASE TO ggadmin;
GRANT SELECT ON SYS.V_$LOG TO ggadmin;
GRANT SELECT ON SYS.V_$LOGFILE TO ggadmin;
GRANT SELECT ON SYS.V_$ARCHIVED_LOG TO ggadmin;
GRANT SELECT ON SYS.V_$LOG_HISTORY TO ggadmin;
GRANT SELECT ON SYS.V_$TRANSACTION TO ggadmin;
-- Enable supplemental logging on specific tables
-- Run this for each table you want to replicate
ALTER TABLE hr.employees ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;
ALTER TABLE hr.departments ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;
ALTER TABLE orders.order_header ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;
ALTER TABLE orders.order_lines ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;

Let’s write another blog that should be technical as well, no repeated content from OsamaOracle and No AI Indicator such as “—”

Searched the web

Searched the web

Good. I have a clear picture of what is covered. Topics not touched on the site: OCI Queue Service, OCI Cache (Redis), OCI GoldenGate, OCI DevOps pipelines, OCI Compute autoscaling with custom metrics, and OCI Network Firewall. I will go with OCI Network Firewall as the topic — it is a dedicated next-generation firewall service, fully distinct from NSGs, Security Lists, or WAF, and nothing on the site covers it.


OCI Network Firewall: Building a Centralized Inspection Architecture with Terraform

Security Lists and Network Security Groups handle stateful packet filtering at the subnet and VNIC level. They are the right tool for controlling which ports and protocols reach a resource. What they cannot do is inspect the content of traffic, detect threats based on application-layer signatures, block specific URLs or FQDNs, or apply SSL inspection to decrypt and re-encrypt traffic in flight. That requires a different layer entirely.

OCI Network Firewall is Oracle’s managed next-generation firewall service, built on Palo Alto Networks technology and integrated natively into VCN routing. It supports application-layer inspection, IDPS (Intrusion Detection and Prevention), URL filtering, FQDN-based rules, and TLS inspection. Unlike a third-party firewall appliance you would deploy on a compute instance, OCI Network Firewall is a fully managed service: Oracle handles the underlying infrastructure, HA, and scaling. You manage the policy.

In this post I will walk through designing a hub-and-spoke inspection architecture, deploying the firewall and its policy using Terraform, configuring IDPS and URL filtering rules, and validating traffic flow with OCI Flow Logs.

Architecture: Hub-and-Spoke with Centralized Inspection

The standard pattern for OCI Network Firewall in multi-VCN environments is centralized inspection through a hub VCN. All spoke VCNs route traffic through the hub, and the firewall sits in the hub inspecting both north-south (internet-bound) and east-west (spoke-to-spoke) traffic.

Internet Gateway
|
[OCI Network Firewall] (hub VCN - firewall subnet)
|
Dynamic Routing Gateway
/ \
Spoke VCN 1 Spoke VCN 2
(app tier) (data tier)

Traffic routing in this architecture uses a combination of DRG route tables and VCN ingress/egress route tables to steer all flows through the firewall subnet before they reach their destination. This is the most important concept to get right: the firewall only inspects traffic that is routed through it. Misconfigured route tables mean packets bypass the firewall entirely with no error or warning.

Step 1: Hub VCN and Firewall Subnet

hcl

resource "oci_core_vcn" "hub_vcn" {
compartment_id = var.compartment_id
cidr_blocks = ["192.168.0.0/16"]
display_name = "hub-inspection-vcn"
dns_label = "hubvcn"
}
# Firewall subnet - the firewall VNIC lives here
resource "oci_core_subnet" "firewall_subnet" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.hub_vcn.id
cidr_block = "192.168.1.0/24"
display_name = "firewall-subnet"
dns_label = "fwsubnet"
prohibit_public_ip_on_vnic = true
route_table_id = oci_core_route_table.firewall_subnet_rt.id
security_list_ids = [oci_core_security_list.firewall_sl.id]
}
# Internet Gateway for north-south traffic
resource "oci_core_internet_gateway" "hub_igw" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.hub_vcn.id
display_name = "hub-internet-gateway"
enabled = true
}
# DRG for spoke VCN attachment
resource "oci_core_drg" "hub_drg" {
compartment_id = var.compartment_id
display_name = "hub-drg"
}
resource "oci_core_drg_attachment" "hub_vcn_attachment" {
drg_id = oci_core_drg.hub_drg.id
display_name = "hub-vcn-attachment"
network_details {
id = oci_core_vcn.hub_vcn.id
type = "VCN"
}
}

The firewall subnet must not have a public IP on its VNIC. The firewall receives traffic through routing, not through a public endpoint.

Step 2: Firewall Policy

The policy is the heart of the firewall. It contains address lists, URL lists, application lists, and the ordered set of security rules. All of these are defined as child resources of the policy and are applied when the policy is attached to a firewall instance.

hcl

resource "oci_network_firewall_network_firewall_policy" "production_policy" {
compartment_id = var.compartment_id
display_name = "production-inspection-policy"
}
# IP address list for trusted internal RFC1918 ranges
resource "oci_network_firewall_network_firewall_policy_address_list" "internal_ranges" {
name = "internal-rfc1918"
network_firewall_policy_id = oci_network_firewall_network_firewall_policy.production_policy.id
type = "IP"
addresses = [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"
]
}
# FQDN list for allowed outbound SaaS destinations
resource "oci_network_firewall_network_firewall_policy_address_list" "allowed_saas" {
name = "allowed-saas-fqdns"
network_firewall_policy_id = oci_network_firewall_network_firewall_policy.production_policy.id
type = "FQDN"
addresses = [
"*.oracle.com",
"*.oraclecloud.com",
"*.github.com",
"registry-1.docker.io",
"auth.docker.io",
"production.cloudflare.docker.com"
]
}
# URL list for blocked categories
resource "oci_network_firewall_network_firewall_policy_url_list" "blocked_urls" {
name = "blocked-url-categories"
network_firewall_policy_id = oci_network_firewall_network_firewall_policy.production_policy.id
urls {
pattern = "*.pastebin.com"
type = "SIMPLE"
}
urls {
pattern = "*.ngrok.io"
type = "SIMPLE"
}
urls {
pattern = "*.ngrok-free.app"
type = "SIMPLE"
}
}
# Application list scoping HTTPS traffic
resource "oci_network_firewall_network_firewall_policy_application_group" "web_apps" {
name = "web-traffic"
network_firewall_policy_id = oci_network_firewall_network_firewall_policy.production_policy.id
apps = ["HTTP", "HTTPS", "SSL"]
}

Step 3: Security Rules

Rules are evaluated in order. The first matching rule wins. Structure your rules from most specific to most general and always end with an explicit deny-all for traffic that does not match any allow rule.

hcl

# Rule 1: Allow spoke-to-spoke east-west traffic between known internal ranges
resource "oci_network_firewall_network_firewall_policy_security_rule" "allow_east_west" {
name = "allow-internal-east-west"
network_firewall_policy_id = oci_network_firewall_network_firewall_policy.production_policy.id
action = "ALLOW"
priority = 100
condition {
source_address = [oci_network_firewall_network_firewall_policy_address_list.internal_ranges.name]
destination_address = [oci_network_firewall_network_firewall_policy_address_list.internal_ranges.name]
}
inspection = "INTRUSION_DETECTION"
}
# Rule 2: Allow outbound HTTPS to approved SaaS FQDNs with IPS enabled
resource "oci_network_firewall_network_firewall_policy_security_rule" "allow_saas_egress" {
name = "allow-saas-egress"
network_firewall_policy_id = oci_network_firewall_network_firewall_policy.production_policy.id
action = "ALLOW"
priority = 200
condition {
source_address = [oci_network_firewall_network_firewall_policy_address_list.internal_ranges.name]
destination_address = [oci_network_firewall_network_firewall_policy_address_list.allowed_saas.name]
application = [oci_network_firewall_network_firewall_policy_application_group.web_apps.name]
}
inspection = "INTRUSION_PREVENTION"
}
# Rule 3: Block access to known bad URL patterns
resource "oci_network_firewall_network_firewall_policy_security_rule" "block_bad_urls" {
name = "block-prohibited-urls"
network_firewall_policy_id = oci_network_firewall_network_firewall_policy.production_policy.id
action = "DROP"
priority = 300
condition {
url = [oci_network_firewall_network_firewall_policy_url_list.blocked_urls.name]
}
}
# Rule 4: Explicit deny-all as the last rule
resource "oci_network_firewall_network_firewall_policy_security_rule" "deny_all" {
name = "deny-all-unmatched"
network_firewall_policy_id = oci_network_firewall_network_firewall_policy.production_policy.id
action = "DROP"
priority = 65534
condition {}
}

The inspection field on allow rules sets the IDPS mode. INTRUSION_DETECTION logs threats without blocking. INTRUSION_PREVENTION blocks them. Use detection first in a new environment and move to prevention after you have validated no false positives are hitting legitimate traffic.

Step 4: Deploy the Firewall Instance

hcl

resource "oci_network_firewall_network_firewall" "hub_firewall" {
compartment_id = var.compartment_id
display_name = "hub-production-firewall"
subnet_id = oci_core_subnet.firewall_subnet.id
network_firewall_policy_id = oci_network_firewall_network_firewall_policy.production_policy.id
availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name
defined_tags = {
"Operations.Environment" = "production"
"Operations.ManagedBy" = "terraform"
}
}
# Retrieve the firewall's private IP - needed for route table configuration
data "oci_core_private_ips" "firewall_ip" {
subnet_id = oci_core_subnet.firewall_subnet.id
ip_address = oci_network_firewall_network_firewall.hub_firewall.ipv4_address
depends_on = [oci_network_firewall_network_firewall.hub_firewall]
}

The firewall takes 10 to 15 minutes to provision on first deployment. The Terraform apply will wait. Do not cancel it.

Step 5: Route Tables for Traffic Steering

This is where most implementations go wrong. Three separate route tables are required to steer traffic correctly through the firewall.

The first route table is for the Internet Gateway ingress: inbound traffic from the internet destined for a spoke VCN must be routed to the firewall before reaching the DRG.

hcl

# IGW ingress route table - applied to the Internet Gateway
resource "oci_core_route_table" "igw_ingress_rt" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.hub_vcn.id
display_name = "igw-ingress-route-table"
route_rules {
destination = "10.0.0.0/8"
destination_type = "CIDR_BLOCK"
network_entity_id = data.oci_core_private_ips.firewall_ip.private_ips[0].id
}
route_rules {
destination = "172.16.0.0/12"
destination_type = "CIDR_BLOCK"
network_entity_id = data.oci_core_private_ips.firewall_ip.private_ips[0].id
}
}
# Associate route table with the Internet Gateway
resource "oci_core_vcn_dns_resolver_association" "igw_rt_association" {
# Note: in OCI you associate the route table with the IGW via the VCN route table update
}
resource "oci_core_route_table" "firewall_subnet_rt" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.hub_vcn.id
display_name = "firewall-subnet-route-table"
# Outbound internet traffic from the firewall goes to IGW
route_rules {
destination = "0.0.0.0/0"
destination_type = "CIDR_BLOCK"
network_entity_id = oci_core_internet_gateway.hub_igw.id
}
# Return traffic to spokes goes through DRG
route_rules {
destination = "10.0.0.0/8"
destination_type = "CIDR_BLOCK"
network_entity_id = oci_core_drg.hub_drg.id
}
}
# DRG route table - forces all spoke traffic toward the firewall
resource "oci_core_drg_route_table" "drg_spoke_rt" {
drg_id = oci_core_drg.hub_drg.id
display_name = "drg-spoke-inspection-rt"
is_ecmp_enabled = false
import_drg_route_distribution_id = oci_core_drg_route_distribution.hub_distribution.id
}
resource "oci_core_drg_route_table_route_rule" "default_to_firewall" {
drg_route_table_id = oci_core_drg_route_table.drg_spoke_rt.id
destination = "0.0.0.0/0"
destination_type = "CIDR_BLOCK"
next_hop_drg_attachment_id = oci_core_drg_attachment.hub_vcn_attachment.id
}

The DRG route table sends all traffic from spokes into the hub VCN attachment. The hub VCN’s route tables then redirect that traffic to the firewall’s private IP before it exits to the internet or to another spoke.

Step 6: Attaching Spoke VCNs

Each spoke VCN attaches to the DRG and assigns the inspection route table to that attachment so traffic from the spoke is steered through the firewall.

hcl

resource "oci_core_drg_attachment" "spoke1_attachment" {
drg_id = oci_core_drg.hub_drg.id
display_name = "spoke1-vcn-attachment"
network_details {
id = var.spoke1_vcn_id
type = "VCN"
}
drg_route_table_id = oci_core_drg_route_table.drg_spoke_rt.id
}
resource "oci_core_drg_attachment" "spoke2_attachment" {
drg_id = oci_core_drg.hub_drg.id
display_name = "spoke2-vcn-attachment"
network_details {
id = var.spoke2_vcn_id
type = "VCN"
}
drg_route_table_id = oci_core_drg_route_table.drg_spoke_rt.id
}

Each spoke VCN also needs a local route rule pointing its default gateway to the DRG:

hcl

# This goes in each spoke VCN's subnet route table
route_rules {
destination = "0.0.0.0/0"
destination_type = "CIDR_BLOCK"
network_entity_id = oci_core_drg.hub_drg.id
}

Step 7: Enable Firewall Logging

Without logs you cannot verify the firewall is working, investigate blocked traffic, or tune your rules. OCI Network Firewall supports three log types: Traffic logs, Threat logs, and Traffic Insights logs. Enable all three.

hcl

resource "oci_logging_log_group" "firewall_log_group" {
compartment_id = var.compartment_id
display_name = "network-firewall-logs"
}
resource "oci_logging_log" "firewall_traffic_log" {
display_name = "firewall-traffic"
log_group_id = oci_logging_log_group.firewall_log_group.id
log_type = "SERVICE"
configuration {
source {
category = "traffic"
resource = oci_network_firewall_network_firewall.hub_firewall.id
service = "oci-network-firewall"
source_type = "OCISERVICE"
}
compartment_id = var.compartment_id
}
retention_duration = 60
is_enabled = true
}
resource "oci_logging_log" "firewall_threat_log" {
display_name = "firewall-threats"
log_group_id = oci_logging_log_group.firewall_log_group.id
log_type = "SERVICE"
configuration {
source {
category = "threat"
resource = oci_network_firewall_network_firewall.hub_firewall.id
service = "oci-network-firewall"
source_type = "OCISERVICE"
}
compartment_id = var.compartment_id
}
retention_duration = 60
is_enabled = true
}

Validating the Configuration

Once deployed, validate traffic routing before declaring the rollout complete.

First, verify a connection from a spoke instance to an allowed destination reaches it without being dropped:

bash

# From a compute instance in spoke VCN 1
curl -v https://objectstorage.me-jeddah-1.oraclecloud.com/healthcheck

Then check the firewall traffic log in OCI Logging to confirm the connection was seen and allowed:

bash

oci logging-search search-logs \
--search-query 'search "ocid1.compartment.oc1..yourcompartmentocid/oci-network-firewall/firewall-traffic" | where data.action = '"'"'ALLOW'"'"'' \
--time-start "$(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%SZ)" \
--time-end "$(date -u +%Y-%m-%dT%H:%M:%SZ)"

Test the block rule by attempting to reach a prohibited URL:

bash

curl -v https://pastebin.com

This should time out. The threat log should contain an entry with action: DROP and the destination matching your blocked URL list.

To test IDPS, you can use a safe test signature. The EICAR test string sent over HTTP triggers a detection event without any real malicious activity:

bash

curl -v http://any-http-server/path?test=X5O!P%40AP%5B4%5C\PZX54(P%5E)7CC)7%7D%24EICAR-STANDARD-ANTIVIRUS-TEST-FILE!%24H%2BH*

If IDPS is in detection mode, the threat log will show the signature hit. If it is in prevention mode, the connection will be dropped immediately.

Policy Updates Without Downtime

One advantage of OCI Network Firewall’s policy model is that the policy is a separate resource from the firewall instance. You can build a new policy, validate it, and then update the firewall’s network_firewall_policy_id to point to the new policy. The firewall applies the new policy without a service interruption. This makes iterative rule changes safe to deploy during business hours.

hcl

# Create new policy version
resource "oci_network_firewall_network_firewall_policy" "production_policy_v2" {
compartment_id = var.compartment_id
display_name = "production-inspection-policy-v2"
}
# After validating the new policy, update the firewall to reference it
resource "oci_network_firewall_network_firewall" "hub_firewall" {
# ...existing config...
network_firewall_policy_id = oci_network_firewall_network_firewall_policy.production_policy_v2.id
}

In a GitOps pipeline, this means policy changes go through a PR, get reviewed, apply to a staging firewall first, and then the same policy OCID is referenced in the production firewall config. No manual steps, no maintenance window required for rule changes.

Where OCI Network Firewall Fits in Your Security Stack

OCI Network Firewall is not a replacement for Security Lists, NSGs, or WAF. Each operates at a different layer and serves a different purpose.

Security Lists and NSGs handle stateful L3/L4 filtering close to the resource. They are fast, simple, and the right tool for controlling which ports reach a compute instance or database.

OCI WAF sits at the edge handling HTTP/HTTPS application-layer attacks: SQL injection, XSS, bot detection, and rate limiting against your public endpoints.

OCI Network Firewall sits between those two layers handling deep packet inspection, IDPS, URL filtering, and FQDN-based egress control across all traffic flowing through your VCN topology.

Running all three in the same environment is not redundancy for its own sake. Each catches a class of threat the others cannot. Security Lists catch misconfigured port exposure. WAF catches web application exploits at the edge. Network Firewall catches lateral movement, data exfiltration attempts, and known malware signatures in traffic that has already passed the perimeter.

Regards,
Osama


LinkedIn Hashtags:

#OCI #OracleCloud #NetworkFirewall #CloudSecurity #NGFW #IDPS #Terraform #InfrastructureAsCode #IaC #CloudNetworking #DevSecOps #OracleCloudInfrastructure #HubAndSpoke #NetworkSecurity #CloudArchitecture #PlatformEngineering #ThreatDetection #CloudNative #ZeroTrust #TechBlog

I need another blog for OCI

Searched the web

Searched the web

Clear picture now. OCI DevOps Service (the full CI/CD pipeline service with build pipelines, deployment pipelines, artifact registry, and OKE/Functions deployment) has never been covered as a dedicated post. That is the topic.


OCI DevOps: Building a Production CI/CD Pipeline with Terraform

Most teams running workloads on OCI manage their deployments through a mix of external tools: GitHub Actions pushing to OKE, Jenkins deploying to compute instances, manual Terraform runs triggered from a developer’s laptop. This works until it does not. The audit trail is scattered, secrets flow through CI runners that may not be in your VCN, and there is no native integration between the deployment tooling and the OCI IAM model that controls the infrastructure.

OCI DevOps is Oracle’s native CI/CD service. It covers source code mirroring, build pipelines, artifact management, and deployment pipelines to OKE, compute instances, Functions, and other targets. Everything runs inside your tenancy, authenticates through IAM Dynamic Groups and policies, and integrates natively with OCI Vault for secrets, OCI Container Registry for images, and OCI Artifact Registry for generic artifacts.

In this post I will build a complete pipeline from source code mirror through build, test, image push, and deployment to an OKE cluster, using Terraform for all infrastructure and a real application for the pipeline to deploy.

Service Architecture

OCI DevOps has five main components that work together.

The Project is the top-level container. It groups all related resources: code repositories, build pipelines, deployment pipelines, and environments.

Code Repositories mirror external Git repositories (GitHub, GitLab, Bitbucket) or host code natively inside OCI. Mirroring syncs on a schedule or on webhook trigger.

Build Pipelines execute build stages: managed build (runs your build spec on Oracle-managed runners), deliver artifact (pushes to Container Registry or Artifact Registry), and trigger deployment.

Artifact Registry stores generic versioned artifacts: Helm charts, Terraform modules, JAR files, and deployment manifests.

Deployment Pipelines run the actual deployment to a target environment. They support blue-green, canary, and rolling deployment strategies with built-in approval gates.

Step 1: IAM Setup

OCI DevOps needs a Dynamic Group that matches the build and deployment pipeline resources, and a policy that grants them the permissions to do their work.

hcl

resource "oci_identity_dynamic_group" "devops_build_dg" {
compartment_id = var.tenancy_ocid
name = "devops-build-pipelines"
description = "Dynamic group for OCI DevOps build pipeline runners"
matching_rule = "All {resource.type = 'devopsbuildpipeline', resource.compartment.id = '${var.compartment_id}'}"
}
resource "oci_identity_dynamic_group" "devops_deploy_dg" {
compartment_id = var.tenancy_ocid
name = "devops-deploy-pipelines"
description = "Dynamic group for OCI DevOps deployment pipelines"
matching_rule = "All {resource.type = 'devopsdeploypipeline', resource.compartment.id = '${var.compartment_id}'}"
}
resource "oci_identity_policy" "devops_policy" {
compartment_id = var.compartment_id
name = "devops-pipeline-policy"
description = "Permissions for OCI DevOps build and deploy pipelines"
statements = [
# Build pipelines need to read secrets and push to container registry
"Allow dynamic-group devops-build-pipelines to manage repos in compartment id ${var.compartment_id}",
"Allow dynamic-group devops-build-pipelines to read secret-family in compartment id ${var.compartment_id}",
"Allow dynamic-group devops-build-pipelines to manage artifacts in compartment id ${var.compartment_id}",
"Allow dynamic-group devops-build-pipelines to manage devops-family in compartment id ${var.compartment_id}",
# Deploy pipelines need to manage OKE workloads and read artifacts
"Allow dynamic-group devops-deploy-pipelines to manage cluster-family in compartment id ${var.compartment_id}",
"Allow dynamic-group devops-deploy-pipelines to use artifacts in compartment id ${var.compartment_id}",
"Allow dynamic-group devops-deploy-pipelines to manage devops-family in compartment id ${var.compartment_id}",
"Allow dynamic-group devops-deploy-pipelines to read secret-family in compartment id ${var.compartment_id}"
]
}

Step 2: Create the DevOps Project

hcl

resource "oci_devops_project" "orders_api_project" {
compartment_id = var.compartment_id
name = "orders-api"
description = "CI/CD pipeline for the orders API service"
notification_config {
topic_id = oci_ons_notification_topic.devops_alerts.id
}
defined_tags = {
"Operations.Environment" = "production"
"Operations.ManagedBy" = "terraform"
}
}
resource "oci_ons_notification_topic" "devops_alerts" {
compartment_id = var.compartment_id
name = "devops-pipeline-alerts"
description = "Notifications for DevOps pipeline events"
}
resource "oci_ons_subscription" "devops_email" {
compartment_id = var.compartment_id
topic_id = oci_ons_notification_topic.devops_alerts.id
protocol = "EMAIL"
endpoint = var.devops_alert_email
}

Step 3: Mirror the GitHub Repository

OCI DevOps can mirror a GitHub repository and trigger a build pipeline on push events. The mirror keeps a copy of the source inside OCI so builds do not depend on external connectivity to GitHub at build time.

hcl

resource "oci_devops_repository" "orders_api_repo" {
project_id = oci_devops_project.orders_api_project.id
name = "orders-api"
description = "Mirror of GitHub orders-api repository"
repository_type = "MIRRORED"
default_branch = "main"
mirror_repository_config {
repository_url = "https://github.com/your-org/orders-api.git"
connector_id = oci_devops_connection.github_connection.id
trigger_schedule {
schedule_type = "CUSTOM"
custom_schedule = "0 */6 * * *"
}
}
}
resource "oci_devops_connection" "github_connection" {
project_id = oci_devops_project.orders_api_project.id
display_name = "github-connection"
connection_type = "GITHUB_ACCESS_TOKEN"
description = "Connection to GitHub using PAT stored in OCI Vault"
access_token = oci_vault_secret.github_pat.id
}
resource "oci_vault_secret" "github_pat" {
compartment_id = var.compartment_id
vault_id = var.vault_id
key_id = var.vault_key_id
secret_name = "github-pat-devops"
secret_content {
content_type = "BASE64"
content = base64encode(var.github_personal_access_token)
}
}

The GitHub PAT is stored in OCI Vault, not in a Terraform variable or environment variable on a CI runner. The build pipeline retrieves it at runtime using the Dynamic Group policy.

Step 4: Build Spec

The build spec is a YAML file committed to your repository at build_spec.yaml. It defines the steps the managed build runner executes.

yaml

version: 0.1
component: build
timeoutInSeconds: 1800
env:
exportedVariables:
- BUILDRUN_HASH
steps:
- type: Command
name: Set build hash
command: |
export BUILDRUN_HASH=$(echo ${OCI_BUILD_RUN_ID} | tail -c 8)
echo "BUILDRUN_HASH: ${BUILDRUN_HASH}"
- type: Command
name: Install dependencies
command: |
cd orders-api
pip install -r requirements.txt --quiet
- type: Command
name: Run unit tests
command: |
cd orders-api
python -m pytest tests/unit/ -v --tb=short --junitxml=test-results.xml
if [ $? -ne 0 ]; then
echo "Unit tests failed. Aborting build."
exit 1
fi
- type: Command
name: Run security scan
command: |
pip install bandit --quiet
cd orders-api
bandit -r src/ -f json -o bandit-report.json -ll
if [ $? -eq 1 ]; then
echo "High severity security issues found. Aborting build."
exit 1
fi
- type: Command
name: Build container image
command: |
cd orders-api
IMAGE_TAG="${CONTAINER_REGISTRY}/${NAMESPACE}/orders-api:${BUILDRUN_HASH}"
docker build -t orders-api:latest -t ${IMAGE_TAG} .
echo "IMAGE_TAG=${IMAGE_TAG}" >> ${OCI_PRIMARY_SOURCE_DIR}/build_output.env
- type: Command
name: Push image to OCI Container Registry
command: |
docker push ${IMAGE_TAG}
outputArtifacts:
- name: orders-api-image
type: DOCKER_IMAGE
location: ${IMAGE_TAG}
- name: kubernetes-manifests
type: BINARY
location: ${OCI_PRIMARY_SOURCE_DIR}/orders-api/k8s/

The security scan step uses Bandit to flag high-severity Python security issues and fails the build if any are found. This happens before the image is built, not after.

Step 5: Build Pipeline

hcl

resource "oci_devops_build_pipeline" "orders_api_build" {
project_id = oci_devops_project.orders_api_project.id
display_name = "orders-api-build"
description = "Build, test, scan, and push the orders API container image"
build_pipeline_parameters {
items {
name = "CONTAINER_REGISTRY"
default_value = "${var.oci_region_key}.ocir.io"
description = "OCI Container Registry endpoint"
}
items {
name = "NAMESPACE"
default_value = var.tenancy_namespace
description = "OCI tenancy namespace for Container Registry"
}
}
}
# Stage 1: Managed Build
resource "oci_devops_build_pipeline_stage" "managed_build" {
build_pipeline_id = oci_devops_build_pipeline.orders_api_build.id
display_name = "managed-build"
description = "Execute build spec on managed runner"
build_pipeline_stage_type = "BUILD"
build_spec_file = "build_spec.yaml"
stage_execution_timeout_in_seconds = 1800
image = "OL7_X86_64_STANDARD_10"
build_source_collection {
items {
connection_type = "DEVOPS_CODE_REPOSITORY"
repository_id = oci_devops_repository.orders_api_repo.id
name = "orders-api"
branch = "main"
repository_url = oci_devops_repository.orders_api_repo.http_url
}
}
build_pipeline_stage_predecessor_collection {
items {
id = oci_devops_build_pipeline.orders_api_build.id
}
}
}
# Stage 2: Deliver Artifact to Container Registry
resource "oci_devops_build_pipeline_stage" "deliver_artifact" {
build_pipeline_id = oci_devops_build_pipeline.orders_api_build.id
display_name = "deliver-artifact"
description = "Push built image to OCI Container Registry"
build_pipeline_stage_type = "DELIVER_ARTIFACT"
deliver_artifact_collection {
items {
artifact_name = "orders-api-image"
artifact_id = oci_devops_deploy_artifact.orders_api_image.id
}
items {
artifact_name = "kubernetes-manifests"
artifact_id = oci_devops_deploy_artifact.k8s_manifests.id
}
}
build_pipeline_stage_predecessor_collection {
items {
id = oci_devops_build_pipeline_stage.managed_build.id
}
}
}
# Stage 3: Trigger Deployment Pipeline
resource "oci_devops_build_pipeline_stage" "trigger_deploy" {
build_pipeline_id = oci_devops_build_pipeline.orders_api_build.id
display_name = "trigger-deployment"
description = "Trigger the deployment pipeline on successful build"
build_pipeline_stage_type = "TRIGGER_DEPLOYMENT_PIPELINE"
deploy_pipeline_id = oci_devops_deploy_pipeline.orders_api_deploy.id
is_pass_all_parameters_enabled = true
build_pipeline_stage_predecessor_collection {
items {
id = oci_devops_build_pipeline_stage.deliver_artifact.id
}
}
}

Step 6: Artifact Registry

hcl

resource "oci_artifacts_repository" "k8s_manifests_repo" {
compartment_id = var.compartment_id
display_name = "orders-api-manifests"
description = "Kubernetes deployment manifests for orders API"
is_immutable = false
repository_type = "GENERIC"
}
resource "oci_devops_deploy_artifact" "orders_api_image" {
project_id = oci_devops_project.orders_api_project.id
display_name = "orders-api-container-image"
argument_substitution_mode = "SUBSTITUTE_PLACEHOLDERS"
deploy_artifact_type = "DOCKER_IMAGE"
deploy_artifact_source {
deploy_artifact_source_type = "OCIR"
image_uri = "${var.oci_region_key}.ocir.io/${var.tenancy_namespace}/orders-api:$${BUILDRUN_HASH}"
image_digest = " "
}
}
resource "oci_devops_deploy_artifact" "k8s_manifests" {
project_id = oci_devops_project.orders_api_project.id
display_name = "orders-api-k8s-manifests"
argument_substitution_mode = "SUBSTITUTE_PLACEHOLDERS"
deploy_artifact_type = "KUBERNETES_MANIFEST"
deploy_artifact_source {
deploy_artifact_source_type = "GENERIC_ARTIFACT"
repository_id = oci_artifacts_repository.k8s_manifests_repo.id
deploy_artifact_path = "k8s/deployment.yaml"
deploy_artifact_version = "$${BUILDRUN_HASH}"
}
}

Step 7: Deployment Environment and Pipeline

The deployment pipeline targets the OKE cluster. Define the environment first, then the pipeline stages.

hcl

resource "oci_devops_deploy_environment" "oke_prod" {
project_id = oci_devops_project.orders_api_project.id
display_name = "oke-production"
description = "Production OKE cluster"
deploy_environment_type = "OKE_CLUSTER"
cluster_id = var.oke_cluster_id
}
resource "oci_devops_deploy_pipeline" "orders_api_deploy" {
project_id = oci_devops_project.orders_api_project.id
display_name = "orders-api-deploy"
description = "Blue-green deployment of orders API to production OKE"
deploy_pipeline_parameters {
items {
name = "NAMESPACE"
default_value = "orders"
description = "Kubernetes namespace for the deployment"
}
items {
name = "IMAGE_TAG"
default_value = "latest"
description = "Container image tag to deploy"
}
}
}
# Stage 1: Approval gate before production deployment
resource "oci_devops_deploy_stage" "approval_gate" {
deploy_pipeline_id = oci_devops_deploy_pipeline.orders_api_deploy.id
display_name = "production-approval"
description = "Manual approval required before deploying to production"
deploy_stage_type = "MANUAL_APPROVAL"
approval_policy {
approval_policy_type = "COUNT_BASED_APPROVAL"
number_of_approvals_required = 1
}
deploy_stage_predecessor_collection {
items {
id = oci_devops_deploy_pipeline.orders_api_deploy.id
}
}
}
# Stage 2: Blue-green deploy to OKE
resource "oci_devops_deploy_stage" "oke_blue_green_deploy" {
deploy_pipeline_id = oci_devops_deploy_pipeline.orders_api_deploy.id
display_name = "oke-blue-green-deploy"
description = "Deploy new version to green environment"
deploy_stage_type = "OKE_BLUE_GREEN_DEPLOYMENT"
oke_blue_green_deploy_stage_details {
kubernetes_manifest_deploy_artifact_ids = [
oci_devops_deploy_artifact.k8s_manifests.id
]
oke_cluster_deploy_environment_id = oci_devops_deploy_environment.oke_prod.id
blue_green_strategy {
strategy_type = "NGINX_INGRESS_STRATEGY"
namespace_a = "orders-blue"
namespace_b = "orders-green"
ingress_name = "orders-api-ingress"
}
}
deploy_stage_predecessor_collection {
items {
id = oci_devops_deploy_stage.approval_gate.id
}
}
}
# Stage 3: Traffic shift after successful deployment validation
resource "oci_devops_deploy_stage" "traffic_shift" {
deploy_pipeline_id = oci_devops_deploy_pipeline.orders_api_deploy.id
display_name = "shift-traffic-to-green"
description = "Shift 100% of traffic to the newly deployed green environment"
deploy_stage_type = "OKE_BLUE_GREEN_TRAFFIC_SHIFT"
oke_blue_green_traffic_shift_deploy_stage_details {
oke_blue_green_deployment_deploy_stage_id = oci_devops_deploy_stage.oke_blue_green_deploy.id
}
deploy_stage_predecessor_collection {
items {
id = oci_devops_deploy_stage.oke_blue_green_deploy.id
}
}
}

Step 8: Trigger on Code Push

The trigger watches the mirrored repository and fires the build pipeline when a push lands on the main branch.

hcl

resource "oci_devops_trigger" "main_branch_push" {
project_id = oci_devops_project.orders_api_project.id
display_name = "main-branch-push-trigger"
description = "Trigger build pipeline on every push to main"
trigger_source = "DEVOPS_CODE_REPOSITORY"
repository_id = oci_devops_repository.orders_api_repo.id
actions {
type = "TRIGGER_BUILD_PIPELINE"
build_pipeline_id = oci_devops_build_pipeline.orders_api_build.id
filter {
trigger_source = "DEVOPS_CODE_REPOSITORY"
events = ["PUSH"]
include {
head_ref = "main"
}
exclude {
file_filter {
file_paths = ["docs/*", "*.md", ".github/*"]
}
}
}
}
}

The exclude block prevents documentation-only changes from triggering a full build and deploy. Pushing a README update does not kick off the pipeline.

Step 9: Verifying the Pipeline

Once Terraform applies, validate the end-to-end flow.

Check mirror sync status:

bash

oci devops repository get \
--repository-id <your-repo-ocid> \
--query 'data.{name:name, mirror-status:"mirror-repository-config"}' \
--output table

Manually trigger a build to test without waiting for a push:

bash

oci devops build-run create \
--build-pipeline-id <your-build-pipeline-ocid> \
--display-name "manual-validation-run" \
--build-run-arguments '{"items": [{"name": "IMAGE_TAG", "value": "validation-test"}]}'

Watch the build run progress:

bash

oci devops build-run get \
--build-run-id <build-run-ocid> \
--query 'data.{status:"lifecycle-state", phase:"build-run-progress"."build-pipeline-stage-run-progress"}' \
--output table

List deployment history to confirm deployments are being tracked:

bash

oci devops deployment list \
--project-id <project-ocid> \
--sort-by timeCreated \
--sort-order DESC \
--limit 10 \
--query 'data.items[*].{name:"display-name", status:"lifecycle-state", time:\"time-created\"}' \
--output table

Rollback

If a deployment introduces a regression, OCI DevOps blue-green makes rollback immediate. Traffic is still flowing to the old environment until the traffic shift stage completes. If you catch the issue before the shift, simply reject the traffic shift stage from the console or CLI:

bash

oci devops deployment approve \
--deployment-id <deployment-ocid> \
--deploy-stage-id <traffic-shift-stage-ocid> \
--reason "Rolling back: latency regression detected in green environment" \
--action REJECT

The green environment is torn down, the blue environment continues serving traffic, and the deployment is marked as failed with the reason recorded in the audit log.

Where This Fits in a Real Team

The value of OCI DevOps over an external CI/CD tool is not raw feature count. GitHub Actions or GitLab CI have richer marketplace ecosystems. The value is native IAM integration and residency inside your tenancy.

Build runners authenticate to OCI Vault, Container Registry, and Artifact Registry using the Dynamic Group policy with no credentials stored on a third-party platform. Every build and deployment is recorded in OCI Audit with the OCID of the pipeline that ran it. Deployment approvals are logged against the OCI user who approved or rejected them. For regulated environments where you need to prove that every production change was approved by a named human identity and executed by an automated system with least-privilege credentials, OCI DevOps gives you that audit trail natively.

For teams already running everything inside OCI, it is the most operationally coherent choice.

Regards,
Osama


LinkedIn Hashtags:

#OCI #OracleCloud #OracleDevOps #CICD #DevOps #Terraform #OKE #BlueGreen #InfrastructureAsCode #IaC #CloudNative #PlatformEngineering #ContainerRegistry #GitOps #OracleCloudInfrastructure #Kubernetes #CloudSecurity #CloudArchitecture #DeploymentAutomation #TechBlog

I need another OCI Blogs

Searched the web

Searched the web

Good. The site covers: OKE, Functions, Streaming, API Gateway, Vault, Cloud Guard, NSGs, Bastion, Service Gateway, Network Firewall, DRCC, DevOps pipelines, Resource Manager/GitOps, Autonomous Database, Data Flow, Data Integration, Logging Analytics, Monitoring, DR, Cost Management, AI/ML services, and Multi-Cloud. The one clear gap at the infrastructure layer is OCI Compute Autoscaling with Instance Pools and Custom Metrics — no dedicated post exists. That is the topic.


OCI Instance Pools and Autoscaling: Building a Production-Grade Compute Scaling Architecture with Terraform

Vertical scaling on OCI is straightforward: stop the instance, change the shape, start it again. It works but it does not solve the problem you face at 9am on a Monday when traffic doubles in ten minutes and you need twenty more instances, not one bigger one. That is horizontal scaling, and doing it properly on OCI requires understanding how Instance Configurations, Instance Pools, and Autoscaling Configurations work together.

Most teams get to instance pools quickly. They read the docs, create a pool with a fixed size, and think they are done. What they miss is the autoscaling layer on top, the load balancer backend set attachment that makes the pool actually serve traffic, the health check configuration that removes unhealthy instances before they receive requests, and the custom metric path that scales on application-level signals instead of just CPU.

This post covers all of it: the full Terraform implementation of a production autoscaling group behind a load balancer, health checks, scaling policies using both metric-based and schedule-based triggers, and custom metric publishing so you can scale on queue depth or request latency instead of raw CPU utilization.

How the Components Fit Together

Before writing any Terraform, the relationship between the three core resources matters.

An Instance Configuration is a template. It defines the compute shape, the OS image, the boot volume size, the VCN subnet placement, the cloud-init script, and any attached block volumes. The Instance Configuration itself does not run anything. It is a snapshot of how an instance should be created.

An Instance Pool uses that template to create and manage a group of identically configured instances. The pool maintains a target size, handles replacements when an instance becomes unhealthy, and integrates with the OCI Load Balancer to register and deregister instances automatically as they join or leave the pool.

An Autoscaling Configuration sits on top of the pool and adjusts the target size based on rules you define. It can scale out when CPU exceeds a threshold, scale in when it drops, and follow a fixed schedule for predictable load patterns.

Step 1: Instance Configuration

The cloud-init script inside the instance configuration is where you install your application, configure the OCI Monitoring agent for custom metrics, and register the instance with your configuration management system. Keep it idempotent.

hcl

data "oci_core_images" "ol8_image" {
compartment_id = var.compartment_id
operating_system = "Oracle Linux"
operating_system_version = "8"
shape = "VM.Standard.E4.Flex"
sort_by = "TIMECREATED"
sort_order = "DESC"
filter {
name = "display_name"
values = ["^.*Oracle-Linux-8.*$"]
regex = true
}
}
resource "oci_core_instance_configuration" "app_instance_config" {
compartment_id = var.compartment_id
display_name = "orders-api-instance-config-v${var.app_version}"
instance_details {
instance_type = "compute"
launch_details {
compartment_id = var.compartment_id
display_name = "orders-api-node"
shape = "VM.Standard.E4.Flex"
shape_config {
ocpus = 2
memory_in_gbs = 16
}
source_details {
source_type = "image"
image_id = data.oci_core_images.ol8_image.images[0].id
boot_volume_size_in_gbs = 50
}
create_vnic_details {
subnet_id = var.app_subnet_id
assign_public_ip = false
nsg_ids = [var.app_nsg_id]
hostname_label_prefix = "orders-api"
}
metadata = {
ssh_authorized_keys = var.ssh_public_key
user_data = base64encode(templatefile("${path.module}/templates/cloud-init.yaml", {
app_version = var.app_version
compartment_id = var.compartment_id
region = var.region
monitoring_enabled = "true"
}))
}
defined_tags = {
"Operations.Environment" = "production"
"Operations.Application" = "orders-api"
"Operations.ManagedBy" = "terraform"
}
}
}
}

The cloud-init template at templates/cloud-init.yaml:

yaml

#cloud-config
runcmd:
# Install OCI Unified Monitoring Agent for custom metrics
- dnf install -y oracle-cloud-agent
- systemctl enable oracle-cloud-agent
- systemctl start oracle-cloud-agent
# Install the application
- mkdir -p /opt/orders-api
- dnf install -y python3.11 python3.11-pip
- pip3.11 install orders-api==${app_version}
# Configure the application
- |
cat > /etc/orders-api/config.yaml <<EOF
environment: production
compartment_id: ${compartment_id}
region: ${region}
metrics_namespace: custom_orders_api
EOF
# Start the application
- systemctl enable orders-api
- systemctl start orders-api
write_files:
- path: /etc/systemd/system/orders-api.service
content: |
[Unit]
Description=Orders API Service
After=network.target
[Service]
Type=simple
User=app
ExecStart=/usr/local/bin/orders-api serve
Restart=always
RestartSec=5
Environment=CONFIG_FILE=/etc/orders-api/config.yaml
[Install]
WantedBy=multi-user.target

Step 2: Instance Pool

hcl

resource "oci_core_instance_pool" "orders_api_pool" {
compartment_id = var.compartment_id
instance_configuration_id = oci_core_instance_configuration.app_instance_config.id
display_name = "orders-api-pool"
size = 2
placement_configurations {
availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name
primary_subnet_id = var.app_subnet_id
fault_domains = ["FAULT-DOMAIN-1", "FAULT-DOMAIN-2", "FAULT-DOMAIN-3"]
}
placement_configurations {
availability_domain = data.oci_identity_availability_domains.ads.availability_domains[1].name
primary_subnet_id = var.app_subnet_id_ad2
fault_domains = ["FAULT-DOMAIN-1", "FAULT-DOMAIN-2", "FAULT-DOMAIN-3"]
}
load_balancers {
backend_set_name = oci_load_balancer_backend_set.orders_api_backend.name
load_balancer_id = oci_load_balancer.orders_lb.id
port = 8080
vnic_selection = "PrimaryVnic"
}
defined_tags = {
"Operations.Environment" = "production"
"Operations.Application" = "orders-api"
}
}

Two placement configurations across two availability domains with all three fault domains specified in each. This spreads instances evenly across the physical failure domains within each AD. A single hardware failure affecting one fault domain takes out at most one third of your capacity in one AD, not all of it.

The load_balancers block registers the pool with the load balancer backend set automatically. When the pool adds an instance, OCI registers it with the backend set. When it removes one, OCI deregisters it before terminating the instance so it drains connections cleanly.

Step 3: Load Balancer and Health Check

hcl

resource "oci_load_balancer" "orders_lb" {
compartment_id = var.compartment_id
display_name = "orders-api-lb"
shape = "flexible"
subnet_ids = [var.public_subnet_id]
is_private = false
shape_details {
minimum_bandwidth_in_mbps = 10
maximum_bandwidth_in_mbps = 400
}
defined_tags = {
"Operations.Environment" = "production"
"Operations.Application" = "orders-api"
}
}
resource "oci_load_balancer_backend_set" "orders_api_backend" {
name = "orders-api-backend-set"
load_balancer_id = oci_load_balancer.orders_lb.id
policy = "LEAST_CONNECTIONS"
health_checker {
protocol = "HTTP"
port = 8080
url_path = "/health"
interval_ms = 10000
timeout_in_millis = 3000
retries = 3
return_code = 200
response_body_regex = ".*\"status\":\"healthy\".*"
}
session_persistence_configuration {
cookie_name = "orders_session"
disable_fallback = false
}
}
resource "oci_load_balancer_listener" "orders_https" {
load_balancer_id = oci_load_balancer.orders_lb.id
name = "orders-https-listener"
default_backend_set_name = oci_load_balancer_backend_set.orders_api_backend.name
port = 443
protocol = "HTTP"
ssl_configuration {
certificate_name = oci_load_balancer_certificate.orders_cert.certificate_name
verify_peer_certificate = false
protocols = ["TLSv1.2", "TLSv1.3"]
cipher_suite_name = "oci-wider-compatible-ssl-cipher-suite-v1"
}
connection_configuration {
idle_timeout_in_seconds = 60
}
}

The health checker uses response_body_regex to validate the response body, not just the HTTP status code. Your /health endpoint should return a JSON payload that confirms the application is ready to serve traffic, not just that the process is running. A process can be alive but unable to connect to the database, which makes it unhealthy from a request-serving perspective even though it returns 200.

Step 4: Metric-Based Autoscaling

The default metric-based autoscaling policy uses CPU utilization. This works for CPU-bound workloads but misses the mark for I/O-bound services where CPU stays low while request queues build up.

hcl

resource "oci_autoscaling_auto_scaling_configuration" "orders_api_asc" {
compartment_id = var.compartment_id
display_name = "orders-api-autoscaling"
is_enabled = true
cool_down_in_seconds = 300
auto_scaling_resources {
id = oci_core_instance_pool.orders_api_pool.id
type = "instancePool"
}
policies {
display_name = "cpu-scale-out"
policy_type = "threshold"
capacity {
initial = 2
min = 2
max = 20
}
rules {
display_name = "scale-out-on-high-cpu"
action {
type = "CHANGE_COUNT_BY"
value = 2
}
metric {
metric_type = "CPU_UTILIZATION"
threshold {
operator = "GT"
value = 75
}
}
}
rules {
display_name = "scale-in-on-low-cpu"
action {
type = "CHANGE_COUNT_BY"
value = -1
}
metric {
metric_type = "CPU_UTILIZATION"
threshold {
operator = "LT"
value = 25
}
}
}
}
}

The cool_down_in_seconds = 300 prevents the autoscaler from firing again within five minutes of the last scaling action. Without this, a sudden traffic spike triggers a scale-out, the new instances come online, CPU drops, a scale-in fires immediately, the instances terminate, CPU climbs again, and you get an oscillation loop. Five minutes gives new instances time to warm up and take on load before the next evaluation.

Scale out by 2, scale in by 1. Always scale out faster than you scale in. The cost of having one extra instance for a few minutes is trivial compared to the cost of serving degraded traffic because you removed capacity too aggressively.

Step 5: Custom Metric Autoscaling

CPU-based scaling is not enough for most production services. A better signal is often active request queue depth or response latency percentile. If your application publishes custom metrics to OCI Monitoring, you can scale on those instead.

Here is how the application publishes a custom metric from Python:

python

import oci
import json
from datetime import datetime, timezone
def publish_queue_depth_metric(queue_depth: int, compartment_id: str):
config = oci.config.from_file()
monitoring_client = oci.monitoring.MonitoringClient(
config,
service_endpoint="https://telemetry-ingestion.{}.oraclecloud.com".format(config["region"])
)
metric_data = oci.monitoring.models.PostMetricDataDetails(
metric_data=[
oci.monitoring.models.MetricDataDetails(
namespace="custom_orders_api",
compartment_id=compartment_id,
name="RequestQueueDepth",
dimensions={
"environment": "production",
"application": "orders-api"
},
datapoints=[
oci.monitoring.models.Datapoint(
timestamp=datetime.now(timezone.utc),
value=float(queue_depth)
)
],
metadata={
"unit": "count",
"displayName": "Request Queue Depth"
}
)
]
)
response = monitoring_client.post_metric_data(
post_metric_data_details=metric_data
)
return response.status

Call this function every 60 seconds from a background thread in your application. Once the metric appears in OCI Monitoring under the custom_orders_api namespace, you can create an autoscaling rule against it.

OCI’s native autoscaling configuration only supports CPU_UTILIZATION and MEMORY_UTILIZATION as built-in metric types. To scale on a custom metric you pair OCI Monitoring alarms with an OCI Functions trigger that calls the Instance Pool resize API directly.

hcl

resource "oci_monitoring_alarm" "queue_depth_high" {
compartment_id = var.compartment_id
display_name = "orders-api-queue-depth-high"
is_enabled = true
metric_compartment_id = var.compartment_id
namespace = "custom_orders_api"
query = "RequestQueueDepth[1m]{environment = 'production'}.mean() > 500"
severity = "WARNING"
pending_duration = "PT2M"
destinations = [oci_ons_notification_topic.scaling_topic.id]
body = "Queue depth exceeded 500 for 2 minutes. Scaling out instance pool."
}
resource "oci_monitoring_alarm" "queue_depth_low" {
compartment_id = var.compartment_id
display_name = "orders-api-queue-depth-low"
is_enabled = true
metric_compartment_id = var.compartment_id
namespace = "custom_orders_api"
query = "RequestQueueDepth[5m]{environment = 'production'}.mean() < 100"
severity = "INFO"
pending_duration = "PT10M"
destinations = [oci_ons_notification_topic.scaling_topic.id]
body = "Queue depth below 100 for 10 minutes. Scaling in instance pool."
}
resource "oci_ons_notification_topic" "scaling_topic" {
compartment_id = var.compartment_id
name = "orders-api-scaling-events"
description = "Triggers custom metric scaling function"
}
resource "oci_ons_subscription" "scaling_function_sub" {
compartment_id = var.compartment_id
topic_id = oci_ons_notification_topic.scaling_topic.id
protocol = "ORACLE_FUNCTIONS"
endpoint = oci_functions_function.pool_scaler.id
}

The OCI Function that handles the scaling action:

python

import io
import json
import oci
import logging
logger = logging.getLogger()
POOL_ID = "ocid1.instancepool.oc1..."
MIN_SIZE = 2
MAX_SIZE = 20
SCALE_OUT_BY = 2
SCALE_IN_BY = 1
def handler(ctx, data: io.BytesIO = None):
try:
body = json.loads(data.getvalue())
alarm_body = body.get("body", "")
logger.info(f"Received alarm notification: {alarm_body}")
except Exception as ex:
logger.error(f"Failed to parse notification: {ex}")
return
signer = oci.auth.signers.get_resource_principals_signer()
compute_mgmt = oci.core.ComputeManagementClient(config={}, signer=signer)
pool = compute_mgmt.get_instance_pool(POOL_ID).data
current_size = pool.size
if "Scaling out" in alarm_body:
new_size = min(current_size + SCALE_OUT_BY, MAX_SIZE)
action = "scale-out"
elif "Scaling in" in alarm_body:
new_size = max(current_size - SCALE_IN_BY, MIN_SIZE)
action = "scale-in"
else:
logger.info("Unrecognized alarm body, no action taken")
return
if new_size == current_size:
logger.info(f"Already at {'max' if action == 'scale-out' else 'min'} size ({current_size}), no action")
return
update_details = oci.core.models.UpdateInstancePoolDetails(size=new_size)
compute_mgmt.update_instance_pool(POOL_ID, update_details)
logger.info(f"Pool resize triggered: {current_size} to {new_size} ({action})")

The function uses get_resource_principals_signer() to authenticate with the Dynamic Group policy. No credentials are stored in the function configuration.

Step 6: Schedule-Based Scaling

For workloads with predictable patterns you can layer a schedule-based policy on top of the metric-based policy. Business-hours applications can scale up before the working day starts and scale down after it ends, reducing idle capacity costs at night.

hcl

resource "oci_autoscaling_auto_scaling_configuration" "orders_api_schedule" {
compartment_id = var.compartment_id
display_name = "orders-api-schedule-scaling"
is_enabled = true
cool_down_in_seconds = 300
auto_scaling_resources {
id = oci_core_instance_pool.orders_api_pool.id
type = "instancePool"
}
policies {
display_name = "business-hours-scale-up"
policy_type = "scheduled"
execution_schedule {
expression = "0 7 * * 0-4"
timezone = "Asia/Riyadh"
type = "cron"
}
capacity {
initial = 6
min = 6
max = 20
}
}
policies {
display_name = "after-hours-scale-down"
policy_type = "scheduled"
execution_schedule {
expression = "0 20 * * 0-4"
timezone = "Asia/Riyadh"
type = "cron"
}
capacity {
initial = 2
min = 2
max = 20
}
}
}

The cron expression 0 7 * * 0-4 fires at 07:00 Sunday through Thursday in the Asia/Riyadh timezone, which covers the standard working week in the Gulf region. At 20:00 the pool scales back to the minimum. The max remains at 20 in both schedules so metric-based scaling can still expand beyond the scheduled minimum during peak periods.

Step 7: Rolling Instance Configuration Update

When you need to deploy a new application version, you update the Instance Configuration and then replace pool instances without taking the pool offline.

hcl

# Create new instance configuration with updated app version
resource "oci_core_instance_configuration" "app_instance_config_v2" {
compartment_id = var.compartment_id
display_name = "orders-api-instance-config-v${var.new_app_version}"
# Same configuration as v1 with updated user_data referencing new_app_version
instance_details {
instance_type = "compute"
launch_details {
# ... identical to v1 except user_data references new_app_version
}
}
}
# Update the pool to use the new configuration
resource "oci_core_instance_pool" "orders_api_pool" {
instance_configuration_id = oci_core_instance_configuration.app_instance_config_v2.id
# ... rest of pool config unchanged
}

Updating instance_configuration_id on the pool does not immediately replace running instances. Existing instances continue running with the old configuration. New instances added by scaling or manual pool resize use the new configuration. To replace all existing instances with the new version, trigger a rolling replacement using the OCI CLI:

bash

oci compute-management instance-pool-instance attach \
--instance-pool-id <pool-ocid> \
--instance-id <instance-ocid>
# Or use the softreset action to trigger a rolling replace
oci compute-management instance-pool softreset \
--instance-pool-id <pool-ocid>

The softreset action replaces instances one at a time, waiting for each new instance to pass the load balancer health check before terminating the next old instance. Zero downtime rolling deploy without any orchestration tooling.

Validating the Autoscaling Behavior

Generate artificial CPU load to test the scale-out policy:

bash

# SSH into one of the pool instances via OCI Bastion
# Then stress the CPU
stress-ng --cpu 2 --cpu-load 90 --timeout 600

Watch the pool size change in real time:

bash

watch -n 10 'oci compute-management instance-pool get \
--instance-pool-id <pool-ocid> \
--query "data.{size:size, state:\"lifecycle-state\"}" \
--output table'

List all instances currently in the pool with their health status:

bash

oci compute-management instance-pool-instance list \
--instance-pool-id <pool-ocid> \
--query 'data[*].{id:"id", state:"state", ad:"availability-domain", fault-domain:"fault-domain"}' \
--output table

Check the autoscaling activity history to see every scale event with its trigger reason:

bash

oci autoscaling auto-scaling-configuration list \
--compartment-id <compartment-ocid> \
--query 'data[*].{name:"display-name", enabled:"is-enabled"}' \
--output table

Operational Notes

A few things that matter in production but are easy to miss.

The load balancer backend set policy is set to LEAST_CONNECTIONS. This distributes new connections to the instance with the fewest active connections rather than round-robin. For APIs with variable request duration, this prevents a slow request on one instance from causing it to accumulate a backlog while other instances are idle.

Instance Configuration versioning in Terraform requires care. The configuration resource name includes the version number (app_instance_config_v${var.app_version}), which means Terraform creates a new resource rather than modifying the existing one. This preserves the old configuration so you can roll back by pointing the pool back to the previous configuration OCID if the new version has problems.

The minimum pool size of 2 placed across two availability domains means you always have at least one instance in each AD. A complete outage of one availability domain still leaves the pool functional. Set your minimum to at least 2 and spread placement across ADs for any production workload.

Regards,
Osama


LinkedIn Hashtags:

#OCI #OracleCloud #OracleCloudInfrastructure #InstancePools #Autoscaling #Terraform #InfrastructureAsCode #IaC #CloudNative #CloudArchitecture #PlatformEngineering #DevOps #ComputeScaling #CloudAutomation #OracleCompute #LoadBalancer #HighAvailability #CloudEngineering #FinOps #TechBlog

Another blog

Searched the web

Searched the web

OCI GoldenGate has never been covered as a dedicated technical post. The 2017 mention is just a brief service description in an old summary post. Topic: OCI GoldenGate: Real-Time Data Replication from On-Premises Oracle Database to OCI Autonomous Database with Terraform and CDC Pipeline.


OCI GoldenGate: Building a Real-Time CDC Pipeline from On-Premises Oracle Database to Autonomous Database

Database migration projects fail in predictable ways. The team runs an export, copies the data, imports it, and then discovers the source database kept taking writes during the transfer. The delta needs to be applied. The delta takes longer than expected. The maintenance window closes. The project slips. The next attempt includes a longer maintenance window, which the business rejects.

Change Data Capture solves this at the architecture level. Instead of taking a point-in-time copy, you stream every committed transaction from the source database to the target in real time. The target stays current continuously. When you are ready to cut over, the gap between source and target is seconds, not hours. You flip the connection string, drain in-flight requests, and the migration is done.

OCI GoldenGate is Oracle’s managed CDC and replication service. It runs the GoldenGate engine inside OCI as a fully managed deployment, handles the Extract process on the source side, moves transactions across the network, and applies them to the target using a Replicat process. In this post I will walk through configuring a full CDC pipeline from an on-premises Oracle Database 19c to OCI Autonomous Database using Terraform for the infrastructure layer and the GoldenGate REST API for pipeline configuration.

Architecture

The pipeline has three components running in sequence.

The Extract process connects to the source Oracle Database, reads the redo logs, and captures committed DML and DDL operations. It writes these to trail files inside the GoldenGate deployment.

The Distribution Path moves trail files from the source-side Extract to the OCI GoldenGate deployment over an encrypted connection. In a hybrid setup where the source database is on-premises, this path crosses the network via FastConnect or IPSec VPN.

The Replicat process reads trail files inside OCI GoldenGate and applies the transactions to Autonomous Database using the native OCI Autonomous Database connection.

On-Premises Oracle DB 19c
|
[GoldenGate Extract] (reads redo logs)
|
[Distribution Path] (encrypted, over FastConnect or VPN)
|
OCI GoldenGate Deployment
|
[Replicat Process] (applies transactions)
|
OCI Autonomous Database

Prerequisites

Before starting you need the following in place.

On the source Oracle Database: supplemental logging must be enabled at the database level and at the table level for all tables being replicated. The GoldenGate Extract user needs SELECT on the tables, access to VLOGandVLOGandVLOGFILE, and EXECUTE on DBMS_FLASHBACK.

On the network side: a FastConnect private peering or IPSec VPN between your on-premises network and your OCI VCN. The GoldenGate deployment sits in a private subnet and reaches the source database over this private connection.

On OCI: an Autonomous Database instance already provisioned, with the wallet downloaded and the admin password stored in OCI Vault.

Step 1: Source Database Preparation

Connect to the source Oracle Database as SYSDBA and run:

sql

-- Enable supplemental logging at the database level
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA;
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (PRIMARY KEY) COLUMNS;
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (UNIQUE) COLUMNS;
-- Verify supplemental logging is active
SELECT SUPPLEMENTAL_LOG_DATA_MIN,
SUPPLEMENTAL_LOG_DATA_PK,
SUPPLEMENTAL_LOG_DATA_UI
FROM V$DATABASE;
-- Create the GoldenGate capture user
CREATE USER ggadmin IDENTIFIED BY "YourStrongPassword123!";
GRANT CREATE SESSION TO ggadmin;
GRANT SELECT ANY DICTIONARY TO ggadmin;
GRANT SELECT ANY TABLE TO ggadmin;
GRANT FLASHBACK ANY TABLE TO ggadmin;
GRANT EXECUTE ON DBMS_FLASHBACK TO ggadmin;
GRANT SELECT ON SYS.V_$DATABASE TO ggadmin;
GRANT SELECT ON SYS.V_$LOG TO ggadmin;
GRANT SELECT ON SYS.V_$LOGFILE TO ggadmin;
GRANT SELECT ON SYS.V_$ARCHIVED_LOG TO ggadmin;
GRANT SELECT ON SYS.V_$LOG_HISTORY TO ggadmin;
GRANT SELECT ON SYS.V_$TRANSACTION TO ggadmin;
-- Enable supplemental logging on specific tables
-- Run this for each table you want to replicate
ALTER TABLE hr.employees ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;
ALTER TABLE hr.departments ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;
ALTER TABLE orders.order_header ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;
ALTER TABLE orders.order_lines ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNS;

The SUPPLEMENTAL LOG DATA (ALL) COLUMNS clause on each table is important. Without it, GoldenGate can only capture the changed column values, not the before-image values needed to reconstruct the full row state on the target for UPDATE and DELETE operations.

Step 2: IAM Setup for OCI GoldenGate

resource "oci_identity_dynamic_group" "goldengate_dg" {
compartment_id = var.tenancy_ocid
name = "goldengate-deployment-dg"
description = "Dynamic group for OCI GoldenGate deployments"
matching_rule = "All {resource.type = 'goldengatedeployment', resource.compartment.id = '${var.compartment_id}'}"
}
resource "oci_identity_policy" "goldengate_policy" {
compartment_id = var.compartment_id
name = "goldengate-service-policy"
description = "Permissions for OCI GoldenGate to access required services"
statements = [
"Allow dynamic-group goldengate-deployment-dg to manage secret-family in compartment id ${var.compartment_id}",
"Allow dynamic-group goldengate-deployment-dg to manage autonomous-database-family in compartment id ${var.compartment_id}",
"Allow dynamic-group goldengate-deployment-dg to manage buckets in compartment id ${var.compartment_id}",
"Allow dynamic-group goldengate-deployment-dg to manage objects in compartment id ${var.compartment_id}",
"Allow service goldengate to manage autonomous-database-family in compartment id ${var.compartment_id}",
"Allow service goldengate to read secret-family in compartment id ${var.compartment_id}"
]
}

Step 3: Store Credentials in OCI Vault

The GoldenGate deployment retrieves source and target database credentials from OCI Vault at runtime. Never pass credentials as plain text in deployment parameters.

resource "oci_vault_secret" "source_db_password" {
compartment_id = var.compartment_id
vault_id = var.vault_id
key_id = var.vault_key_id
secret_name = "goldengate-source-db-password"
secret_content {
content_type = "BASE64"
content = base64encode(var.source_db_password)
}
defined_tags = {
"Operations.ManagedBy" = "terraform"
"Operations.Environment" = "production"
}
}
resource "oci_vault_secret" "target_adb_password" {
compartment_id = var.compartment_id
vault_id = var.vault_id
key_id = var.vault_key_id
secret_name = "goldengate-target-adb-password"
secret_content {
content_type = "BASE64"
content = base64encode(var.target_adb_admin_password)
}
}
resource "oci_vault_secret" "goldengate_admin_password" {
compartment_id = var.compartment_id
vault_id = var.vault_id
key_id = var.vault_key_id
secret_name = "goldengate-admin-password"
secret_content {
content_type = "BASE64"
content = base64encode(var.goldengate_admin_password)
}
}

Step 4: Deploy the OCI GoldenGate Instance

resource "oci_golden_gate_deployment" "cdc_deployment" {
compartment_id = var.compartment_id
display_name = "prod-cdc-pipeline"
description = "Real-time CDC from on-premises Oracle 19c to Autonomous Database"
deployment_type = "OGG"
license_model = "LICENSE_INCLUDED"
subnet_id = var.private_subnet_id
is_auto_scaling_enabled = true
cpu_core_count = 2
fqdn = "goldengate.internal.example.com"
ogg_data {
admin_username = "oggadmin"
admin_password_secret_id = oci_vault_secret.goldengate_admin_password.id
deployment_name = "cdc-prod"
}
maintenance_window {
day = "SUNDAY"
start_hour = 2
}
defined_tags = {
"Operations.Environment" = "production"
"Operations.ManagedBy" = "terraform"
}
}
output "goldengate_console_url" {
value = oci_golden_gate_deployment.cdc_deployment.deployment_url
description = "URL to access the GoldenGate administration console"
}

The is_auto_scaling_enabled = true flag allows GoldenGate to scale its CPU automatically under load. Set this to true for production deployments where replication lag during peak transaction periods is a concern.

The deployment takes 10 to 15 minutes to provision. Once it is in the ACTIVE lifecycle state, proceed to create the connections.

Step 5: Create the Source and Target Connections

OCI GoldenGate uses Connection resources that abstract the database credentials and connectivity details away from the pipeline configuration.

# Source: On-premises Oracle Database 19c
resource "oci_golden_gate_connection" "source_oracle_db" {
compartment_id = var.compartment_id
display_name = "source-oracle-19c"
description = "On-premises Oracle Database 19c source for CDC"
connection_type = "ORACLE"
technology_type = "ORACLE_DATABASE"
username = "ggadmin"
password_secret_id = oci_vault_secret.source_db_password.id
# Connection string using TNS format for on-premises database
connection_string = "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=${var.source_db_host})(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=${var.source_db_service_name})))"
# Subnet for private connectivity to on-premises network
subnet_id = var.private_subnet_id
nsg_ids = [var.goldengate_nsg_id]
defined_tags = {
"Operations.Environment" = "production"
"Operations.ManagedBy" = "terraform"
}
}
# Target: OCI Autonomous Database
resource "oci_golden_gate_connection" "target_autonomous_db" {
compartment_id = var.compartment_id
display_name = "target-autonomous-db"
description = "OCI Autonomous Database target"
connection_type = "ORACLE"
technology_type = "ORACLE_AUTONOMOUS_DATABASE"
username = "admin"
password_secret_id = oci_vault_secret.target_adb_password.id
database_id = var.autonomous_database_id
# Wallet for mTLS connection to Autonomous Database
wallet_secret_id = oci_vault_secret.adb_wallet_secret.id
subnet_id = var.private_subnet_id
nsg_ids = [var.goldengate_nsg_id]
}
# Assignment: attach connections to the deployment
resource "oci_golden_gate_deployment_backup" "source_assignment" {
# Connections are assigned via the GoldenGate console or REST API after creation
# Terraform manages the connection resources; assignment happens through the console
}

After the connections are created in Terraform, assign them to the deployment through the GoldenGate administration console or the REST API:

# Get the deployment console URL
CONSOLE_URL=$(terraform output -raw goldengate_console_url)
# Authenticate against the GoldenGate admin API
TOKEN=$(curl -s -X POST "${CONSOLE_URL}/services/v2/authtokens" \
-H "Content-Type: application/json" \
-d '{"username":"oggadmin","password":"'${GOLDENGATE_ADMIN_PASSWORD}'"}' \
| jq -r '.authToken')
echo "GoldenGate auth token obtained: ${TOKEN:0:20}..."

Step 6: Configure the Extract Process

The Extract process connects to the source database and reads the redo logs. Configure it through the GoldenGate REST API.

# Create the Extract process via REST API
curl -s -X POST "${CONSOLE_URL}/services/v2/extracts" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "EXTSRC",
"type": "INTEGRATED",
"credentials": "source-oracle-19c",
"beginTime": "NOW",
"integrated": {
"threadCount": 4,
"maxSGASize": 1024,
"logRetentionHours": 24
}
}'
# Add the source tables to the Extract
curl -s -X POST "${CONSOLE_URL}/services/v2/extracts/EXTSRC/tables" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"tables": [
{"schema": "HR", "table": "EMPLOYEES"},
{"schema": "HR", "table": "DEPARTMENTS"},
{"schema": "ORDERS", "table": "ORDER_HEADER"},
{"schema": "ORDERS", "table": "ORDER_LINES"}
]
}'
# Add a trail file for the Extract output
curl -s -X POST "${CONSOLE_URL}/services/v2/extracts/EXTSRC/trailfiles" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"trailName": "lt",
"trailSize": 500
}'Step 7: Configure the Replicat Process
The Replicat reads trail files and applies transactions to Autonomous Database. Use the PARALLEL Replicat type for better throughput on high-volume schemas.

Step 7: Configure the Replicat Process

The Replicat reads trail files and applies transactions to Autonomous Database. Use the PARALLEL Replicat type for better throughput on high-volume schemas.

# Create the Replicat process
curl -s -X POST "${CONSOLE_URL}/services/v2/replicats" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "REPADB",
"type": "PARALLEL",
"credentials": "target-autonomous-db",
"trailFile": "lt",
"parallel": {
"threads": 4,
"commitBatchSize": 1000,
"maxTransactions": 100
},
"conflictResolution": {
"updateMissing": "DISCARD",
"deleteMissing": "DISCARD",
"insertDuplicate": "UPDATE"
}
}'
# Map source tables to target tables
curl -s -X POST "${CONSOLE_URL}/services/v2/replicats/REPADB/mappings" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"mappings": [
{
"source": {"schema": "HR", "table": "EMPLOYEES"},
"target": {"schema": "HR", "table": "EMPLOYEES"}
},
{
"source": {"schema": "HR", "table": "DEPARTMENTS"},
"target": {"schema": "HR", "table": "DEPARTMENTS"}
},
{
"source": {"schema": "ORDERS", "table": "ORDER_HEADER"},
"target": {"schema": "ORDERS", "table": "ORDER_HEADER"}
},
{
"source": {"schema": "ORDERS", "table": "ORDER_LINES"},
"target": {"schema": "ORDERS", "table": "ORDER_LINES"}
}
]
}'

The conflictResolution settings define what happens when the Replicat encounters a conflict between the incoming transaction and the current state of the target. insertDuplicate: UPDATE means if a row being inserted already exists on the target, update it instead of raising an error. This handles the case where your initial load and the CDC stream overlap and the same row arrives twice.

Step 8: Initial Load Before Starting CDC

Before starting the Extract and Replicat, you need to populate the target tables with the current data from the source. Do this with a coordinated approach that captures a consistent SCN from the source

-- On the source database, capture the current SCN
-- This will be used as the starting point for the Extract
SELECT CURRENT_SCN FROM V$DATABASE;
-- Note this SCN: for example, 8573921
-- Export data consistent as of this SCN using Data Pump
expdp system/password@source_db \
DIRECTORY=dp_dir \
DUMPFILE=initial_load.dmp \
LOGFILE=initial_load.log \
SCHEMAS=HR,ORDERS \
FLASHBACK_SCN=8573921
-- Import into Autonomous Database
impdp admin/password@adb_high \
DIRECTORY=data_pump_dir \
DUMPFILE=initial_load.dmp \
LOGFILE=initial_load_import.log \
REMAP_SCHEMA=HR:HR \
REMAP_SCHEMA=ORDERS:ORDERS \
TABLE_EXISTS_ACTION=REPLACE

Once the import completes, configure the Extract to start from the SCN you captured:

# Start the Extract from the SCN captured before the Data Pump export
curl -s -X POST "${CONSOLE_URL}/services/v2/extracts/EXTSRC/start" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"startPosition": {
"type": "SCN",
"scn": "8573921"
}
}'
# Start the Replicat
curl -s -X POST "${CONSOLE_URL}/services/v2/replicats/REPADB/start" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{}'

GoldenGate will begin reading from SCN 8573921 and apply every committed transaction that occurred after the Data Pump export snapshot. The target database catches up to the source in minutes depending on transaction volume since the export.

Step 9: Monitoring the Pipeline

Check Extract and Replicat status and lag:

# Get all process statuses
curl -s "${CONSOLE_URL}/services/v2/extracts/EXTSRC" \
-H "Authorization: Bearer ${TOKEN}" \
| jq '{
status: .status,
lag: .lagSeconds,
extractedSCN: .currentSCN,
lastCheckpoint: .lastCheckpointTime
}'
curl -s "${CONSOLE_URL}/services/v2/replicats/REPADB" \
-H "Authorization: Bearer ${TOKEN}" \
| jq '{
status: .status,
lag: .lagSeconds,
appliedSCN: .currentSCN,
appliedTransactions: .totalTransactions
}'

A healthy pipeline shows lag in single-digit seconds for low to moderate transaction volumes. If lag starts growing, check the Replicat thread count, the commit batch size, and whether the Autonomous Database OCPUs are being throttled.

For production monitoring, create an OCI alarm against the GoldenGate service metrics:

resource "oci_monitoring_alarm" "goldengate_lag_alarm" {
compartment_id = var.compartment_id
display_name = "goldengate-high-lag"
is_enabled = true
metric_compartment_id = var.compartment_id
namespace = "oci_goldengate"
query = "ReplicatLag[5m]{deploymentName = 'cdc-prod'}.mean() > 60"
severity = "CRITICAL"
pending_duration = "PT5M"
destinations = [var.ops_notification_topic_id]
body = "GoldenGate Replicat lag has exceeded 60 seconds for 5 minutes. Investigate pipeline health immediately."
}
resource "oci_monitoring_alarm" "goldengate_process_down" {
compartment_id = var.compartment_id
display_name = "goldengate-process-not-running"
is_enabled = true
metric_compartment_id = var.compartment_id
namespace = "oci_goldengate"
query = "ExtractStatus[1m]{deploymentName = 'cdc-prod'}.mean() < 1"
severity = "CRITICAL"
pending_duration = "PT2M"
destinations = [var.ops_notification_topic_id]
body = "GoldenGate Extract process is not running. Replication has stopped."
}

Step 10: Cutover Procedure

When you are ready to cut over the application from the source to the target database, follow this sequence.

First, verify the Replicat lag is below five seconds and stable:

curl -s "${CONSOLE_URL}/services/v2/replicats/REPADB" \
-H "Authorization: Bearer ${TOKEN}" \
| jq '.lagSeconds'

Stop the application from writing to the source database. This can be done at the load balancer level by removing backend instances from the pool, or by switching the application to read-only mode if it supports it.

Wait for the Replicat lag to reach zero and confirm the applied SCN matches the source current SCN:

# Source current SCN
sqlplus -S system/password@source_db <<EOF
SELECT CURRENT_SCN FROM V\$DATABASE;
EOF
# Replicat applied SCN
curl -s "${CONSOLE_URL}/services/v2/replicats/REPADB" \
-H "Authorization: Bearer ${TOKEN}" \
| jq '.currentSCN'

When the SCNs match, the target is fully caught up. Update your application connection string to point to the Autonomous Database, bring the application back online, and stop the GoldenGate processes.

# Stop Extract and Replicat after successful cutover
curl -s -X POST "${CONSOLE_URL}/services/v2/extracts/EXTSRC/stop" \
-H "Authorization: Bearer ${TOKEN}"
curl -s -X POST "${CONSOLE_URL}/services/v2/replicats/REPADB/stop" \
-H "Authorization: Bearer ${TOKEN}"

When GoldenGate Is the Right Tool

GoldenGate adds operational complexity that a simple Data Pump export and import does not. It is the right tool when: the source database is too large for a maintenance window export to complete within business constraints, the application cannot tolerate more than a few seconds of downtime, or you need bidirectional replication where both source and target accept writes simultaneously during the transition.

For databases under a few hundred gigabytes with a maintenance window of a few hours, Data Pump with a brief application outage is simpler and carries less operational risk. For terabyte-scale Oracle databases with SLAs that do not permit extended downtime, GoldenGate CDC is the correct architecture.

Regards,
Osama

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.