Building a Complete GitOps CI/CD Pipeline with OCI DevOps, Terraform, and OKE
Introduction
Building a Complete GitOps CI/CD Pipeline with OCI DevOps, Terraform, and OKE
Introduction
GitOps is a simple idea with powerful consequences: your Git repository is the single source of truth for both infrastructure and application state. Every change goes through a pull request. Every deployment is traceable. Every rollback is a git revert.
Most GitOps tutorials use AWS or GCP with third-party tools like ArgoCD, Flux, or Jenkins. In this post, I will build the same pipeline using OCI-native services — OCI DevOps for CI/CD, Terraform with OCI Resource Manager for infrastructure, and OKE for the runtime. No third-party CI tools, no external dependencies.
The result is a pipeline where:
- Infrastructure changes in Terraform trigger automated
planandapplyworkflows - Application code changes trigger container builds, security scans, and rolling deployments
- Every change is auditable, reversible, and requires approval
Architecture
┌─────────────────────────────────────────────────────────────────────────┐
│ GitOps Pipeline Architecture │
│ │
│ ┌──────────────┐ ┌──────────────────────────────────────────────┐ │
│ │ OCI DevOps │ │ Build Pipeline │ │
│ │ Code Repo │────▶│ │ │
│ │ │ │ ┌────────┐ ┌─────────┐ ┌──────────────┐ │ │
│ │ /app │ │ │ Build │─▶│ Test │─▶│ Push to OCIR │ │ │
│ │ /infra │ │ │ Image │ │ + Scan │ │ │ │ │
│ │ /k8s │ │ └────────┘ └─────────┘ └──────┬───────┘ │ │
│ └──────────────┘ └───────────────────────────────────┼──────────┘ │
│ │ │
│ ┌──────────────────────────────────┐│ │
│ │ Deploy Pipeline ││ │
│ │ ▼│ │
│ │ ┌──────────┐ ┌────────┐ ┌────────────┐ │
│ │ │ Approval │─▶│ Deploy │─▶│ Verify │ │
│ │ │ Gate │ │ to OKE │ │ Health │ │
│ │ └──────────┘ └────────┘ └────────────┘ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Infrastructure Pipeline │ │
│ │ │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌────────┐ ┌───────────┐ │ │
│ │ │ Terraform │─▶│ OCI Resource │─▶│ Plan │─▶│ Apply │ │ │
│ │ │ Code Change │ │ Manager │ │ Review │ │ (Approved)│ │ │
│ │ └─────────────┘ └──────────────┘ └────────┘ └───────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ OKE Cluster │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │ │
│ │ │ Staging │ │Production│ │ Ingress │ │ OCI Load │ │ │
│ │ │ Namespace│ │Namespace │ │ NGINX │ │ Balancer │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Prerequisites
- OCI account with administrator access (or appropriate IAM policies)
- OCI CLI configured with API key authentication
- Terraform 1.5+ installed locally
kubectlinstalled- A domain name (optional, for HTTPS ingress)
Step 1: Infrastructure as Code with Terraform
Project Structure
infra/
├── environments/
│ ├── staging/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ └── production/
│ ├── main.tf
│ ├── variables.tf
│ └── terraform.tfvars
├── modules/
│ ├── network/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── oke/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── devops/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── backend.tf
Network Module
# modules/network/main.tf
resource "oci_core_vcn" "main" {
compartment_id = var.compartment_id
cidr_blocks = ["10.0.0.0/16"]
display_name = "${var.environment}-vcn"
dns_label = "${var.environment}net"
}
resource "oci_core_nat_gateway" "main" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.main.id
display_name = "${var.environment}-nat-gw"
}
resource "oci_core_internet_gateway" "main" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.main.id
display_name = "${var.environment}-igw"
enabled = true
}
resource "oci_core_service_gateway" "main" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.main.id
display_name = "${var.environment}-sgw"
services {
service_id = data.oci_core_services.all_services.services[0].id
}
}
# Public subnet for load balancers
resource "oci_core_subnet" "public_lb" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.main.id
cidr_block = "10.0.20.0/24"
display_name = "${var.environment}-public-lb"
dns_label = "publb"
prohibit_public_ip_on_vnic = false
route_table_id = oci_core_route_table.public.id
security_list_ids = [oci_core_security_list.public_lb.id]
}
# Private subnet for OKE API endpoint
resource "oci_core_subnet" "oke_api" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.main.id
cidr_block = "10.0.0.0/28"
display_name = "${var.environment}-oke-api"
dns_label = "okeapi"
prohibit_public_ip_on_vnic = true
route_table_id = oci_core_route_table.private.id
security_list_ids = [oci_core_security_list.oke_api.id]
}
# Private subnet for worker nodes
resource "oci_core_subnet" "oke_workers" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.main.id
cidr_block = "10.0.10.0/24"
display_name = "${var.environment}-oke-workers"
dns_label = "okeworkers"
prohibit_public_ip_on_vnic = true
route_table_id = oci_core_route_table.private.id
security_list_ids = [oci_core_security_list.oke_workers.id]
}
# Route tables
resource "oci_core_route_table" "public" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.main.id
display_name = "${var.environment}-public-rt"
route_rules {
destination = "0.0.0.0/0"
network_entity_id = oci_core_internet_gateway.main.id
}
}
resource "oci_core_route_table" "private" {
compartment_id = var.compartment_id
vcn_id = oci_core_vcn.main.id
display_name = "${var.environment}-private-rt"
route_rules {
destination = "0.0.0.0/0"
network_entity_id = oci_core_nat_gateway.main.id
}
route_rules {
destination = data.oci_core_services.all_services.services[0].cidr_block
destination_type = "SERVICE_CIDR_BLOCK"
network_entity_id = oci_core_service_gateway.main.id
}
}
OKE Module
# modules/oke/main.tf
resource "oci_containerengine_cluster" "main" {
compartment_id = var.compartment_id
kubernetes_version = var.kubernetes_version
name = "${var.environment}-oke-cluster"
vcn_id = var.vcn_id
endpoint_config {
is_public_ip_enabled = false
subnet_id = var.api_subnet_id
nsg_ids = [oci_core_network_security_group.oke_api.id]
}
cluster_pod_network_options {
cni_type = "OCI_VCN_IP_NATIVE"
}
options {
service_lb_subnet_ids = [var.lb_subnet_id]
add_ons {
is_kubernetes_dashboard_enabled = false
is_tiller_enabled = false
}
admission_controller_options {
is_pod_security_policy_enabled = false
}
}
}
resource "oci_containerengine_node_pool" "arm_workers" {
compartment_id = var.compartment_id
cluster_id = oci_containerengine_cluster.main.id
kubernetes_version = var.kubernetes_version
name = "${var.environment}-arm-pool"
node_shape = "VM.Standard.A1.Flex"
node_shape_config {
memory_in_gbs = var.node_memory_gb
ocpus = var.node_ocpus
}
node_config_details {
size = var.node_count
placement_configs {
availability_domain = var.availability_domain
subnet_id = var.worker_subnet_id
}
node_pool_pod_network_option_details {
cni_type = "OCI_VCN_IP_NATIVE"
max_pods_per_node = 31
pod_subnet_ids = [var.pod_subnet_id]
}
}
node_source_details {
image_id = var.node_image_id
source_type = "IMAGE"
}
initial_node_labels {
key = "environment"
value = var.environment
}
}
# Output the kubeconfig generation command
output "kubeconfig_command" {
value = "oci ce cluster create-kubeconfig --cluster-id ${oci_containerengine_cluster.main.id} --file $HOME/.kube/config --region ${var.region} --token-version 2.0.0"
}
DevOps Module
# modules/devops/main.tf
resource "oci_devops_project" "main" {
compartment_id = var.compartment_id
name = "${var.environment}-devops-project"
notification_config {
topic_id = oci_ons_notification_topic.devops.id
}
}
# Code repository
resource "oci_devops_repository" "app" {
project_id = oci_devops_project.main.id
name = "application"
repository_type = "HOSTED"
default_branch = "refs/heads/main"
description = "Application source code and Kubernetes manifests"
}
# Build pipeline
resource "oci_devops_build_pipeline" "main" {
project_id = oci_devops_project.main.id
display_name = "${var.environment}-build-pipeline"
description = "Build, test, and push container images"
}
# Build stage: Build and test
resource "oci_devops_build_pipeline_stage" "build" {
build_pipeline_id = oci_devops_build_pipeline.main.id
display_name = "build-and-test"
build_pipeline_stage_type = "BUILD"
build_source_collection {
items {
connection_type = "DEVOPS_CODE_REPOSITORY"
repository_id = oci_devops_repository.app.id
branch = "main"
name = "primary"
}
}
build_spec_file = "build_spec.yaml"
image = "OL7_X86_64_STANDARD_10"
stage_execution_timeout_in_seconds = 1800
build_pipeline_stage_predecessor_collection {
items {
id = oci_devops_build_pipeline.main.id
}
}
}
# Build stage: Push to OCIR
resource "oci_devops_build_pipeline_stage" "push" {
build_pipeline_id = oci_devops_build_pipeline.main.id
display_name = "push-to-ocir"
build_pipeline_stage_type = "DELIVER_ARTIFACT"
deliver_artifact_collection {
items {
artifact_id = oci_devops_deploy_artifact.container_image.id
artifact_name = "app-image"
}
}
build_pipeline_stage_predecessor_collection {
items {
id = oci_devops_build_pipeline_stage.build.id
}
}
}
# Deploy pipeline
resource "oci_devops_deploy_pipeline" "main" {
project_id = oci_devops_project.main.id
display_name = "${var.environment}-deploy-pipeline"
description = "Deploy to OKE with approval gates"
}
# Deploy stage: Approval
resource "oci_devops_deploy_stage" "approval" {
deploy_pipeline_id = oci_devops_deploy_pipeline.main.id
display_name = "production-approval"
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.main.id
}
}
}
# Deploy stage: OKE deployment
resource "oci_devops_deploy_stage" "oke_deploy" {
deploy_pipeline_id = oci_devops_deploy_pipeline.main.id
display_name = "deploy-to-oke"
deploy_stage_type = "OKE_DEPLOYMENT"
oke_cluster_deploy_environment_id = oci_devops_deploy_environment.oke.id
kubernetes_manifest_deploy_artifact_ids = [
oci_devops_deploy_artifact.k8s_manifest.id
]
rollback_policy {
policy_type = "AUTOMATED_STAGE_ROLLBACK_POLICY"
}
deploy_stage_predecessor_collection {
items {
id = oci_devops_deploy_stage.approval.id
}
}
}
# Trigger: Run build pipeline on code push
resource "oci_devops_trigger" "push_trigger" {
project_id = oci_devops_project.main.id
display_name = "on-push-to-main"
trigger_source = "DEVOPS_CODE_REPOSITORY"
repository_id = oci_devops_repository.app.id
actions {
build_pipeline_id = oci_devops_build_pipeline.main.id
type = "TRIGGER_BUILD_PIPELINE"
filter {
trigger_source = "DEVOPS_CODE_REPOSITORY"
events = ["PUSH"]
include {
head_ref = "main"
}
}
}
}
Step 2: Build Specification
The OCI DevOps build runner uses a build_spec.yaml file (similar to a Jenkinsfile or GitHub Actions workflow):
# build_spec.yaml
version: 0.1
component: build
timeoutInSeconds: 1800
shell: bash
env:
variables:
APP_NAME: "product-api"
exportedVariables:
- IMAGE_TAG
- BUILD_HASH
steps:
- type: Command
name: "Set build variables"
command: |
export IMAGE_TAG="${OCI_BUILD_RUN_ID:0:8}"
export BUILD_HASH=$(git rev-parse --short HEAD)
echo "Building ${APP_NAME}:${IMAGE_TAG} (commit: ${BUILD_HASH})"
- type: Command
name: "Run unit tests"
command: |
go test ./... -v -coverprofile=coverage.out
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}')
echo "Test coverage: ${COVERAGE}"
# Fail if coverage is below 80%
COVERAGE_NUM=${COVERAGE%\%}
if (( $(echo "$COVERAGE_NUM < 80" | bc -l) )); then
echo "ERROR: Test coverage ${COVERAGE} is below 80% threshold"
exit 1
fi
- type: Command
name: "Run security scan"
command: |
# Install and run govulncheck
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- type: Command
name: "Build container image"
command: |
docker build \
--build-arg BUILD_HASH=${BUILD_HASH} \
--build-arg BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
-t ${APP_NAME}:${IMAGE_TAG} \
-t ${APP_NAME}:latest \
.
- type: Command
name: "Run container structure tests"
command: |
# Verify the container starts and responds to health checks
docker run -d --name test-container -p 8080:8080 \
-e DB_DSN="mock" \
${APP_NAME}:${IMAGE_TAG}
sleep 5
# Check health endpoint
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health)
if [ "$HTTP_CODE" != "200" ]; then
echo "ERROR: Health check failed with HTTP ${HTTP_CODE}"
docker logs test-container
exit 1
fi
docker stop test-container && docker rm test-container
echo "Container health check passed"
outputArtifacts:
- name: app-image
type: DOCKER_IMAGE
location: ${APP_NAME}:${IMAGE_TAG}
Step 3: Kubernetes Manifests with Kustomize
Using Kustomize allows environment-specific overlays without duplicating manifests:
k8s/
├── base/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ └── kustomization.yaml
├── overlays/
│ ├── staging/
│ │ ├── kustomization.yaml
│ │ └── patches/
│ │ └── deployment-patch.yaml
│ └── production/
│ ├── kustomization.yaml
│ └── patches/
│ └── deployment-patch.yaml
Base Manifests
# k8s/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-api
labels:
app: product-api
version: latest
spec:
replicas: 2
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: product-api
template:
metadata:
labels:
app: product-api
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: product-api
terminationGracePeriodSeconds: 30
containers:
- name: api
image: product-api:latest
ports:
- name: http
containerPort: 8080
protocol: TCP
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 10
periodSeconds: 15
failureThreshold: 3
env:
- name: ENVIRONMENT
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
envFrom:
- secretRef:
name: product-api-secrets
# k8s/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- hpa.yaml
- ingress.yaml
Production Overlay
# k8s/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
- ../../base
patches:
- path: patches/deployment-patch.yaml
images:
- name: product-api
newName: iad.ocir.io/tenancy/product-api
newTag: ${IMAGE_TAG}
replicas:
- name: product-api
count: 3
# k8s/overlays/production/patches/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-api
spec:
template:
spec:
containers:
- name: api
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "1Gi"
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: product-api
Step 4: Infrastructure Pipeline with OCI Resource Manager
OCI Resource Manager acts as a managed Terraform backend:
# Create a Resource Manager stack from Terraform code
STACK_ID=$(oci resource-manager stack create \
--compartment-id "$COMPARTMENT_ID" \
--display-name "production-infrastructure" \
--description "Production OKE cluster and networking" \
--config-source '{
"configSourceType": "DEVOPS_CONFIG_SOURCE",
"projectId": "'$DEVOPS_PROJECT_ID'",
"repositoryId": "'$REPO_ID'",
"branchName": "main",
"workingDirectory": "infra/environments/production"
}' \
--terraform-version "1.5.x" \
--variables '{
"compartment_id": "'$COMPARTMENT_ID'",
"environment": "production",
"kubernetes_version": "v1.28.2",
"node_count": 3,
"node_ocpus": 2,
"node_memory_gb": 16
}' \
--query 'data.id' --raw-output)
# Run Terraform plan
PLAN_JOB_ID=$(oci resource-manager job create-plan-job \
--stack-id "$STACK_ID" \
--display-name "plan-$(date +%Y%m%d-%H%M%S)" \
--query 'data.id' --raw-output)
# Review the plan
oci resource-manager job get-job-logs \
--job-id "$PLAN_JOB_ID" \
--query 'data.items[].message' --output table
# Apply after review
APPLY_JOB_ID=$(oci resource-manager job create-apply-job \
--stack-id "$STACK_ID" \
--execution-plan-strategy "FROM_PLAN_JOB_ID" \
--execution-plan-job-id "$PLAN_JOB_ID" \
--display-name "apply-$(date +%Y%m%d-%H%M%S)" \
--query 'data.id' --raw-output)
Step 5: Setting Up the Trigger Chain
The final piece connects everything together — a push to main triggers the build, and a successful build triggers the deployment:
# Create the build-to-deploy trigger
oci devops trigger create-devops-code-repo-trigger \
--project-id "$DEVOPS_PROJECT_ID" \
--display-name "build-and-deploy-on-push" \
--repository-id "$REPO_ID" \
--actions '[
{
"type": "TRIGGER_BUILD_PIPELINE",
"buildPipelineId": "'$BUILD_PIPELINE_ID'",
"filter": {
"triggerSource": "DEVOPS_CODE_REPOSITORY",
"events": ["PUSH"],
"include": {
"headRef": "main",
"fileFilter": {
"filePaths": ["app/**", "Dockerfile"]
}
}
}
}
]'
# Create a separate trigger for infrastructure changes
oci devops trigger create-devops-code-repo-trigger \
--project-id "$DEVOPS_PROJECT_ID" \
--display-name "infra-plan-on-push" \
--repository-id "$REPO_ID" \
--actions '[
{
"type": "TRIGGER_BUILD_PIPELINE",
"buildPipelineId": "'$INFRA_PIPELINE_ID'",
"filter": {
"triggerSource": "DEVOPS_CODE_REPOSITORY",
"events": ["PUSH"],
"include": {
"headRef": "main",
"fileFilter": {
"filePaths": ["infra/**"]
}
}
}
}
]'
This creates two independent pipelines triggered by the same repository:
- Changes to
app/**orDockerfile→ Build and deploy pipeline - Changes to
infra/**→ Terraform plan and apply pipeline
Pipeline Execution Flow
Here is what happens when a developer pushes code:
1. Developer pushes to main branch
└─▶ OCI DevOps Trigger fires
2. Build Pipeline starts
├─▶ Stage 1: Run unit tests (go test ./...)
├─▶ Stage 2: Security vulnerability scan (govulncheck)
├─▶ Stage 3: Build Docker image
├─▶ Stage 4: Container health check test
└─▶ Stage 5: Push to OCI Container Registry
3. Deploy Pipeline starts automatically
├─▶ Stage 1: Deploy to staging namespace
├─▶ Stage 2: Run integration tests against staging
├─▶ Stage 3: Manual approval gate (Slack notification)
└─▶ Stage 4: Rolling deploy to production namespace
4. Post-deployment verification
├─▶ Health check passes → Pipeline succeeds
└─▶ Health check fails → Automatic rollback
Monitoring the Pipeline
OCI DevOps Dashboard Metrics
# Get recent build runs
oci devops build-run list \
--build-pipeline-id "$BUILD_PIPELINE_ID" \
--query 'data.items[*].{
id: id,
status: "lifecycle-state",
commit: "commit-info"."commit-hash",
started: "time-created",
duration: "build-run-progress"."time-finished"
}' --output table
# Get deployment history
oci devops deployment list \
--deploy-pipeline-id "$DEPLOY_PIPELINE_ID" \
--query 'data.items[*].{
id: id,
status: "lifecycle-state",
type: "deployment-type",
created: "time-created"
}' --output table
Set Up Notifications
# Create notification topic for pipeline events
TOPIC_ID=$(oci ons topic create \
--compartment-id "$COMPARTMENT_ID" \
--name "devops-notifications" \
--query 'data."topic-id"' --raw-output)
# Subscribe Slack webhook
oci ons subscription create \
--compartment-id "$COMPARTMENT_ID" \
--topic-id "$TOPIC_ID" \
--protocol "HTTPS" \
--endpoint "https://hooks.slack.com/services/YOUR/WEBHOOK"
# Create event rule for build failures
oci events rule create \
--compartment-id "$COMPARTMENT_ID" \
--display-name "build-failure-alert" \
--is-enabled true \
--condition '{"eventType": ["com.oraclecloud.devops.buildrunfailed"]}' \
--actions '{
"actions": [{
"actionType": "ONS",
"topicId": "'$TOPIC_ID'",
"isEnabled": true
}]
}'
Cost Analysis
ResourceConfigurationMonthly CostOCI DevOpsBuild pipelines, deploymentsFree (included)OKE Control PlaneManaged KubernetesFreeWorker Nodes (3x ARM)VM.Standard.A1.Flex, 2 OCPU, 12 GB each~$0 (Always Free*)Container Registry10 GB storage~$1Resource ManagerTerraform state, runsFreeNotifications10,000 messages/month~$0Total~$1/month
*Always Free tier includes 4 ARM OCPUs and 24 GB RAM total, enough for a small cluster.
Compare this to GitHub Actions + EKS + ECR: $150–300/month for similar infrastructure.
Lessons Learned
What worked well:
- OCI DevOps triggers with file filters — Separating application and infrastructure pipelines by file path keeps things clean. No accidental infrastructure changes from app code pushes
- Resource Manager as Terraform backend — State management, locking, and drift detection are handled automatically. No S3 bucket + DynamoDB table configuration
- Approval gates — The manual approval stage with Slack notifications created a natural checkpoint. Our team caught two configuration issues during approval review
What to watch out for:
- Build runner limitations — OCI DevOps build runners are x86 only. For ARM images, use Docker buildx with
--platform linux/arm64in the build spec - Secret management — Store database credentials and API tokens in OCI Vault, not in build spec variables. Use Vault dynamic secrets for OKE pod credentials
- Deployment artifact versioning — Always use specific image tags (commit hash or build ID), never
latest. The deploy pipeline needs to know exactly which image to deploy
Conclusion
A complete GitOps pipeline on OCI — from infrastructure provisioning to application deployment — can be built entirely with OCI-native services. No Jenkins, no GitHub Actions, no external CI/CD tools. The integrated nature of OCI DevOps with OKE, Container Registry, and Resource Manager eliminates the glue code and service integrations that consume engineering time on other platforms.
The Always Free tier makes this accessible for learning, prototyping, and even small production workloads. For teams already on Oracle Cloud, adopting OCI DevOps is the path of least resistance to GitOps.
메타데이터
- post_id
- bc061a53efac
- slug
- building-a-complete-gitops-ci-cd-pipeline-with-oci-devops-terraform-and-oke-bc061a53efac
- url
- https://medium.com/@pavan4devops/building-a-complete-gitops-ci-cd-pipeline-with-oci-devops-terraform-and-oke-bc061a53efac
- canonical_url
- https://medium.com/@pavan4devops/building-a-complete-gitops-ci-cd-pipeline-with-oci-devops-terraform-and-oke-bc061a53efac
- author_url
- https://medium.com/@pavan4devops
- status
- ok
- fetched_at
- 2026-06-12 22:02:08