diff --git a/DEPLOY.md b/DEPLOY.md index 648cad2..09c75a4 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -14,6 +14,8 @@ 6. [Troubleshooting Guide](#6-troubleshooting-guide) 7. [Updating the Application](#7-updating-the-application) 8. [Teardown](#8-teardown) +9. [CI/CD — Jenkins Multibranch Pipeline](#9-cicd--jenkins-multibranch-pipeline) +10. [GitOps — ArgoCD (Reference)](#10-gitops--argocd-reference) --- @@ -797,6 +799,275 @@ kubectl delete -f https://raw.githubusercontent.com/rancher/local-path-provision --- +--- + +## 9. CI/CD — Jenkins Multibranch Pipeline + +This project ships with a `Jenkinsfile` at the repo root that automates the full +build → test → push → deploy lifecycle. + +### 9.1 Prerequisites on the Jenkins Server + +| Tool | Purpose | Install | +|------|---------|---------| +| Docker | Build & push images | `apt install docker.io` + add `jenkins` user to `docker` group | +| kubectl | Apply k8s manifests | [Official install guide](https://kubernetes.io/docs/tasks/tools/) | +| kustomize | Render overlays | `curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash` | +| NodeJS (v18+) | `npm ci` / `npm test` | Jenkins NodeJS plugin or system package | + +```bash +# Add jenkins user to docker group (run on Jenkins host, then restart Jenkins) +sudo usermod -aG docker jenkins +sudo systemctl restart jenkins +``` + +### 9.2 Jenkins Plugins Required + +Install via **Manage Jenkins → Plugins**: + +| Plugin | Purpose | +|--------|---------| +| **Multibranch Pipeline** | Detects branches + PRs automatically | +| **Docker Pipeline** | Docker credential binding | +| **Credentials Binding** | Injects secrets into pipeline steps | +| **NodeJS** | Managed Node.js installations | +| **Git** | SCM checkout | + +### 9.3 Configure Jenkins Credentials + +Go to **Manage Jenkins → Credentials → System → Global credentials → Add**: + +| Credential ID | Kind | Fields | Used for | +|---------------|------|--------|---------| +| `dockerhub-credentials` | Username with password | Username: `subkamble` / Password: Docker Hub PAT | `docker push` | +| `kubeconfig-secret` | Secret file | Upload your `~/.kube/config` | `kubectl` cluster access | + +> **Docker Hub PAT:** Generate at hub.docker.com → Account Settings → Security → New Access Token (Read/Write scope). + +> **kubeconfig tip:** If Jenkins runs inside the same cluster, use a ServiceAccount + RBAC instead of a kubeconfig file. + +### 9.4 Create the Multibranch Pipeline Job + +1. **Jenkins Dashboard → New Item** +2. Name: `react-mysql` → select **Multibranch Pipeline** → OK +3. Under **Branch Sources** → Add source → **Git** + - Repository URL: your repo URL + - Credentials: add repo access if private +4. Under **Build Configuration** → Mode: **by Jenkinsfile** → Script Path: `Jenkinsfile` +5. Under **Scan Multibranch Pipeline Triggers** → enable **Periodically if not otherwise run** (e.g. every 5 min), or configure a webhook (preferred) +6. Save → **Scan Multibranch Pipeline Now** + +Jenkins will discover all branches and create sub-jobs automatically. + +### 9.5 Webhook Setup (recommended — avoids polling) + +**GitHub:** +1. Repo → Settings → Webhooks → Add webhook +2. Payload URL: `http:///multibranch-webhook-trigger/invoke?token=react-mysql` +3. Content type: `application/json` +4. Events: **Push** + **Pull requests** + +Install the **Multibranch Scan Webhook Trigger** plugin on Jenkins to handle the token. + +### 9.6 Branch Behavior Summary + +| Branch | Build | Test | Push Image | Deploy | +|--------|-------|------|-----------|--------| +| `main` | Yes | Yes | Yes (`latest` + SHA tag) | `k8s/overlays/onpremise` (production) | +| `staging` | Yes | Yes | Yes (branch-SHA tag) | `k8s/overlays/minikube` (staging) | +| `develop` | Yes | Yes | No | No | +| `feature/**` | Yes | Yes | No | No | +| PR branches | Yes | Yes | No | No | + +### 9.7 Image Tagging Convention + +| Branch | Tag format | Example | +|--------|-----------|---------| +| `main` | `latest` + `` | `latest`, `a1b2c3d4` | +| `staging` | `staging-` | `staging-a1b2c3d4` | +| `feature/login` | `feature-login-` | `feature-login-a1b2c3d4` | + +### 9.8 First-Run Checklist + +```bash +# Verify Jenkins can reach Docker Hub +docker login -u subkamble + +# Verify Jenkins can reach the cluster +kubectl get nodes + +# Verify kustomize is on PATH +kustomize version + +# Trigger a manual build for main branch from Jenkins UI +# then watch the Console Output for each stage +``` + +### 9.9 Rollback + +```bash +# List available image tags (via Docker Hub API or local docker images) +docker images subkamble/react-mysql-backend + +# Pin a specific tag manually and re-deploy +cd k8s/base +kustomize edit set image \ + subkamble/react-mysql-backend=subkamble/react-mysql-backend: \ + subkamble/react-mysql-frontend=subkamble/react-mysql-frontend: +cd - +kubectl apply -k k8s/overlays/onpremise +kubectl rollout status deployment/backend -n react-mysql +kubectl rollout status deployment/frontend -n react-mysql +``` + +--- + +## 10. GitOps — ArgoCD (Reference) + +> The Jenkinsfile contains fully commented-out ArgoCD pipeline stages. +> Follow this section to activate GitOps mode. + +### 10.1 Install ArgoCD + +```bash +kubectl create namespace argocd +kubectl apply -n argocd \ + -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml + +# Wait for all ArgoCD pods to be ready +kubectl wait --for=condition=Ready pods --all -n argocd --timeout=180s + +# Expose the ArgoCD API server (choose one): + +# Option A — Port-forward (local access only) +kubectl port-forward svc/argocd-server -n argocd 8080:443 + +# Option B — LoadBalancer (on-prem with MetalLB or cloud) +kubectl patch svc argocd-server -n argocd \ + -p '{"spec":{"type":"LoadBalancer"}}' + +# Option C — Ingress (add argocd.myapp.local to your Ingress controller) +``` + +### 10.2 Get Initial Admin Password + +```bash +kubectl get secret argocd-initial-admin-secret \ + -n argocd \ + -o jsonpath="{.data.password}" | base64 -d && echo + +# Login +argocd login localhost:8080 --username admin --password --insecure + +# Change password immediately +argocd account update-password +``` + +### 10.3 Apply ArgoCD Application Manifests + +Create the two Application objects (one per environment). +These are documented in full inside the `Jenkinsfile` comments. +Here is the summary of what they do: + +| Application | Watches branch | Overlay | Sync policy | +|-------------|---------------|---------|------------| +| `react-mysql-production` | `main` | `k8s/overlays/onpremise` | Automated (prune + selfHeal) | +| `react-mysql-staging` | `staging` | `k8s/overlays/minikube` | Automated (prune + selfHeal) | + +```bash +# Copy the YAML blocks from the Jenkinsfile comments into files, then: +kubectl apply -f argocd/application-production.yaml +kubectl apply -f argocd/application-staging.yaml + +# Verify apps are registered +argocd app list +``` + +### 10.4 Jenkins Credentials for GitOps Mode + +Add these alongside the existing credentials: + +| Credential ID | Kind | Purpose | +|---------------|------|---------| +| `argocd-auth-token` | Secret text | ArgoCD API token for `argocd app sync` | +| `gitops-ssh-key` | SSH username with private key | Push image tag updates back to Git | + +**Generate an ArgoCD API token:** +```bash +argocd account generate-token --account admin +# Copy the output → paste as the 'argocd-auth-token' secret text +``` + +### 10.5 Activate GitOps in the Jenkinsfile + +1. Open `Jenkinsfile` +2. Remove the active **Deploy** and **Smoke Test** stages (or leave them as fallback) +3. Uncomment the `Update GitOps Manifest` and `ArgoCD Sync` stage blocks in the GitOps section at the bottom +4. Replace `YOUR_ARGOCD_SERVER` with your ArgoCD server hostname/IP +5. Replace `YOUR_ORG` in the git clone URL with your GitHub org/username +6. Commit and push — Jenkins picks up the change automatically + +### 10.6 GitOps Flow Diagram + +``` +Developer pushes code + │ + ▼ + Jenkins CI + ┌─────────────────────────────────────┐ + │ 1. npm ci → test │ + │ 2. docker build + push : │ + │ 3. kustomize edit set image │ + │ 4. git commit + push [skip ci] │ + └─────────────────┬───────────────────┘ + │ Git push (image tag bump) + ▼ + Git Repository + (k8s/base/kustomization.yaml updated) + │ + │ ArgoCD polls every 3 min + │ (or Jenkins triggers sync via argocd CLI) + ▼ + ArgoCD detects drift + ┌────────────────────────────┐ + │ kubectl apply -k overlay/ │ + │ prune removed resources │ + │ self-heal manual changes │ + └────────────┬───────────────┘ + │ + ▼ + Kubernetes Cluster + (react-mysql namespace updated) +``` + +### 10.7 ArgoCD Quick Reference + +```bash +# View app status +argocd app get react-mysql-production + +# Force immediate sync (skip waiting for poll interval) +argocd app sync react-mysql-production + +# Rollback to previous revision +argocd app history react-mysql-production +argocd app rollback react-mysql-production + +# Pause auto-sync (for maintenance) +argocd app set react-mysql-production --sync-policy none + +# Resume auto-sync +argocd app set react-mysql-production --sync-policy automated + +# Delete app (does NOT delete k8s resources by default) +argocd app delete react-mysql-production + +# Delete app AND k8s resources +argocd app delete react-mysql-production --cascade +``` + +--- + ## Quick Reference Card ```bash diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..bfdbe1c --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,428 @@ +// ============================================================================= +// Multibranch Pipeline - react-mysql +// Docker Hub: subkamble/react-mysql-backend, subkamble/react-mysql-frontend +// Kubernetes Namespaces: react-mysql +// Overlays: k8s/overlays/minikube | k8s/overlays/onpremise +// +// Branch Strategy: +// main → deploy to production (onpremise overlay) +// staging → deploy to staging (minikube overlay, or staging namespace) +// develop → build + test only (no deploy) +// feature/** → build + test only (no deploy) +// PR branches → build + test only (no deploy) +// ============================================================================= + +pipeline { + agent any + + // ------------------------------------------------------------------------- + // Environment variables - sensitive values come from Jenkins Credentials + // ------------------------------------------------------------------------- + environment { + DOCKER_HUB_USER = 'subkamble' + BACKEND_IMAGE = "${DOCKER_HUB_USER}/react-mysql-backend" + FRONTEND_IMAGE = "${DOCKER_HUB_USER}/react-mysql-frontend" + + // Jenkins credential IDs (configure in Jenkins > Credentials) + DOCKER_CREDENTIALS = 'dockerhub-credentials' // kind: Username/Password + KUBECONFIG_SECRET = 'kubeconfig-secret' // kind: Secret File + + // Derived from branch name + IS_MAIN = "${env.BRANCH_NAME == 'main'}" + IS_STAGING = "${env.BRANCH_NAME == 'staging'}" + IS_DEPLOYABLE = "${env.BRANCH_NAME == 'main' || env.BRANCH_NAME == 'staging'}" + + // Image tag: use git short SHA for traceability + GIT_SHORT_SHA = "${env.GIT_COMMIT ? env.GIT_COMMIT.take(8) : 'unknown'}" + IMAGE_TAG = "${env.BRANCH_NAME == 'main' ? 'latest' : env.BRANCH_NAME.replaceAll('/', '-') + '-' + GIT_SHORT_SHA}" + } + + options { + buildDiscarder(logRotator(numToKeepStr: '20')) + timeout(time: 30, unit: 'MINUTES') + disableConcurrentBuilds() + timestamps() + } + + stages { + + // ===================================================================== + // STAGE 1: Checkout & Metadata + // ===================================================================== + stage('Checkout') { + steps { + checkout scm + script { + echo "Branch : ${env.BRANCH_NAME}" + echo "Commit SHA : ${env.GIT_COMMIT}" + echo "Image Tag : ${env.IMAGE_TAG}" + echo "Deployable : ${env.IS_DEPLOYABLE}" + } + } + } + + // ===================================================================== + // STAGE 2: Install Dependencies & Lint + // ===================================================================== + stage('Install & Lint') { + parallel { + stage('Backend - Install') { + steps { + dir('backend') { + sh 'npm ci --prefer-offline' + } + } + } + stage('Frontend - Install') { + steps { + dir('frontend') { + sh 'npm ci --prefer-offline' + } + } + } + } + } + + // ===================================================================== + // STAGE 3: Test + // ===================================================================== + stage('Test') { + parallel { + stage('Backend - Test') { + steps { + dir('backend') { + // Replace with your actual test command + sh 'npm test --if-present || echo "No backend tests configured"' + } + } + } + stage('Frontend - Test') { + steps { + dir('frontend') { + // CI=true prevents interactive watch mode in React + sh 'CI=true npm test --if-present || echo "No frontend tests configured"' + } + } + } + } + } + + // ===================================================================== + // STAGE 4: Docker Build (all branches) + // ===================================================================== + stage('Docker Build') { + parallel { + stage('Build Backend') { + steps { + dir('backend') { + sh "docker build -t ${BACKEND_IMAGE}:${IMAGE_TAG} ." + } + } + } + stage('Build Frontend') { + steps { + dir('frontend') { + sh "docker build -t ${FRONTEND_IMAGE}:${IMAGE_TAG} ." + } + } + } + } + } + + // ===================================================================== + // STAGE 5: Docker Push (main + staging branches only) + // ===================================================================== + stage('Docker Push') { + when { + expression { env.IS_DEPLOYABLE == 'true' } + } + steps { + withCredentials([usernamePassword( + credentialsId: "${DOCKER_CREDENTIALS}", + usernameVariable: 'DOCKER_USER', + passwordVariable: 'DOCKER_PASS' + )]) { + sh ''' + echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin + docker push ${BACKEND_IMAGE}:${IMAGE_TAG} + docker push ${FRONTEND_IMAGE}:${IMAGE_TAG} + docker logout + ''' + } + // Also tag + push as 'latest' on main + script { + if (env.BRANCH_NAME == 'main') { + withCredentials([usernamePassword( + credentialsId: "${DOCKER_CREDENTIALS}", + usernameVariable: 'DOCKER_USER', + passwordVariable: 'DOCKER_PASS' + )]) { + sh ''' + echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin + docker tag ${BACKEND_IMAGE}:${IMAGE_TAG} ${BACKEND_IMAGE}:latest + docker tag ${FRONTEND_IMAGE}:${IMAGE_TAG} ${FRONTEND_IMAGE}:latest + docker push ${BACKEND_IMAGE}:latest + docker push ${FRONTEND_IMAGE}:latest + docker logout + ''' + } + } + } + } + } + + // ===================================================================== + // STAGE 6: Deploy to Kubernetes + // + // main → onpremise overlay (production) + // staging → minikube overlay (staging / dev cluster) + // ===================================================================== + stage('Deploy') { + when { + expression { env.IS_DEPLOYABLE == 'true' } + } + steps { + script { + def overlay = (env.BRANCH_NAME == 'main') ? 'onpremise' : 'minikube' + echo "Deploying to overlay: ${overlay}" + + withCredentials([file( + credentialsId: "${KUBECONFIG_SECRET}", + variable: 'KUBECONFIG' + )]) { + sh """ + export KUBECONFIG=\$KUBECONFIG + + # Update image tags in kustomize overlays in-place + cd k8s/base + kustomize edit set image \\ + ${BACKEND_IMAGE}=${BACKEND_IMAGE}:${IMAGE_TAG} \\ + ${FRONTEND_IMAGE}=${FRONTEND_IMAGE}:${IMAGE_TAG} + cd - + + # Apply the overlay + kubectl apply -k k8s/overlays/${overlay} + + # Wait for rollout + kubectl rollout status deployment/backend -n react-mysql --timeout=120s + kubectl rollout status deployment/frontend -n react-mysql --timeout=120s + """ + } + } + } + } + + // ===================================================================== + // STAGE 7: Smoke Test (post-deploy health check) + // ===================================================================== + stage('Smoke Test') { + when { + expression { env.IS_DEPLOYABLE == 'true' } + } + steps { + script { + withCredentials([file( + credentialsId: "${KUBECONFIG_SECRET}", + variable: 'KUBECONFIG' + )]) { + sh ''' + export KUBECONFIG=$KUBECONFIG + # Verify pods are running + kubectl get pods -n react-mysql + # Quick readiness check + kubectl wait --for=condition=Ready pods -l app=backend -n react-mysql --timeout=60s + kubectl wait --for=condition=Ready pods -l app=frontend -n react-mysql --timeout=60s + echo "Smoke test passed." + ''' + } + } + } + } + } + + // ========================================================================= + // POST Actions + // ========================================================================= + post { + always { + // Clean up dangling local Docker images to save disk space + sh ''' + docker rmi ${BACKEND_IMAGE}:${IMAGE_TAG} || true + docker rmi ${FRONTEND_IMAGE}:${IMAGE_TAG} || true + docker image prune -f || true + ''' + } + success { + echo "Pipeline SUCCESS on branch '${env.BRANCH_NAME}' — image tag: ${env.IMAGE_TAG}" + } + failure { + echo "Pipeline FAILED on branch '${env.BRANCH_NAME}'. Check logs above." + // Uncomment to send email notifications: + // emailext( + // subject: "FAILED: ${env.JOB_NAME} [${env.BUILD_NUMBER}]", + // body: "Branch: ${env.BRANCH_NAME}\nBuild URL: ${env.BUILD_URL}", + // to: 'your-team@example.com' + // ) + } + } +} + + +// ============================================================================= +// ============================================================================= +// +// ██████╗ ██╗████████╗ ██████╗ ██████╗ ███████╗ ██████╗ █████╗ ████████╗ +// ██╔════╝ ██║╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝ ██╔══██╗██╔══██╗╚══██╔══╝ +// ██║ ███╗██║ ██║ ██║ ██║██████╔╝███████╗ ██║ ██║███████║ ██║ +// ██║ ██║██║ ██║ ██║ ██║██╔═══╝ ╚════██║ ██║ ██║██╔══██║ ██║ +// ╚██████╔╝██║ ██║ ╚██████╔╝██║ ███████║ ██████╔╝██║ ██║ ██║ +// ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ +// +// GITOPS / ARGOCD WORKFLOW (commented out — activate when ArgoCD is installed) +// ============================================================================= +// +// Overview: +// Instead of Jenkins calling kubectl directly (push model), ArgoCD watches +// a Git repository for k8s manifests and syncs the cluster (pull model). +// +// Jenkins responsibility becomes: +// 1. Build & push Docker image +// 2. Update the image tag in the GitOps repo (PR or direct commit) +// 3. ArgoCD detects the change and syncs the cluster automatically +// +// Prerequisites: +// - ArgoCD installed in cluster: kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml +// - argocd CLI available on Jenkins agent +// - Jenkins credentials: +// 'argocd-auth-token' → Secret Text (ArgoCD API token) +// 'gitops-ssh-key' → SSH Key (write access to GitOps repo) +// - A dedicated GitOps repo (can be this same repo or a separate one) +// +// ============================================================================= +// +// ---- ARGOCD APPLICATION MANIFEST (apply once, not in pipeline) -------------- +// +// # argocd/application-production.yaml +// apiVersion: argoproj.io/v1alpha1 +// kind: Application +// metadata: +// name: react-mysql-production +// namespace: argocd +// spec: +// project: default +// source: +// repoURL: https://github.com/YOUR_ORG/react-mysql.git +// targetRevision: main +// path: k8s/overlays/onpremise +// destination: +// server: https://kubernetes.default.svc +// namespace: react-mysql +// syncPolicy: +// automated: +// prune: true # Remove resources no longer in Git +// selfHeal: true # Revert manual cluster changes +// syncOptions: +// - CreateNamespace=true +// +// --- +// +// # argocd/application-staging.yaml +// apiVersion: argoproj.io/v1alpha1 +// kind: Application +// metadata: +// name: react-mysql-staging +// namespace: argocd +// spec: +// project: default +// source: +// repoURL: https://github.com/YOUR_ORG/react-mysql.git +// targetRevision: staging +// path: k8s/overlays/minikube +// destination: +// server: https://kubernetes.default.svc +// namespace: react-mysql +// syncPolicy: +// automated: +// prune: true +// selfHeal: true +// syncOptions: +// - CreateNamespace=true +// +// ============================================================================= +// +// ---- GITOPS-ENABLED PIPELINE STAGES (replace stages 5-7 above) -------------- +// +// // stage('Docker Push') { ... } ← keep as-is +// +// // stage('Update GitOps Manifest') { +// // when { expression { env.IS_DEPLOYABLE == 'true' } } +// // steps { +// // script { +// // def overlay = (env.BRANCH_NAME == 'main') ? 'onpremise' : 'minikube' +// // withCredentials([sshUserPrivateKey( +// // credentialsId: 'gitops-ssh-key', +// // keyFileVariable: 'SSH_KEY' +// // )]) { +// // sh """ +// // # Configure git SSH +// // GIT_SSH_COMMAND="ssh -i \$SSH_KEY -o StrictHostKeyChecking=no" +// // export GIT_SSH_COMMAND +// // +// // # Clone the GitOps repo (or use current repo) +// // git clone git@github.com:YOUR_ORG/react-mysql.git gitops-repo +// // cd gitops-repo +// // +// // git config user.email "jenkins@ci.local" +// // git config user.name "Jenkins CI" +// // +// // # Bump image tags using kustomize +// // cd k8s/base +// // kustomize edit set image \\ +// // ${BACKEND_IMAGE}=${BACKEND_IMAGE}:${IMAGE_TAG} \\ +// // ${FRONTEND_IMAGE}=${FRONTEND_IMAGE}:${IMAGE_TAG} +// // cd ../.. +// // +// // git add k8s/ +// // git commit -m "ci: bump images to ${IMAGE_TAG} [${env.BRANCH_NAME}] [skip ci]" +// // git push origin ${env.BRANCH_NAME} +// // """ +// // } +// // } +// // } +// // } +// +// // stage('ArgoCD Sync') { +// // when { expression { env.IS_DEPLOYABLE == 'true' } } +// // steps { +// // script { +// // def appName = (env.BRANCH_NAME == 'main') ? 'react-mysql-production' : 'react-mysql-staging' +// // withCredentials([string( +// // credentialsId: 'argocd-auth-token', +// // variable: 'ARGOCD_TOKEN' +// // )]) { +// // sh """ +// // # Login to ArgoCD (token-based) +// // argocd login YOUR_ARGOCD_SERVER \\ +// // --auth-token \$ARGOCD_TOKEN \\ +// // --grpc-web \\ +// // --insecure +// // +// // # Trigger sync (ArgoCD auto-detects git changes too, +// // # but this forces an immediate sync) +// // argocd app sync ${appName} --prune --timeout 120 +// // +// // # Wait for sync to complete +// // argocd app wait ${appName} --health --timeout 180 +// // +// // echo "ArgoCD sync complete for ${appName}" +// // """ +// // } +// // } +// // } +// // } +// +// // stage('Smoke Test') { ... } ← keep as-is (or query ArgoCD health instead) +// +// ============================================================================= +// END GITOPS / ARGOCD SECTION +// =============================================================================