CI ko builder VM par bhejo, CD controller par: docker.sock hataya, secrets ab Jenkins credentials se

This commit is contained in:
devops 2026-09-16 14:48:47 +00:00
parent 2122ef2437
commit 7e4b66556e
2 changed files with 187 additions and 177 deletions

357
Jenkinsfile vendored
View File

@ -1,5 +1,8 @@
pipeline { 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 { parameters {
string(name: 'ROLLBACK_TAG', defaultValue: '', string(name: 'ROLLBACK_TAG', defaultValue: '',
@ -9,18 +12,17 @@ pipeline {
environment { environment {
REGISTRY = 'ns31240276.ip-51-195-4.eu:5000' REGISTRY = 'ns31240276.ip-51-195-4.eu:5000'
IMAGE_NAME = 'swim/demo-app' IMAGE_NAME = 'swim/demo-app'
VAULT_ADDR = 'http://vault:8200' // Sab jagah se ek hi naam. Controller isse proxy par bhejta hai
// Jenkins container ka workspace /var/jenkins_home/... hai, par docker // (extra_hosts), builder VM bridge IP par (/etc/hosts). TLS cert
// daemon HOST par chalta hai jahan wahi folder /opt/devops/data/jenkins/... // bhi isi naam ka hai, isliye andar ke DNS naam use nahi karte.
// par hai. `docker run -v` ko HOST ka path chahiye. VAULT_ADDR = 'https://ns31240276.ip-51-195-4.eu:8444'
JENKINS_HOME_CONTAINER = '/var/jenkins_home' SONAR_URL = 'https://ns31240276.ip-51-195-4.eu:8446'
JENKINS_HOME_HOST = '/opt/devops/data/jenkins' HOST_BRIDGE_IP = '192.168.124.1'
TRIVY_SEVERITY = 'HIGH,CRITICAL' TRIVY_SEVERITY = 'HIGH,CRITICAL'
} }
// Trigger ko YAHAN declare karna zaroori hai. Agar sirf job config.xml me ho // Trigger ko YAHAN declare karna zaroori hai. Sirf config.xml me hone par
// aur Jenkinsfile me na ho, to DeclarativeJobPropertyTrackerAction NPE deta hai. // DeclarativeJobPropertyTrackerAction NPE deta hai.
// Khaali spec = polling band, sirf webhook (notifyCommit) se chalega.
triggers { triggers {
pollSCM('') pollSCM('')
} }
@ -28,169 +30,189 @@ pipeline {
options { options {
timestamps() timestamps()
buildDiscarder(logRotator(numToKeepStr: '20')) buildDiscarder(logRotator(numToKeepStr: '20'))
timeout(time: 30, unit: 'MINUTES') timeout(time: 40, unit: 'MINUTES')
disableConcurrentBuilds() disableConcurrentBuilds()
} }
stages { stages {
stage('Init') { // ============ CI: sab kuch builder VM par ============
steps { // Yahan docker daemon builder VM ka hai. Build escape hui to attacker ko
script { // sirf ye VM milti hai - na secrets, na Vault, na production ki key.
// params ko environment block me mat chhuo - wahan null hone par stage('CI (builder-vm-01)') {
// NPE aata hai. Yahan script block me safe hai. agent { label 'builder' }
def rb = (params.ROLLBACK_TAG ?: '').trim()
env.IS_ROLLBACK = rb ? 'true' : 'false' stages {
env.IMAGE_TAG = rb ? rb : "${env.BUILD_NUMBER}" stage('Init') {
echo "MODE : ${env.IS_ROLLBACK == 'true' ? 'ROLLBACK' : 'NORMAL BUILD'}" steps {
echo "IMAGE_TAG : ${env.IMAGE_TAG}" 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') { stage('Checkout') {
steps { steps {
checkout scm checkout scm
sh 'echo "commit: $(git rev-parse --short HEAD)"' sh 'echo "commit: $(git rev-parse --short HEAD)"'
} }
} }
stage('Secrets from Vault') { stage('Secrets from Vault') {
steps { steps {
sh ''' // Token ab file se nahi, Jenkins credential se aata hai aur
set +x // log me masked rehta hai. Ye token sirf swim/registry padh
VT=$(cat /var/jenkins_secrets/vault_token) // sakta hai - baaki Vault iske liye band hai.
# Vault se registry ka password lo - Jenkins credential se NAHI withCredentials([string(credentialsId: 'vault-builder-token', variable: 'VAULT_TOKEN')]) {
curl -sf -H "X-Vault-Token: $VT" \ sh '''
"${VAULT_ADDR}/v1/swim/data/registry" \ set +x
| python3 -c " curl -sf -H "X-Vault-Token: $VAULT_TOKEN" \
"${VAULT_ADDR}/v1/swim/data/registry" \
| python3 -c "
import sys, json import sys, json
d = json.load(sys.stdin)['data']['data'] d = json.load(sys.stdin)['data']['data']
open('.vault-registry.env','w').write( open('.vault-registry.env','w').write(
'REG_USER=%s\\nREG_PASS=%s\\n' % (d['username'], d['password'])) 'REG_USER=%s\\nREG_PASS=%s\\n' % (d['username'], d['password']))
print(' Vault se registry creds mil gaye (user=%s)' % d['username']) print(' Vault se registry creds mil gaye (user=%s)' % d['username'])
" "
chmod 600 .vault-registry.env chmod 600 .vault-registry.env
''' '''
} }
} }
}
stage('Test') { stage('Test') {
when { expression { env.IS_ROLLBACK != 'true' } } when { expression { env.IS_ROLLBACK != 'true' } }
steps { steps {
sh ''' sh '''
HOST_WS=$(echo "$PWD" | sed "s|^${JENKINS_HOME_CONTAINER}|${JENKINS_HOME_HOST}|") docker run --rm -v "$PWD":/src -w /src python:3.12-slim sh -c "
docker run --rm -v "$HOST_WS":/src -w /src python:3.12-slim sh -c " pip install --quiet -r requirements-dev.txt &&
pip install --quiet -r requirements-dev.txt && python -m pytest tests/ -v
python -m pytest tests/ -v "
" '''
''' }
} }
}
stage('Code quality (SonarQube)') { stage('Code quality (SonarQube)') {
when { expression { env.IS_ROLLBACK != 'true' } } when { expression { env.IS_ROLLBACK != 'true' } }
steps { steps {
sh ''' withCredentials([string(credentialsId: 'sonar-token', variable: 'SONAR_TOKEN')]) {
set +x sh '''
ST=$(cat /var/jenkins_secrets/sonar_token) set +x
HOST_WS=$(echo "$PWD" | sed "s|^${JENKINS_HOME_CONTAINER}|${JENKINS_HOME_HOST}|") docker run --rm \
docker run --rm \ --add-host "ns31240276.ip-51-195-4.eu:${HOST_BRIDGE_IP}" \
--network devops \ -e SONAR_HOST_URL="${SONAR_URL}" \
-e SONAR_HOST_URL=http://sonarqube:9000 \ -e SONAR_TOKEN="$SONAR_TOKEN" \
-e SONAR_TOKEN="$ST" \ -v "$PWD":/usr/src \
-v "$HOST_WS":/usr/src \ sonarsource/sonar-scanner-cli:11 \
sonarsource/sonar-scanner-cli:11 \ -Dsonar.projectKey=swim-demo-app
-Dsonar.projectKey=swim-demo-app '''
''' }
} }
} }
stage('Build image') { stage('Build image') {
when { expression { env.IS_ROLLBACK != 'true' } } when { expression { env.IS_ROLLBACK != 'true' } }
steps { steps {
sh ''' sh '''
docker build \ docker build \
--build-arg APP_VERSION=${IMAGE_TAG} \ --build-arg APP_VERSION=${IMAGE_TAG} \
-t ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \ -t ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \
-t ${REGISTRY}/${IMAGE_NAME}:latest . -t ${REGISTRY}/${IMAGE_NAME}:latest .
''' '''
} }
} }
stage('Security scan (Trivy)') { stage('Security scan (Trivy)') {
when { expression { env.IS_ROLLBACK != 'true' } } when { expression { env.IS_ROLLBACK != 'true' } }
steps { steps {
sh ''' sh '''
# GATE hai, report nahi: HIGH/CRITICAL mila to build yahin fail hogi. # GATE hai, report nahi: HIGH/CRITICAL mila to build yahin fail hogi.
# Jo CVE business ne accept kiye hain wo .trivyignore me hain. # Jo CVE business ne accept kiye hain wo .trivyignore me hain.
# DooD: docker daemon host ka hai, isliye workspace ka host path chahiye. docker run --rm \
HOST_WS=$(echo "$PWD" | sed "s|^${JENKINS_HOME_CONTAINER}|${JENKINS_HOME_HOST}|") -v /var/run/docker.sock:/var/run/docker.sock \
docker run --rm \ -v trivy-cache:/root/.cache/ \
-v /var/run/docker.sock:/var/run/docker.sock \ -v "$PWD/.trivyignore":/.trivyignore:ro \
-v trivy-cache:/root/.cache/ \ aquasec/trivy:0.58.1 image \
-v "$HOST_WS/.trivyignore":/.trivyignore:ro \ --severity ${TRIVY_SEVERITY} \
aquasec/trivy:0.58.1 image \ --ignore-unfixed \
--severity ${TRIVY_SEVERITY} \ --ignorefile /.trivyignore \
--ignore-unfixed \ --exit-code 1 \
--ignorefile /.trivyignore \ --format table \
--exit-code 1 \ ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
--format table \ '''
${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} }
''' }
}
}
stage('Smoke test image') { stage('Smoke test image') {
when { expression { env.IS_ROLLBACK != 'true' } } when { expression { env.IS_ROLLBACK != 'true' } }
steps { steps {
sh ''' sh '''
CID=$(docker run -d -P ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}) CID=$(docker run -d -P ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG})
sleep 5 trap "docker rm -f $CID >/dev/null 2>&1" EXIT
docker exec $CID python -c " sleep 5
docker exec $CID python -c "
import urllib.request, json import urllib.request, json
r = json.loads(urllib.request.urlopen('http://127.0.0.1:8000/version').read()) r = json.loads(urllib.request.urlopen('http://127.0.0.1:8000/version').read())
print('version endpoint:', r) print('version endpoint:', r)
assert r['version'] == '${IMAGE_TAG}', 'version galat!' 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') { // ============ CD: controller par ============
when { expression { env.IS_ROLLBACK != 'true' } } // Deploy key sirf yahan aati hai. Builder VM ise kabhi nahi dekhti.
steps { stage('Deploy STAGING') {
sh ''' agent { label 'built-in' }
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') {
steps { 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 ''' sh '''
SVM=$(grep "^staging-vm" deploy/inventory.ini | sed "s/.*ansible_host=//" | tr -d " ") 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}" echo " staging expected: ${IMAGE_TAG} | live: ${GOT}"
[ "$GOT" = "${IMAGE_TAG}" ] || { echo "STAGING MISMATCH"; exit 1; } [ "$GOT" = "${IMAGE_TAG}" ] || { echo "STAGING MISMATCH"; exit 1; }
echo " STAGING VERIFIED" echo " STAGING VERIFIED"
@ -199,12 +221,10 @@ assert r['version'] == '${IMAGE_TAG}', 'version galat!'
} }
stage('Production approval') { stage('Production approval') {
// agent none = intezaar ke 15 minute me koi executor block nahi hota
agent none
steps { steps {
script { 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') { timeout(time: 15, unit: 'MINUTES') {
input(message: "Staging par ${env.IMAGE_TAG} verified hai. PRODUCTION par deploy karein?", input(message: "Staging par ${env.IMAGE_TAG} verified hai. PRODUCTION par deploy karein?",
ok: 'Deploy to Production') ok: 'Deploy to Production')
@ -214,42 +234,35 @@ assert r['version'] == '${IMAGE_TAG}', 'version galat!'
} }
} }
stage('Deploy to PRODUCTION') { stage('Deploy PRODUCTION') {
steps { agent { label 'built-in' }
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') {
steps { 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 ''' sh '''
PVM=$(grep "^app-vm" deploy/inventory.ini | sed "s/.*ansible_host=//" | tr -d " ") 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}" echo " production expected: ${IMAGE_TAG} | live: ${GOT}"
[ "$GOT" = "${IMAGE_TAG}" ] || { echo "PROD MISMATCH"; exit 1; } [ "$GOT" = "${IMAGE_TAG}" ] || { echo "PROD MISMATCH"; exit 1; }
echo " PRODUCTION VERIFIED" echo " PRODUCTION VERIFIED"
''' '''
} }
post { always { cleanWs() } }
} }
} }
post { 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}" } success { echo "BUILD ${env.BUILD_NUMBER} SAFAL -> ${env.REGISTRY}/${env.IMAGE_NAME}:${env.IMAGE_TAG}" }
failure { echo "BUILD ${env.BUILD_NUMBER} FAIL" } failure { echo "BUILD ${env.BUILD_NUMBER} FAIL" }
} }

View File

@ -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]
staging-vm-01 ansible_host=192.168.124.167 staging-vm-01 ansible_host=192.168.124.167
[production] [production]
app-vm-01 ansible_host=192.168.124.136 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