Требование:
С помощью Jenkins Pipeline на каждый пул-реквест поднимать стейдж окружение и делать web-доступ на индивидуальном поддомене в kubernetes кластере, чтобы прогонять тесты, проверять гипотезы и предоставлять заказчику preview выполненной работы.
Описание:
Для каждого PR поднимается свой деплой со своими deployment, service, ingress в namespace project-stage.
Удаляются дейплои после закрытия PR с помощью скрипта и github api – это задача devops.
В Jenkinsfile определяется, что нужно задеплоить: stage или prod, соответственно, определяются значения переменных для namespace, prefix, custom envs.
Jenkins Pipeline Jenkinsfile Example:
#!/usr/bin/env groovy def commitId pipeline { agent { kubernetes { label 'project-builder' defaultContainer 'jnlp' yaml """ apiVersion: v1 kind: Pod spec: serviceAccountName: tiller containers: - name: php-fpm image: php:7.2.7-fpm-alpine command: - cat tty: true env: - name: POSTGRES_DRIVER value: "pdo_pgsql" - name: POSTGRES_DB value: "" - name: POSTGRES_USER value: "" - name: POSTGRES_PASSWORD value: "" - name: POSTGRES_HOST value: "" - name: POSTGRES_PORT value: "" resources: requests: memory: "200Mi" cpu: "300m" limits: memory: "250Mi" cpu: "300m" - name: docker image: docker:18.02 command: - cat tty: true volumeMounts: - mountPath: /var/run/docker.sock name: docker-socket resources: requests: memory: "150Mi" cpu: "150m" limits: memory: "200Mi" cpu: "200m" - name: kubectl image: lachlanevenson/k8s-kubectl:latest command: - cat tty: true resources: requests: memory: "150Mi" cpu: "150m" limits: memory: "200Mi" cpu: "200m" volumes: - name: docker-socket hostPath: path: /var/run/docker.sock type: Socket nodeSelector: nodetype: jenkins tolerations: - key: "constraint" operator: "Equal" value: "jenkins" effect: "NoSchedule" """ } } environment { SUCCESS_MESSAGE = "Job: ${env.JOB_NAME} with number ${env.BUILD_NUMBER} was successful \n ${env.BUILD_URL}" FAILURE_MESSAGE = "Job: ${env.JOB_NAME} with number ${env.BUILD_NUMBER} was failed \n ${env.BUILD_URL}" SLACK_RECIPIENTS = 'developers-notifications' REPOSITORY = 'localhost:80' APP_ENV = 'prod' POSTGRES_PASSWORD_RO = credentials('postgres_password_ro') POSTGRES_PASSWORD_RW = credentials('postgres_password') POSTGRES_USER_RO = 'project_ro' POSTGRES_USER_RW = 'project' POSTGRES_DRIVER = 'pdo_pgsql' POSTGRES_HOST = 'project.wcvsdb42.eu-west-1.rds.amazonaws.com' POSTGRES_PORT = '5432' POSTGRES_DB = 'project_db' } options { timeout(time: 15, unit: 'MINUTES') disableConcurrentBuilds() } stages { stage ('Checkout') { steps { git branch: "${env.BRANCH_NAME}", credentialsId: 'k8s_developers_github_token', url: 'https://github.com/organization/project.git' script { commitId = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() currentBuild.description = "${commitId}" echo "${commitId}" } } } stage ('Declare STAGE environments') { when { branch 'PR-*' } steps { script { NAMESPACE = 'project-stage' DOMAINS = env.BRANCH_NAME.toLowerCase() + '.stage.project.organization.zone' PREFIX = env.BRANCH_NAME.toLowerCase() POSTGRES_PASSWORD = '${POSTGRES_PASSWORD_RO}' POSTGRES_USER = '${POSTGRES_USER_RO}' } } } stage ('Declare PROD environments') { when { branch 'master' } steps { script { NAMESPACE = 'project-prod' DOMAINS = 'project.organization.ru' PREFIX = 'prod' POSTGRES_PASSWORD = '${POSTGRES_PASSWORD_RW}' POSTGRES_USER = '${POSTGRES_USER_RW}' } } } stage ('Install env container') { steps { container ('php-fpm') { sh "./bin/composer install --no-interaction --no-plugins --no-scripts --no-dev --optimize-autoloader --ignore-platform-reqs" sh "./bin/console cache:clear --env=${APP_ENV}" } } } stage ('Build and Push images') { steps { container ('docker') { sh "docker build -t organization/project-php-fpm:'${commitId}' -f build/prod/php-fpm/Dockerfile ./" sh "docker tag organization/project-php-fpm:'${commitId}' '${REPOSITORY}'/organization/project-php-fpm:'${commitId}'" sh "docker push '${REPOSITORY}'/organization/project-php-fpm:'${commitId}'" } } } stage ('Create secrets for k8s') { steps { container ('kubectl') { sh "kubectl delete secret ${PREFIX}-secrets -n '${NAMESPACE}' || true" sh "kubectl create secret -n '${NAMESPACE}' generic ${PREFIX}-secrets --from-literal=POSTGRES_PASSWORD='$POSTGRES_PASSWORD'" } } } stage ('Deploy to k8s') { steps { container ('kubectl') { echo 'Domains ' + DOMAINS; echo 'Namespace ' + NAMESPACE; sh "sed -i 's/{{NAMESPACE}}/${NAMESPACE}/g' k8s/configmap.yaml" sh "sed -i 's/{{PREFIX}}/${PREFIX}/g' k8s/configmap.yaml" sh "sed -i 's/{{POSTGRES_USER}}/${POSTGRES_USER}/g' k8s/configmap.yaml" sh "sed -i 's/{{NAMESPACE}}/${NAMESPACE}/g' k8s/deploy.yaml" sh "sed -i 's/{{IMAGE_TAG}}/${commitId}/g' k8s/deploy.yaml" sh "sed -i 's/{{DOMAINS}}/${DOMAINS}/g' k8s/deploy.yaml" sh "sed -i 's/{{PREFIX}}/${PREFIX}/g' k8s/deploy.yaml" sh "kubectl --namespace='${NAMESPACE}' apply -f k8s/configmap.yaml" sh "kubectl --namespace='${NAMESPACE}' apply -f k8s/deploy.yaml" sh "kubectl --namespace='${NAMESPACE}' rollout status deployment ${PREFIX}-web-app" } } } stage ('Apply CronJob to k8s') { steps { container ('kubectl') { echo 'Domains ' + DOMAINS; echo 'Namespace ' + NAMESPACE; sh "sed -i 's/{{NAMESPACE}}/${NAMESPACE}/g' k8s/cronjob.yaml" sh "sed -i 's/{{IMAGE_TAG}}/${commitId}/g' k8s/cronjob.yaml" sh "sed -i 's/{{PREFIX}}/${PREFIX}/g' k8s/cronjob.yaml" sh "kubectl --namespace='${NAMESPACE}' apply -f k8s/cronjob.yaml" } } } } post { failure { slackSend channel: "${SLACK_RECIPIENTS}", color: "danger", message: "${FAILURE_MESSAGE}" } } }
Labels for delete closed PRs from stage:
Чтобы работало автоматическое удаление неактуальных деплоев из stage неймспейса, необходимо объектам в k8s (в конфигмапы, сервис, ингресс и деплойменты) указать лейблы branch.
--- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: "{{PREFIX}}-web-app" namespace: "{{NAMESPACE}}" spec: replicas: 2 progressDeadlineSeconds: 120 template: metadata: labels: name: "{{PREFIX}}-web-app" branch: "{{PREFIX}}" spec: initContainers: - name: "migration" ...
В Jenkinsfile в stage (‘Create secrets for k8s’) добавить дополнительную строку, которая вешает лейбл на секреты:
stage ('Create secrets for k8s') { steps { container ('kubectl') { sh "kubectl delete secret ${PREFIX}-secrets -n '${NAMESPACE}' || true" sh "kubectl create secret -n '${NAMESPACE}' generic ${PREFIX}-secrets --from-literal=POSTGRES_PASSWORD='$POSTGRES_PASSWORD'" sh "kubectl -n '${NAMESPACE}' label secret ${PREFIX}-secrets branch=${PREFIX}" } } }
Сам PREFIX определяется в Jenkinsfile:
PREFIX = env.BRANCH_NAME.toLowerCase()
Удаление будет происходить по лейблу, содержащему номер пулл-реквеста — настраивает команда devops, в командном дженкинсе появится дополнительная джоба. Прицип удаления такой:
kubectl --namespace=${NAMESPACE} delete all -l branch=pr-1003
Определение закрытых пулл-реквестов происходит с помощью Github API.