DevOps Linux

Jenkins Pipeline With k8s Staging

06.02.2020

Требование:

С помощью 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.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *