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.

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

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.

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.

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

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

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.

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.

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:

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:

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:

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:

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:

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

Leave a comment

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