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 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 levelALTER 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 activeSELECT SUPPLEMENTAL_LOG_DATA_MIN, SUPPLEMENTAL_LOG_DATA_PK, SUPPLEMENTAL_LOG_DATA_UIFROM V$DATABASE;-- Create the GoldenGate capture userCREATE 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 replicateALTER 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 hereresource "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 trafficresource "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 attachmentresource "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 rangesresource "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 destinationsresource "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 categoriesresource "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 trafficresource "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 rangesresource "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 enabledresource "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 patternsresource "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 ruleresource "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 configurationdata "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 Gatewayresource "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 Gatewayresource "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 firewallresource "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 tableroute_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 1curl -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 versionresource "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 itresource "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.1component: buildtimeoutInSeconds: 1800env: exportedVariables: - BUILDRUN_HASHsteps: - 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 Buildresource "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 Registryresource "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 Pipelineresource "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 deploymentresource "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 OKEresource "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 validationresource "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-configruncmd: # 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-apiwrite_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 ociimport jsonfrom datetime import datetime, timezonedef 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 ioimport jsonimport ociimport logginglogger = logging.getLogger()POOL_ID = "ocid1.instancepool.oc1..."MIN_SIZE = 2MAX_SIZE = 20SCALE_OUT_BY = 2SCALE_IN_BY = 1def 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 versionresource "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 configurationresource "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 replaceoci 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 CPUstress-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 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:
sql
-- Enable supplemental logging at the database levelALTER 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 activeSELECT SUPPLEMENTAL_LOG_DATA_MIN, SUPPLEMENTAL_LOG_DATA_PK, SUPPLEMENTAL_LOG_DATA_UIFROM V$DATABASE;-- Create the GoldenGate capture userCREATE 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 replicateALTER 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 19cresource "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 Databaseresource "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 deploymentresource "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 URLCONSOLE_URL=$(terraform output -raw goldengate_console_url)# Authenticate against the GoldenGate admin APITOKEN=$(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 APIcurl -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 Extractcurl -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 outputcurl -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 ProcessThe 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 processcurl -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 tablescurl -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 ExtractSELECT CURRENT_SCN FROM V$DATABASE;-- Note this SCN: for example, 8573921-- Export data consistent as of this SCN using Data Pumpexpdp system/password@source_db \ DIRECTORY=dp_dir \ DUMPFILE=initial_load.dmp \ LOGFILE=initial_load.log \ SCHEMAS=HR,ORDERS \ FLASHBACK_SCN=8573921-- Import into Autonomous Databaseimpdp 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 exportcurl -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 Replicatcurl -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 statusescurl -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 SCNsqlplus -S system/password@source_db <<EOFSELECT CURRENT_SCN FROM V\$DATABASE;EOF# Replicat applied SCNcurl -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 cutovercurl -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
