Production-ready Kubernetes on CSC cPouta with CI/CD GitOps pipeline using ArgoCD and GitHub Actions.
- Overview | Architecture | Prerequisites | Setup | CI/CD | GitOps | Access | Troubleshooting
Technology Stack: Terraform + k3s + ArgoCD + GitHub Actions + React + GHCR
Key Features: Zero-downtime deployments, auto-scaling (3-10 pods), automated security scanning, ~25MB images, 10-12 min deployment time
| Component | Technology |
|---|---|
| Cloud | CSC cPouta (OpenStack) |
| IaC | Terraform |
| Kubernetes | k3s |
| GitOps | ArgoCD |
| CI/CD | GitHub Actions |
| Registry | GHCR |
| App | React + Vite + Tailwind |
| Runtime | Nginx Alpine |
| Security | Trivy |
Internet β Floating IP (86.50.229.25:30007) β Security Group β K8s Cluster
ββ k8s-master (192.168.1.10) - Control Plane + ArgoCD
ββ k8s-worker (192.168.1.11) - App Pods (reactapp-ui Γ3, HPA 3-10, CPU 70%)
CI/CD Flow: Push β GitHub Actions (build+scan) β GHCR (image) β Update manifest (SHA tag) β ArgoCD sync (3min poll) β K8s rolling update β Live (zero downtime)
Network: Private 192.168.1.0/24, public IP on master, NodePort 30007
Security: Public SG (SSH/HTTP/HTTPS/6443/30007), Internal SG (full subnet access)
graph TD
classDef git fill:#F05032,stroke:#fff,stroke-width:2px,color:#fff;
classDef github fill:#181717,stroke:#fff,stroke-width:2px,color:#fff;
classDef generic fill:#444,stroke:#fff,stroke-width:1px,color:#fff;
classDef docker fill:#2496ED,stroke:#fff,stroke-width:2px,color:#fff;
classDef k8s fill:#326CE5,stroke:#fff,stroke-width:2px,color:#fff;
classDef argo fill:#EF7B4D,stroke:#fff,stroke-width:2px,color:#fff;
classDef user fill:#222,stroke:#fff,stroke-width:1px,color:#fff;
subgraph DevSpace ["1. Developer Workspace"]
Coder((Developer)):::user
end
subgraph GitHub ["2. Source Control & CI"]
RepoFront[reactapp/ Source<br/>React + Vite + Tailwind]:::github
ActionCI[GitHub Actions CI<br/>Build & Push Workflow]:::github
RepoManifests[k8s/ Manifests<br/>Deployment + Service + HPA]:::github
GHCR(GitHub Container Registry<br/>GHCR.io):::docker
end
subgraph K8sCluster ["3. CSC Pouta Kubernetes Cluster"]
direction TB
subgraph K8sControl [Control Plane]
ArgoCD[Argo CD<br/>GitOps Controller]:::argo
end
subgraph K8sWorkers [Worker Nodes]
subgraph ReactAppNS [reactapp Namespace]
direction LR
AppService[React App Service<br/>NodePort 30007]:::k8s
IngressRes[Ingress Resource<br/>reactapp-ui]:::k8s
HPA[HPA<br/>Autoscaler 3-10]:::k8s
Pod1[Pod 1]:::k8s
Pod2[Pod 2]:::k8s
Pod3[Pod 3]:::k8s
end
end
K8sNet(Kubernetes Networking / Kube-Proxy):::k8s
end
subgraph External ["4. External Access"]
Visitor((End User)):::user
CPoutaSG(cPouta Security Group<br/>k8s-public-secgroup):::generic
end
%% CI Flow
Coder -- "1. git push" --> RepoFront
RepoFront -- "Triggers" --> ActionCI
ActionCI -- "2. Build + Scan + Push Image" --> GHCR
ActionCI -- "3. Commit New SHA Tag" --> RepoManifests
%% CD Flow (GitOps)
ArgoCD -- "4. Auto-Sync Poll (3min)" --> RepoManifests
ArgoCD -- "5. Apply Manifests" --> ReactAppNS
Pod1 & Pod2 & Pod3 -- "6. Pull Image (Always)" --> GHCR
%% Scale Flow
HPA -. "Monitors & Scales" .-> Pod1 & Pod2 & Pod3
%% User Flow
Visitor -- "http://86.50.229.25:30007" --> CPoutaSG
CPoutaSG -- "Allow Port 30007" --> K8sNet
K8sNet --> AppService
AppService -- "Load Balances" --> Pod1 & Pod2 & Pod3
Workflow Explanation:
- Developer pushes code to
reactapp/directory - GitHub Actions builds Docker image, scans with Trivy, pushes to GHCR
- Workflow updates
k8s/reactapp/deployment.yamlwith new image SHA - ArgoCD detects manifest change via polling (every 3 minutes)
- ArgoCD syncs changes to cluster using
kubectl apply - Kubernetes performs rolling update with zero downtime
- End User accesses application via floating IP and NodePort
Required:
- Terraform (v1.0+):
winget install HashiCorp.Terraform - Git:
winget install Git.Git - CSC cPouta account with API access (2 VMs, 1 floating IP, 1 network quota)
- OpenStack credentials:
auth_url,user_name,password,tenant_name,region
Optional:
- kubectl:
winget install Kubernetes.kubectl - ArgoCD CLI: github.com/argoproj/argo-cd/releases
# 1. Clone repo
git clone https://github.com/Junaid-sadiq/multinode-gitops-cluster.git
cd multinode-gitops-cluster
# 2. Configure credentials - Create terraform/terraform.tfvars
auth_url = "https://pouta.csc.fi:5001/v3"
user_name = "your-username"
password = "your-password"
tenant_name = "your-project-name"
region = "regionOne"
# ssh_key_name = "pouta-dokploy-key" # Optional
# 3. Deploy everything (~10-15 min)
.\deploy-k8s-cluster.ps1
# Options:
# .\deploy-k8s-cluster.ps1 -DestroyFirst # Destroy + redeploy
# .\deploy-k8s-cluster.ps1 -SkipTerraform # Skip Terraform stepScript does: Terraform apply β Create VMs β Install k3s (master+worker) β Install ArgoCD β Save credentials
Output:
β Cluster ready! Access at http://86.50.229.25:30007
β ArgoCD: https://86.50.229.25:30008 (admin / see .argocd-credentials)
Click to expand manual steps
Terraform:
cd terraform
terraform init
terraform apply
$MASTER_IP = terraform output -raw master_floating_ipk3s Master:
ssh -i terraform/cpouta_key.pem ubuntu@$MASTER_IP
curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644
sudo cat /var/lib/rancher/k3s/server/node-token # Save tokenk3s Worker:
ssh -J ubuntu@$MASTER_IP -i terraform/cpouta_key.pem ubuntu@192.168.1.11
curl -sfL https://get.k3s.io | K3S_URL=https://192.168.1.10:6443 K3S_TOKEN=<TOKEN> sh -Verify:
kubectl get nodes # Both ReadyArgoCD:
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl patch svc argocd-server -n argocd -p '{"spec":{"type":"NodePort","ports":[{"port":443,"targetPort":8080,"nodePort":30008}]}}'
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d > .argocd-credentialsLocation: reactapp/ - React 18 + Vite + Tailwind v4 + TypeScript
Features: Animated shader background, newsletter signup, confetti celebration, responsive design
Build: Multi-stage Docker (Node build β Nginx runtime, ~25MB final image)
Health: /health endpoint, gzip compression, security headers, SPA routing
# Local dev
cd reactapp
npm install && npm run dev # β http://localhost:5173
# Docker test
docker build -t reactapp:local . && docker run -p 8080:80 reactapp:local1. PR Checks (pr-checks.yml) - Runs on PRs to main:
Trigger: pull_request (reactapp/**)
Jobs: Lint β Type check β Build verification2. Build & Deploy (ci-build-simple.yml) - Runs on push to main:
Trigger: push to main (reactapp/**)
Steps:
1. Checkout code
2. Build multi-stage Docker image (linux/amd64)
3. Scan with Trivy (continue-on-error: true)
4. Login to GHCR (ghcr.io)
5. Push image: ghcr.io/junaid-sadiq/multinode-gitops-cluster/reactapp:sha-{COMMIT_SHA}
6. Update k8s/reactapp/deployment.yaml with new SHA
7. Commit + push manifest changes
8. Trigger ArgoCD sync (auto-detects in 3 min)
Timeout: 25 minutesjunaid-sadiq not Junaid-sadiq
linux/amd64 only (multi-platform causes 30min+ hangs)
imagePullPolicy: Always in deployment.yaml forces fresh pulls
continue-on-error: true)
Configure in GitHub repo β Settings β Secrets:
GHCR_TOKEN - GitHub Personal Access Token with packages:write
- Code push β GitHub Actions triggered (instant)
- Build + scan + push (4-6 min)
- Manifest update (30s)
- ArgoCD sync poll (up to 3 min)
- K8s rolling update (2-3 min)
- Total: 10-12 minutes β
ArgoCD automatically deploys from Git. Application is pre-configured in k8s/reactapp/argocd-application.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: reactapp
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/Junaid-sadiq/multinode-gitops-cluster.git
targetRevision: HEAD
path: k8s/reactapp
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true # Delete resources not in Git
selfHeal: true # Auto-sync if manual changes made
syncOptions:
- CreateNamespace=true# SSH to master node
ssh -i terraform/cpouta_key.pem ubuntu@86.50.229.25
# Apply ArgoCD application
kubectl apply -f https://raw.githubusercontent.com/Junaid-sadiq/multinode-gitops-cluster/main/k8s/reactapp/argocd-application.yaml
# Watch sync status
kubectl get applications -n argocd
# NAME SYNC STATUS HEALTH STATUS
# reactapp Synced Healthy
# Check deployed resources
kubectl get pods,svc,hpa# Check pods (should see 3 replicas)
kubectl get pods -l app=reactapp-ui
# Check service
kubectl get svc react-app-service
# TYPE: NodePort, PORT: 80:30007/TCP
# Check HPA
kubectl get hpa reactapp-ui-hpa
# MIN: 3, MAX: 10, TARGET: 70% CPUApplication URL: http://86.50.229.25:30007
- Developer commits code β Triggers GitHub Actions
- GitHub Actions builds image β Pushes to GHCR β Updates
deployment.yamlwith new SHA - ArgoCD polls repo every 3 minutes β Detects manifest change
- ArgoCD applies changes β
kubectl apply -k k8s/reactapp/ - Kubernetes performs rolling update β Zero downtime
- ArgoCD UI shows sync status β Green = Healthy & Synced
- URL: https://86.50.229.25:30008
- Username:
admin - Password: Check
.argocd-credentialsfile - Features: Visual topology, sync history, logs, manual sync, rollback
Public URL: http://86.50.229.25:30007
Features:
- Newsletter signup with email validation
- Confetti celebration on submit
- Success message: "Congrats! You're on the waiting list"
- Animated shader background
- Fully responsive design
URL: https://86.50.229.25:30008
Login: admin / (password in .argocd-credentials)
Dashboard shows:
- Application health (Healthy/Progressing/Degraded)
- Sync status (Synced/OutOfSync)
- Resource tree (deployments, pods, services, HPA)
- Deployment history and logs
# Master node (public access)
ssh -i terraform/cpouta_key.pem ubuntu@86.50.229.25
# Worker node (via master as jump host)
ssh -J ubuntu@86.50.229.25 -i terraform/cpouta_key.pem ubuntu@192.168.1.11# Get cluster info
kubectl cluster-info
# Get all resources
kubectl get all
# Get pods with details
kubectl get pods -o wide
# Check pod logs
kubectl logs -l app=reactapp-ui --tail=50
# Check HPA metrics
kubectl get hpa
kubectl top pods # Requires metrics-server
# Check ArgoCD application
kubectl get applications -n argocd
# Describe pod (troubleshooting)
kubectl describe pod <pod-name>
# Port forward for local testing
kubectl port-forward svc/react-app-service 8080:80
# Access: http://localhost:8080GHCR URL: https://github.com/Junaid-sadiq?tab=packages
Image: ghcr.io/junaid-sadiq/multinode-gitops-cluster/reactapp:sha-{COMMIT_SHA}
View all published images and their scan results.
# Pod status
kubectl get pods -l app=reactapp-ui
# All should be Running
# Service endpoints
kubectl get endpoints react-app-service
# Should show 3 pod IPs
# HPA status
kubectl get hpa reactapp-ui-hpa
# REPLICAS should be 3/3 (or scaled up if under load)
# Check pod logs
kubectl logs -l app=reactapp-ui --tail=100 -f
# Health check
curl http://86.50.229.25:30007/health
# Should return 200 OKIssue: Pods stuck in ImagePullBackOff
# Check image name (must be lowercase)
kubectl describe pod <pod-name> | grep Image
# Verify GHCR image exists
# Go to: https://github.com/Junaid-sadiq?tab=packages
# Fix: Ensure workflow uses lowercase username
# GitHub Actions: GITHUB_REPOSITORY_OWNER | tr '[:upper:]' '[:lower:]'Issue: ArgoCD shows OutOfSync
# Check application status
kubectl get application reactapp -n argocd -o yaml
# Manual sync
kubectl patch application reactapp -n argocd -p '{"operation":{"sync":{"prune":true}}}' --type merge
# Or via UI: Click "Sync" β "Synchronize"Issue: Deployment not updating
# Check if imagePullPolicy is set to Always
kubectl get deployment reactapp-ui -o yaml | grep imagePullPolicy
# Should show: imagePullPolicy: Always
# Force pod recreation
kubectl rollout restart deployment reactapp-uiIssue: Cannot access application
# Check service
kubectl get svc react-app-service
# Type: NodePort, NodePort: 30007
# Check security group allows port 30007
# Verify in cPouta dashboard: k8s-public-secgroup
# Test from master node
curl http://localhost:30007# Watch pod status
kubectl get pods -l app=reactapp-ui -w
# Watch HPA scaling
kubectl get hpa -w
# Resource usage (requires metrics-server)
kubectl top nodes
kubectl top pods
# Events (recent cluster activity)
kubectl get events --sort-by='.lastTimestamp' | tail -20
# ArgoCD sync history
kubectl get application reactapp -n argocd -o jsonpath='{.status.history}'# Generate load to test auto-scaling
# Install hey: https://github.com/rakyll/hey
hey -z 60s -c 50 http://86.50.229.25:30007
# Watch HPA scale up
kubectl get hpa -w
# REPLICAS will increase from 3 β 10 based on CPU# Application logs
kubectl logs -l app=reactapp-ui --tail=200 -f
# ArgoCD logs
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller
# Deployment events
kubectl describe deployment reactapp-ui
# Pod describe (shows pull errors, crashes, etc.)
kubectl describe pod <pod-name>multinode-gitops-cluster/
βββ .github/workflows/ # CI/CD pipelines
β βββ ci-build-simple.yml # Main build & deploy workflow
β βββ pr-checks.yml # PR validation checks
βββ terraform/ # Infrastructure as Code
β βββ main.tf # OpenStack resources
β βββ variables.tf # Input variables
β βββ outputs.tf # Output values
β βββ terraform.tfvars # Credentials (gitignored)
β βββ .terraform/ # Terraform state & providers
βββ k8s/reactapp/ # Kubernetes manifests
β βββ deployment.yaml # App deployment (3 replicas, HPA)
β βββ service.yaml # NodePort service (30007)
β βββ hpa.yaml # Horizontal Pod Autoscaler
β βββ ingress.yaml # Ingress resource (optional)
β βββ argocd-application.yaml # ArgoCD app definition
β βββ kustomization.yaml # Kustomize config
βββ reactapp/ # React application source
β βββ src/ # React components
β β βββ App.tsx # Main app component
β β βββ components/ # UI components
β β βββ assets/ # Images, SVGs
β βββ public/ # Static assets
β β βββ favicon.svg # Rocket favicon
β βββ Dockerfile # Multi-stage Docker build
β βββ nginx.conf # Nginx configuration
β βββ package.json # Dependencies
β βββ vite.config.js # Vite build config
βββ deploy-k8s-cluster.ps1 # Automated deployment script
βββ .argocd-credentials # ArgoCD password (gitignored)
βββ .gitignore # Git ignore rules
βββ README.md # This file
βββ SECURITY.md # Security policies
deploy-k8s-cluster.ps1- One-command deployment automationci-build-simple.yml- GitHub Actions workflow (build β push β update manifest)deployment.yaml- K8s deployment withimagePullPolicy: Alwaysargocd-application.yaml- GitOps application configDockerfile- Multi-stage build (Node build β Nginx runtime)terraform.tfvars- OpenStack credentials (create this manually)
β
Network Segmentation: Private network (192.168.1.0/24), public access only on master
β
Security Groups: Minimal port exposure (SSH, HTTP, HTTPS, K8s API, NodePort)
β
SSH Key Auth: Password auth disabled, key-based only
β
Firewall Rules: OpenStack security groups restrict traffic
β
Private Worker: Worker node has no public IP
β
Container Scanning: Trivy scans all images for vulnerabilities
β
Minimal Base Image: Alpine Linux (~25MB, reduced attack surface)
β
Non-Root User: Nginx runs as non-root
β
Security Headers: CSP, X-Frame-Options, X-Content-Type-Options
β
HTTPS Ready: ArgoCD exposed with TLS (self-signed)
β
Image Integrity: SHA-tagged images ensure immutable deployments
β
Secrets Management: GitHub Secrets for GHCR token
β
Least Privilege: GHCR token has only packages:write scope
β
Audit Trail: All deployments tracked in Git history
β
Automated Scanning: Every image scanned before deployment
β
GitOps Principles: No direct kubectl access needed
- π Rotate ArgoCD admin password after first login
- π Use GitHub PAT with minimal scopes
- π Never commit
terraform.tfvarsor.argocd-credentials - π Review Trivy scan results in GitHub Actions logs
- π Enable ArgoCD RBAC for team access
- π Use network policies for pod-to-pod communication
- π Regular security updates:
kubectl set image deployment/reactapp-ui ...
Report vulnerabilities: See SECURITY.md
# Deploy application via ArgoCD
kubectl apply -f k8s/reactapp/argocd-application.yaml
# Check deployment status
kubectl get pods,svc,hpa
# View application logs
kubectl logs -l app=reactapp-ui -f
# Manual sync (if ArgoCD shows OutOfSync)
kubectl patch application reactapp -n argocd -p '{"operation":{"sync":{"prune":true}}}' --type merge
# Scale manually (overrides HPA temporarily)
kubectl scale deployment reactapp-ui --replicas=5
# Rollback to previous version
kubectl rollout undo deployment reactapp-ui
# Check rollout history
kubectl rollout history deployment reactapp-ui
# Restart pods (useful for pulling new image)
kubectl rollout restart deployment reactapp-ui- Application: http://86.50.229.25:30007
- ArgoCD UI: https://86.50.229.25:30008
- GitHub Repo: https://github.com/Junaid-sadiq/multinode-gitops-cluster
- GHCR Images: https://github.com/Junaid-sadiq?tab=packages
- ArgoCD:
admin/ (see.argocd-credentials) - SSH:
terraform/cpouta_key.pemor~/.ssh/pouta-dokploy-key - GHCR: GitHub PAT in
GHCR_TOKENsecret
- Kubernetes Documentation
- ArgoCD Documentation
- k3s Documentation
- GitHub Actions Documentation
- CSC cPouta Documentation
- Terraform OpenStack Provider
This project is provided as-is for educational purposes.
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Submit a pull request
Built with β€οΈ using Terraform, k3s, ArgoCD, and GitHub Actions