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.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
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
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 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.
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