Требование:
С помощью 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.