From 7e4b66556e36c4b02fe1c61af3b6182a2245fb5c Mon Sep 17 00:00:00 2001 From: devops Date: Wed, 16 Sep 2026 14:48:47 +0000 Subject: [PATCH] CI ko builder VM par bhejo, CD controller par: docker.sock hataya, secrets ab Jenkins credentials se --- Jenkinsfile | 357 ++++++++++++++++++++++--------------------- deploy/inventory.ini | 7 +- 2 files changed, 187 insertions(+), 177 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 0483aee..745cb6c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,8 @@ pipeline { - agent any + // Koi default agent nahi. Har stage khud batati hai kahan chalegi - + // CI builder VM par, CD controller par. Isse build ko production ki + // chaabi kabhi nahi milti. + agent none parameters { string(name: 'ROLLBACK_TAG', defaultValue: '', @@ -9,18 +12,17 @@ pipeline { environment { REGISTRY = 'ns31240276.ip-51-195-4.eu:5000' IMAGE_NAME = 'swim/demo-app' - VAULT_ADDR = 'http://vault:8200' - // Jenkins container ka workspace /var/jenkins_home/... hai, par docker - // daemon HOST par chalta hai jahan wahi folder /opt/devops/data/jenkins/... - // par hai. `docker run -v` ko HOST ka path chahiye. - JENKINS_HOME_CONTAINER = '/var/jenkins_home' - JENKINS_HOME_HOST = '/opt/devops/data/jenkins' - TRIVY_SEVERITY = 'HIGH,CRITICAL' + // Sab jagah se ek hi naam. Controller isse proxy par bhejta hai + // (extra_hosts), builder VM bridge IP par (/etc/hosts). TLS cert + // bhi isi naam ka hai, isliye andar ke DNS naam use nahi karte. + VAULT_ADDR = 'https://ns31240276.ip-51-195-4.eu:8444' + SONAR_URL = 'https://ns31240276.ip-51-195-4.eu:8446' + HOST_BRIDGE_IP = '192.168.124.1' + TRIVY_SEVERITY = 'HIGH,CRITICAL' } - // Trigger ko YAHAN declare karna zaroori hai. Agar sirf job config.xml me ho - // aur Jenkinsfile me na ho, to DeclarativeJobPropertyTrackerAction NPE deta hai. - // Khaali spec = polling band, sirf webhook (notifyCommit) se chalega. + // Trigger ko YAHAN declare karna zaroori hai. Sirf config.xml me hone par + // DeclarativeJobPropertyTrackerAction NPE deta hai. triggers { pollSCM('') } @@ -28,169 +30,189 @@ pipeline { options { timestamps() buildDiscarder(logRotator(numToKeepStr: '20')) - timeout(time: 30, unit: 'MINUTES') + timeout(time: 40, unit: 'MINUTES') disableConcurrentBuilds() } stages { - stage('Init') { - steps { - script { - // params ko environment block me mat chhuo - wahan null hone par - // NPE aata hai. Yahan script block me safe hai. - def rb = (params.ROLLBACK_TAG ?: '').trim() - env.IS_ROLLBACK = rb ? 'true' : 'false' - env.IMAGE_TAG = rb ? rb : "${env.BUILD_NUMBER}" - echo "MODE : ${env.IS_ROLLBACK == 'true' ? 'ROLLBACK' : 'NORMAL BUILD'}" - echo "IMAGE_TAG : ${env.IMAGE_TAG}" + // ============ CI: sab kuch builder VM par ============ + // Yahan docker daemon builder VM ka hai. Build escape hui to attacker ko + // sirf ye VM milti hai - na secrets, na Vault, na production ki key. + stage('CI (builder-vm-01)') { + agent { label 'builder' } + + stages { + stage('Init') { + steps { + script { + // params ko environment block me mat chhuo - wahan null hone par NPE. + def rb = (params.ROLLBACK_TAG ?: '').trim() + env.IS_ROLLBACK = rb ? 'true' : 'false' + env.IMAGE_TAG = rb ? rb : "${env.BUILD_NUMBER}" + echo "NODE : ${env.NODE_NAME}" + echo "MODE : ${env.IS_ROLLBACK == 'true' ? 'ROLLBACK' : 'NORMAL BUILD'}" + echo "IMAGE_TAG : ${env.IMAGE_TAG}" + } + } } - } - } - stage('Checkout') { - steps { - checkout scm - sh 'echo "commit: $(git rev-parse --short HEAD)"' - } - } + stage('Checkout') { + steps { + checkout scm + sh 'echo "commit: $(git rev-parse --short HEAD)"' + } + } - stage('Secrets from Vault') { - steps { - sh ''' - set +x - VT=$(cat /var/jenkins_secrets/vault_token) - # Vault se registry ka password lo - Jenkins credential se NAHI - curl -sf -H "X-Vault-Token: $VT" \ - "${VAULT_ADDR}/v1/swim/data/registry" \ - | python3 -c " + stage('Secrets from Vault') { + steps { + // Token ab file se nahi, Jenkins credential se aata hai aur + // log me masked rehta hai. Ye token sirf swim/registry padh + // sakta hai - baaki Vault iske liye band hai. + withCredentials([string(credentialsId: 'vault-builder-token', variable: 'VAULT_TOKEN')]) { + sh ''' + set +x + curl -sf -H "X-Vault-Token: $VAULT_TOKEN" \ + "${VAULT_ADDR}/v1/swim/data/registry" \ + | python3 -c " import sys, json d = json.load(sys.stdin)['data']['data'] open('.vault-registry.env','w').write( 'REG_USER=%s\\nREG_PASS=%s\\n' % (d['username'], d['password'])) print(' Vault se registry creds mil gaye (user=%s)' % d['username']) " - chmod 600 .vault-registry.env - ''' - } - } + chmod 600 .vault-registry.env + ''' + } + } + } - stage('Test') { - when { expression { env.IS_ROLLBACK != 'true' } } - steps { - sh ''' - HOST_WS=$(echo "$PWD" | sed "s|^${JENKINS_HOME_CONTAINER}|${JENKINS_HOME_HOST}|") - docker run --rm -v "$HOST_WS":/src -w /src python:3.12-slim sh -c " - pip install --quiet -r requirements-dev.txt && - python -m pytest tests/ -v - " - ''' - } - } + stage('Test') { + when { expression { env.IS_ROLLBACK != 'true' } } + steps { + sh ''' + docker run --rm -v "$PWD":/src -w /src python:3.12-slim sh -c " + pip install --quiet -r requirements-dev.txt && + python -m pytest tests/ -v + " + ''' + } + } - stage('Code quality (SonarQube)') { - when { expression { env.IS_ROLLBACK != 'true' } } - steps { - sh ''' - set +x - ST=$(cat /var/jenkins_secrets/sonar_token) - HOST_WS=$(echo "$PWD" | sed "s|^${JENKINS_HOME_CONTAINER}|${JENKINS_HOME_HOST}|") - docker run --rm \ - --network devops \ - -e SONAR_HOST_URL=http://sonarqube:9000 \ - -e SONAR_TOKEN="$ST" \ - -v "$HOST_WS":/usr/src \ - sonarsource/sonar-scanner-cli:11 \ - -Dsonar.projectKey=swim-demo-app - ''' - } - } + stage('Code quality (SonarQube)') { + when { expression { env.IS_ROLLBACK != 'true' } } + steps { + withCredentials([string(credentialsId: 'sonar-token', variable: 'SONAR_TOKEN')]) { + sh ''' + set +x + docker run --rm \ + --add-host "ns31240276.ip-51-195-4.eu:${HOST_BRIDGE_IP}" \ + -e SONAR_HOST_URL="${SONAR_URL}" \ + -e SONAR_TOKEN="$SONAR_TOKEN" \ + -v "$PWD":/usr/src \ + sonarsource/sonar-scanner-cli:11 \ + -Dsonar.projectKey=swim-demo-app + ''' + } + } + } - stage('Build image') { - when { expression { env.IS_ROLLBACK != 'true' } } - steps { - sh ''' - docker build \ - --build-arg APP_VERSION=${IMAGE_TAG} \ - -t ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \ - -t ${REGISTRY}/${IMAGE_NAME}:latest . - ''' - } - } + stage('Build image') { + when { expression { env.IS_ROLLBACK != 'true' } } + steps { + sh ''' + docker build \ + --build-arg APP_VERSION=${IMAGE_TAG} \ + -t ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \ + -t ${REGISTRY}/${IMAGE_NAME}:latest . + ''' + } + } - stage('Security scan (Trivy)') { - when { expression { env.IS_ROLLBACK != 'true' } } - steps { - sh ''' - # GATE hai, report nahi: HIGH/CRITICAL mila to build yahin fail hogi. - # Jo CVE business ne accept kiye hain wo .trivyignore me hain. - # DooD: docker daemon host ka hai, isliye workspace ka host path chahiye. - HOST_WS=$(echo "$PWD" | sed "s|^${JENKINS_HOME_CONTAINER}|${JENKINS_HOME_HOST}|") - docker run --rm \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -v trivy-cache:/root/.cache/ \ - -v "$HOST_WS/.trivyignore":/.trivyignore:ro \ - aquasec/trivy:0.58.1 image \ - --severity ${TRIVY_SEVERITY} \ - --ignore-unfixed \ - --ignorefile /.trivyignore \ - --exit-code 1 \ - --format table \ - ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} - ''' - } - } + stage('Security scan (Trivy)') { + when { expression { env.IS_ROLLBACK != 'true' } } + steps { + sh ''' + # GATE hai, report nahi: HIGH/CRITICAL mila to build yahin fail hogi. + # Jo CVE business ne accept kiye hain wo .trivyignore me hain. + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v trivy-cache:/root/.cache/ \ + -v "$PWD/.trivyignore":/.trivyignore:ro \ + aquasec/trivy:0.58.1 image \ + --severity ${TRIVY_SEVERITY} \ + --ignore-unfixed \ + --ignorefile /.trivyignore \ + --exit-code 1 \ + --format table \ + ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} + ''' + } + } - stage('Smoke test image') { - when { expression { env.IS_ROLLBACK != 'true' } } - steps { - sh ''' - CID=$(docker run -d -P ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}) - sleep 5 - docker exec $CID python -c " + stage('Smoke test image') { + when { expression { env.IS_ROLLBACK != 'true' } } + steps { + sh ''' + CID=$(docker run -d -P ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}) + trap "docker rm -f $CID >/dev/null 2>&1" EXIT + sleep 5 + docker exec $CID python -c " import urllib.request, json r = json.loads(urllib.request.urlopen('http://127.0.0.1:8000/version').read()) print('version endpoint:', r) assert r['version'] == '${IMAGE_TAG}', 'version galat!' " - docker rm -f $CID - ''' + ''' + } + } + + stage('Push to registry') { + when { expression { env.IS_ROLLBACK != 'true' } } + steps { + sh ''' + set +x + . ./.vault-registry.env + echo "$REG_PASS" | docker login ${REGISTRY} -u "$REG_USER" --password-stdin + docker push ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} + docker push ${REGISTRY}/${IMAGE_NAME}:latest + docker logout ${REGISTRY} + ''' + } + } + } + + post { + always { + sh 'rm -f .vault-registry.env || true' + sh 'docker image prune -f --filter "until=24h" || true' + cleanWs() + } } } - stage('Push to registry') { - when { expression { env.IS_ROLLBACK != 'true' } } - steps { - sh ''' - set +x - . ./.vault-registry.env - echo "$REG_PASS" | docker login ${REGISTRY} -u "$REG_USER" --password-stdin - docker push ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} - docker push ${REGISTRY}/${IMAGE_NAME}:latest - docker logout ${REGISTRY} - ''' - } - } - - stage('Deploy to STAGING') { - steps { - sh ''' - set +x - . ./.vault-registry.env - export REG_USER REG_PASS - export ANSIBLE_HOST_KEY_CHECKING=False - export IMAGE_TAG=${IMAGE_TAG} - ansible-galaxy collection install community.docker --force -q || true - ansible-playbook -i deploy/inventory.ini deploy/deploy.yml -e target=staging - ''' - } - } - - stage('Verify STAGING') { + // ============ CD: controller par ============ + // Deploy key sirf yahan aati hai. Builder VM ise kabhi nahi dekhti. + stage('Deploy STAGING') { + agent { label 'built-in' } steps { + checkout scm + withCredentials([ + sshUserPrivateKey(credentialsId: 'deploy-key', keyFileVariable: 'DEPLOY_KEY', usernameVariable: 'DEPLOY_USER'), + usernamePassword(credentialsId: 'registry-creds', usernameVariable: 'REG_USER', passwordVariable: 'REG_PASS') + ]) { + sh ''' + set +x + export ANSIBLE_HOST_KEY_CHECKING=False + ansible-galaxy collection install community.docker --force -q || true + ansible-playbook -i deploy/inventory.ini deploy/deploy.yml \ + -u "$DEPLOY_USER" --private-key "$DEPLOY_KEY" \ + -e target=staging + ''' + } sh ''' SVM=$(grep "^staging-vm" deploy/inventory.ini | sed "s/.*ansible_host=//" | tr -d " ") - GOT=$(curl -sf --max-time 10 http://${SVM}:8000/version | python3 -c "import sys,json;print(json.load(sys.stdin)[\'version\'])") + GOT=$(curl -sf --max-time 10 http://${SVM}:8000/version | python3 -c "import sys,json;print(json.load(sys.stdin)['version'])") echo " staging expected: ${IMAGE_TAG} | live: ${GOT}" [ "$GOT" = "${IMAGE_TAG}" ] || { echo "STAGING MISMATCH"; exit 1; } echo " STAGING VERIFIED" @@ -199,12 +221,10 @@ assert r['version'] == '${IMAGE_TAG}', 'version galat!' } stage('Production approval') { + // agent none = intezaar ke 15 minute me koi executor block nahi hota + agent none steps { script { - // Enterprise gate: insaan ki manzoori ke bina production par kuch nahi. - // 15 min me jawab na aaye to build abort - production safe rehta hai. - // Simple proceed/abort input. Parameters wale input ko - // API se submit karna jhanjhat hai; UI ke liye bhi ye saaf hai. timeout(time: 15, unit: 'MINUTES') { input(message: "Staging par ${env.IMAGE_TAG} verified hai. PRODUCTION par deploy karein?", ok: 'Deploy to Production') @@ -214,42 +234,35 @@ assert r['version'] == '${IMAGE_TAG}', 'version galat!' } } - stage('Deploy to PRODUCTION') { - steps { - sh ''' - set +x - . ./.vault-registry.env - export REG_USER REG_PASS - export ANSIBLE_HOST_KEY_CHECKING=False - export IMAGE_TAG=${IMAGE_TAG} - ansible-playbook -i deploy/inventory.ini deploy/deploy.yml -e target=production - ''' - } - } - - stage('Verify PRODUCTION') { + stage('Deploy PRODUCTION') { + agent { label 'built-in' } steps { + checkout scm + withCredentials([ + sshUserPrivateKey(credentialsId: 'deploy-key', keyFileVariable: 'DEPLOY_KEY', usernameVariable: 'DEPLOY_USER'), + usernamePassword(credentialsId: 'registry-creds', usernameVariable: 'REG_USER', passwordVariable: 'REG_PASS') + ]) { + sh ''' + set +x + export ANSIBLE_HOST_KEY_CHECKING=False + ansible-playbook -i deploy/inventory.ini deploy/deploy.yml \ + -u "$DEPLOY_USER" --private-key "$DEPLOY_KEY" \ + -e target=production + ''' + } sh ''' PVM=$(grep "^app-vm" deploy/inventory.ini | sed "s/.*ansible_host=//" | tr -d " ") - GOT=$(curl -sf --max-time 10 http://${PVM}:8000/version | python3 -c "import sys,json;print(json.load(sys.stdin)[\'version\'])") + GOT=$(curl -sf --max-time 10 http://${PVM}:8000/version | python3 -c "import sys,json;print(json.load(sys.stdin)['version'])") echo " production expected: ${IMAGE_TAG} | live: ${GOT}" [ "$GOT" = "${IMAGE_TAG}" ] || { echo "PROD MISMATCH"; exit 1; } echo " PRODUCTION VERIFIED" ''' } + post { always { cleanWs() } } } } post { - always { - script { - if (env.WORKSPACE) { - sh 'rm -f .vault-registry.env || true' - sh 'docker image prune -f --filter "until=24h" || true' - cleanWs() - } - } - } success { echo "BUILD ${env.BUILD_NUMBER} SAFAL -> ${env.REGISTRY}/${env.IMAGE_NAME}:${env.IMAGE_TAG}" } failure { echo "BUILD ${env.BUILD_NUMBER} FAIL" } } diff --git a/deploy/inventory.ini b/deploy/inventory.ini index f12534b..dfecc0f 100644 --- a/deploy/inventory.ini +++ b/deploy/inventory.ini @@ -1,10 +1,7 @@ +# SSH key yahan nahi likhi. Jenkins credential se --private-key par aati hai, +# taaki key repo me ya kisi file mount me na rahe. [staging] staging-vm-01 ansible_host=192.168.124.167 [production] app-vm-01 ansible_host=192.168.124.136 - -[all:vars] -ansible_user=deploy -ansible_ssh_private_key_file=/var/jenkins_secrets/deploy_key -ansible_ssh_common_args=-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null