GitOps-based CI/CD pipeline for Ruby applications using modern Kubernetes tools including ArgoCD, Tekton, and automated database management with StatefulSets.
This project demonstrates a complete GitOps workflow for Ruby applications with:
- GitOps Deployment: ArgoCD for automated application deployment
- CI/CD Pipeline: Tekton for build and test automation
- Database Management: PostgreSQL with StatefulSets and automated backups
- Rolling Deployments: Zero-downtime deployment strategies
- Monitoring Integration: Prometheus and Grafana for observability
Developer Push β GitHub β Tekton Pipeline β Container Registry
β
ArgoCD Sync β Kubernetes Cluster β GitOps Repository
β
Ruby Application + PostgreSQL StatefulSet
- GitOps Workflow: Declarative configuration management
- Automated Testing: Unit tests, integration tests, and security scans
- Database State Management: PostgreSQL with persistent volumes
- Rolling Deployment Strategy: Zero-downtime deployments
- Secrets Management: Kubernetes secrets with external-secrets operator
- Monitoring & Logging: Complete observability stack
- Backup & Recovery: Automated database backups to S3
- Application: Ruby on Rails 7.x
- Database: PostgreSQL 14
- Container: Docker, Kubernetes
- CI/CD: Tekton Pipelines
- GitOps: ArgoCD
- Monitoring: Prometheus, Grafana
- Storage: Persistent Volumes, S3
- Security: RBAC, Network Policies, Security Contexts
ruby-gitops-pipeline/
βββ app/
β βββ Gemfile
β βββ Dockerfile
β βββ config/
β βββ app/
β βββ db/
β βββ spec/
βββ k8s/
β βββ base/
β β βββ deployment.yaml
β β βββ service.yaml
β β βββ statefulset.yaml
β β βββ configmap.yaml
β βββ overlays/
β β βββ development/
β β βββ staging/
β β βββ production/
β βββ argocd/
β βββ application.yaml
βββ tekton/
β βββ pipelines/
β βββ tasks/
β βββ triggers/
β βββ resources/
βββ monitoring/
β βββ prometheus/
β βββ grafana/
β βββ alerts/
βββ scripts/
β βββ backup.sh
β βββ restore.sh
β βββ migrate.sh
βββ README.md
- Kubernetes cluster (1.21+)
- ArgoCD installed
- Tekton Pipelines installed
- Container registry access
- Git repository access
# Install ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Apply ArgoCD application
kubectl apply -f k8s/argocd/application.yaml
# Install Tekton Pipelines
kubectl apply -f https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml
# Install Tekton Dashboard
kubectl apply -f https://github.com/tektoncd/dashboard/releases/latest/download/tekton-dashboard-release.yaml
# Apply pipeline resources
kubectl apply -f tekton/
# Create namespace
kubectl create namespace ruby-app
# Apply Kubernetes manifests
kubectl apply -k k8s/overlays/production/
# k8s/base/statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
spec:
serviceName: postgresql
replicas: 1
selector:
matchLabels:
app: postgresql
template:
spec:
containers:
- name: postgresql
image: postgres:14
env:
- name: POSTGRES_DB
value: ruby_app_production
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-secret
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-storage
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "fast-ssd"
resources:
requests:
storage: 20Gi
# k8s/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ruby-app
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: ruby-app
template:
spec:
containers:
- name: ruby-app
image: ruby-app:latest
ports:
- containerPort: 3000
env:
- name: RAILS_ENV
value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-secret
key: url
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
- Source Checkout: Clone repository
- Dependency Installation: Bundle install
- Unit Tests: RSpec test suite
- Security Scan: Brakeman, bundle-audit
- Code Quality: RuboCop linting
- Container Build: Docker image build
- Container Scan: Trivy security scan
- Push Image: Push to container registry
- Deploy: Update GitOps repository
- Integration Tests: Post-deployment testing
# tekton/pipelines/ruby-pipeline.yaml
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
name: ruby-build-deploy
spec:
params:
- name: git-url
type: string
- name: git-revision
type: string
default: main
- name: image-name
type: string
- name: image-tag
type: string
workspaces:
- name: source
- name: dockerconfig
tasks:
- name: fetch-source
taskRef:
name: git-clone
workspaces:
- name: output
workspace: source
params:
- name: url
value: $(params.git-url)
- name: revision
value: $(params.git-revision)
- name: run-tests
taskRef:
name: ruby-test
runAfter:
- fetch-source
workspaces:
- name: source
workspace: source
- name: security-scan
taskRef:
name: ruby-security-scan
runAfter:
- fetch-source
workspaces:
- name: source
workspace: source
- name: build-image
taskRef:
name: buildah
runAfter:
- run-tests
- security-scan
workspaces:
- name: source
workspace: source
- name: dockerconfig
workspace: dockerconfig
params:
- name: IMAGE
value: $(params.image-name):$(params.image-tag)
- name: deploy-to-staging
taskRef:
name: argocd-sync
runAfter:
- build-image
params:
- name: application-name
value: ruby-app-staging
- name: image-tag
value: $(params.image-tag)
# monitoring/prometheus/servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: ruby-app-metrics
spec:
selector:
matchLabels:
app: ruby-app
endpoints:
- port: metrics
path: /metrics
interval: 30s
{
"dashboard": {
"title": "Ruby Application Metrics",
"panels": [
{
"title": "Request Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{method}} {{status}}"
}
]
},
{
"title": "Response Time",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile"
}
]
},
{
"title": "Database Connections",
"type": "stat",
"targets": [
{
"expr": "pg_stat_database_numbackends",
"legendFormat": "Active Connections"
}
]
}
]
}
}
# k8s/base/network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ruby-app-netpol
spec:
podSelector:
matchLabels:
app: ruby-app
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: nginx-ingress
ports:
- protocol: TCP
port: 3000
egress:
- to:
- podSelector:
matchLabels:
app: postgresql
ports:
- protocol: TCP
port: 5432
- to: []
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
# k8s/base/rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: ruby-app-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ruby-app-role
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ruby-app-rolebinding
subjects:
- kind: ServiceAccount
name: ruby-app-sa
roleRef:
kind: Role
name: ruby-app-role
apiGroup: rbac.authorization.k8s.io
#!/bin/bash
# scripts/backup.sh
NAMESPACE="ruby-app"
POSTGRES_POD=$(kubectl get pods -n $NAMESPACE -l app=postgresql -o jsonpath='{.items[0].metadata.name}')
BACKUP_NAME="postgres-backup-$(date +%Y%m%d-%H%M%S)"
S3_BUCKET="ruby-app-backups"
echo "Creating database backup: $BACKUP_NAME"
kubectl exec -n $NAMESPACE $POSTGRES_POD -- pg_dump -U postgres ruby_app_production | \
gzip > "/tmp/$BACKUP_NAME.sql.gz"
aws s3 cp "/tmp/$BACKUP_NAME.sql.gz" "s3://$S3_BUCKET/$BACKUP_NAME.sql.gz"
echo "Backup completed: s3://$S3_BUCKET/$BACKUP_NAME.sql.gz"
# Clean up local file
rm "/tmp/$BACKUP_NAME.sql.gz"
# Keep only last 30 backups
aws s3 ls "s3://$S3_BUCKET/" | sort | head -n -30 | awk '{print $4}' | \
xargs -I {} aws s3 rm "s3://$S3_BUCKET/{}"
#!/bin/bash
# scripts/migrate.sh
NAMESPACE="ruby-app"
APP_POD=$(kubectl get pods -n $NAMESPACE -l app=ruby-app -o jsonpath='{.items[0].metadata.name}')
echo "Running database migrations..."
kubectl exec -n $NAMESPACE $APP_POD -- bundle exec rails db:migrate
echo "Migration completed successfully"
π Live Application: https://ruby-app.buildwithsushant.com
- GitOps deployment workflow
- Rolling updates demonstration
- Database persistence testing
- Monitoring dashboards
cd app
bundle exec rspec
kubectl apply -f tests/integration/
kubectl apply -f tests/load/
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
For questions or issues:
- π§ Email: buildwithsushant@gmail.com
- π Issues: GitHub Issues
Built with β€οΈ by Sushant Kumar