Jenkins: деплой Redis и Helm subchart values

Автор: | 29/10/2020

Задача – содать Jenkins джобу, которая будет деплоить Redis на Dev/Stage/Prod кластера.

В посте Redis: Master-Slave репликация и запуск в Kubernetes сделали это вручную, посмотрели что и как вообще запускается и работает – теперь надо автоматизировать.

Главный вопрос – как вообще деплоить и передавать параметры? Хочется использовать уже готовый чарт, но в тоже время – деплоить тоже Helm-ом со своим values.yaml для каждого окружения – Dev/Stage/Prod.

Значит – можем создать свой Helm-чарт, а готовый чарт с Redis подключить как Helm dependency, которому будем передавать values.yaml из родительского чарта.

Документация тут>>>.

Делаем:

  • родительский чарт – backend-redis
    • в нём dependency bitnami/redis
    • в нём папку env
      • с папкой dev
        • с файлом values.yaml
  • и в Jenkins деплоим как helm install backend-redis -f env/${ENV}/values.yaml

Делаем.

Создание Helm-чарта

В репозитории с нашими сервисами создаём новый чарт:

[simterm]

$ helm create backend-redis
Creating backend-redis

[/simterm]

Удаляем файлы шаблонов – они нам тут не нужны:

[simterm]

$ rm -rf backend-redis/templates/*

[/simterm]

Создаём каталоги для values.yaml:

[simterm]

$ mkdir -p backend-redis/env/{dev,stage,prod}

[/simterm]

Копируем конфиг из предыдущего поста:

[simterm]

$ cp ~/Temp/redis-opts.yaml backend-redis/env/dev/values.yaml

[/simterm]

Проверяем его содержимое:

[simterm]

$ head backend-redis/env/dev/values.yaml
global:
  redis:
    password: "blablacar"

metrics:
  enabled: true
  serviceMonitor:
    enabled: true
    namespace: "monitoring"

[/simterm]

Хорошо, только пароль тут вырежем – он будет передаваться через helm install --set из Jenkins Password parameter.

Helm: values.yaml для сабчарта

Что в этом конфиге надо изменить – это добавить имя сабчарта, что бы мы могли его “скормить” Хельму, что бы он использовал эти значения для чарта Redis от Bitnami.

В начале файла добавляем имя сабчарта redis и вырезаем пароль:

redis:
  global:
    redis:
      password: ""

  metrics:
    enabled: true
    serviceMonitor:
      enabled: true
      namespace: "monitoring"

  master:
    persistence:
      enabled: false
    service:
      type: LoadBalancer
      annotations:
        service.beta.kubernetes.io/aws-load-balancer-internal: "true"
...

Теперь можем добавить dependency в наш чарт backend-redis, находим чарт:

[simterm]

$ helm search repo redis
NAME                                    CHART VERSION   APP VERSION     DESCRIPTION                                       
bitnami/redis                           11.2.3          6.0.9           Open source, advanced key-value store. It is of...
bitnami/redis-cluster                   3.2.10          6.0.9           Open source, advanced key-value store. It is of...
stable/prometheus-redis-exporter        3.5.1           1.3.4           DEPRECATED Prometheus exporter for Redis metrics  
stable/redis                            10.5.7          5.0.7           DEPRECATED Open source, advanced key-value stor...
stable/redis-ha                         4.4.4           5.0.6           Highly available Kubernetes implementation of R...
stable/sensu                            0.2.3           0.28            Sensu monitoring framework backed by the Redis ...

[/simterm]

Используем bitnami/redis 11.2.3, добавляем dependencies в Chart.yaml родительского чарта:

...
dependencies:
- name: redis
  version: ~11.2
  repository: "@bitnami"

Добавляем репозиторий:

[simterm]

$ helm repo add bitnami https://charts.bitnami.com/bitnami

[/simterm]

Запускаем проверку:

[simterm]

$ helm install backend-redis . --dry-run -f env/dev/values.yaml --debug
install.go:172: [debug] Original chart version: ""
install.go:189: [debug] CHART PATH: /home/setevoy/Work/devops-kubernetes/projects/backend/services/backend-redis

Error: found in Chart.yaml, but missing in charts/ directory: redis
helm.go:94: [debug] found in Chart.yaml, but missing in charts/ directory: redis
...

[/simterm]

Error: found in Chart.yaml, but missing in charts/ directory: redis” – ага, да. Забыл. Обновляем зависимости:

[simterm]

$ helm dependency update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "equinor-charts" chart repository
...Successfully got an update from the "bitnami" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈Happy Helming!⎈
Saving 1 charts
Downloading redis from repo https://charts.bitnami.com/bitnami
Deleting outdated charts

[/simterm]

Проверяем архив сабчарта:

[simterm]

$ ll charts/
total 64
-rw-r--r-- 1 setevoy setevoy 64195 Oct 28 16:30 redis-11.2.3.tgz

[/simterm]

Запускаем ещё раз:

[simterm]

$ helm install backend-redis . --dry-run -f env/dev/values.yaml --debug

[/simterm]

И если всё хорошо – запускаем установку:

[simterm]

$ helm upgrade --install --namespace eks-dev-1-backend-redis-ns --create-namespace --atomic backend-redis . -f env/dev/values.yaml --set redis.global.redis.password=p@ssw0rd --debug
history.go:53: [debug] getting history for release backend-redis
Release "backend-redis" does not exist. Installing it now.
install.go:172: [debug] Original chart version: ""
install.go:189: [debug] CHART PATH: /home/setevoy/Work/devops-kubernetes/projects/backend/services/backend-redis

client.go:108: [debug] creating 1 resource(s)
client.go:108: [debug] creating 9 resource(s)
wait.go:53: [debug] beginning wait for 9 resources with timeout of 5m0s
wait.go:206: [debug] Service does not have load balancer ingress IP address: eks-dev-1-backend-redis-ns/backend-redis
wait.go:329: [debug] StatefulSet is not ready: eks-dev-1-backend-redis-ns/backend-redis-node. 1 out of 2 expected pods have been scheduled
...

[/simterm]

Обратите внимание, что в --set параметр password мы передаём тоже с именем сабчарта, а затем нужный блок – redis.global.redis.password.

Проверяем сервисы:

[simterm]

$ kk -n eks-dev-1-backend-redis-ns get svc
NAME                     TYPE           CLUSTER-IP       EXTERNAL-IP                                                                        PORT(S)                          AGE
backend-redis            LoadBalancer   172.20.153.106   internal-a5d0d8f5a5d4a4438a2ca06610886d2f-1400935130.us-east-2.elb.amazonaws.com   6379:30362/TCP,26379:31326/TCP   70s
backend-redis-headless   ClusterIP      None             <none>                                                                             6379/TCP,26379/TCP               70s
backend-redis-metrics    ClusterIP      172.20.50.77     <none>                                                                             9121/TCP                         70s                                                                      9121/TCP                         21s

[/simterm]

Поды:

[simterm]

$ kk -n eks-dev-1-backend-redis-ns get pod
NAME                   READY   STATUS    RESTARTS   AGE
backend-redis-node-0   3/3     Running   0          37s
backend-redis-node-1   3/3     Running   0          21s

[/simterm]

Проверяем работу Redis:

[simterm]

admin@bttrm-dev-app-1:~$ redis-cli -h internal-a5d0d8f5a5d4a4438a2ca06610886d2f-1400935130.us-east-2.elb.amazonaws.com -p 6379 -a p@ssw0rd info replication
# Replication
role:master
connected_slaves:1
slave0:ip=10.3.45.195,port=6379,state=online,offset=15261,lag=1
...

[/simterm]

Отлично – осталось только создать Jenkins-джобу.

Тут уже всё стандартно, по аналогии с джобами из Helm: пошаговое создание чарта и деплоймента из Jenkins.

Jenkins deploy job

Создаём Jenkins Pipeline Job.

Parameters

Описываем параметры:

Тут будут:

  • APP_CHART_NAME: имя родительского чарта,
  • AWS_EKS_NAMESPACE: Kubernetes Namespace, в который деплоим
  • AWS_EKS_CLUSTER: Kubernetes кластер, в который деплоим и для которого генерируется .kube/config для kubectl
  • APP_ENV: используется для подстановки в backend-redis/env/$APP_ENV/values.yaml и для verify(), см. Jenkins: Scripted Pipeline — подтверждение выполнения для Production окружения
  • APP_REPO_URL: репозиторий с чартом
  • APP_REPO_BRANCH: бранч в этом репозитории

Структура каталогов у нас получилась такой:

[simterm]

projects/
└── backend
    └── services
        └── backend-redis
            ├── Chart.lock
            ├── charts
            │   └── redis-11.2.3.tgz
            ├── Chart.yaml
            ├── env
            │   ├── dev
            │   │   └── values.yaml
            │   ├── prod
            │   └── stage
            ├── templates
            └── values.yaml

[/simterm]

Создаём Password Parameter с паролем, который будет задан Redis:

Pipeline script

Далее, описываем сам пайплайн:

// ask confirmation for build if APP_ENV == prod
def verify() {

        def userInput = input(
            id: 'userInput', message: 'This is PRODUCTION!', parameters: [
            [$class: 'BooleanParameterDefinition', defaultValue: false, description: '', name: 'Please confirm you sure to proceed']
        ])
        
        if(!userInput) {
            error "Build wasn't confirmed"
        }
}

// Add slack Notification
def notifySlack(String buildStatus = 'STARTED') {

    // Build status of null means success.
    buildStatus = buildStatus ?: 'SUCCESS'

    def color
    //change for another slack chanel
    def token = 'devops-alarms-ci-slack-notification'

    if (buildStatus == 'STARTED') {
        color = '#D4DADF'
    } else if (buildStatus == 'SUCCESS') {
        color = '#BDFFC3'
    } else if (buildStatus == 'UNSTABLE') {
            color = '#FFFE89'
    } else {
        color = '#FF9FA1'
    }

    def msg = "${buildStatus}: `${env.JOB_NAME}` #${env.BUILD_NUMBER}:\n${env.BUILD_URL}"
    slackSend(color: color, message: msg, tokenCredentialId: token)
}

node {

    try { 

    docker.image('projectname/kubectl-aws:4.1').inside('-v /var/run/docker.sock:/var/run/docker.sock') {
        
        stage('Verify') {
           switch("${env.APP_ENV}") {
               case "prod":
                   verify()
                   echo "Run job"
                   break;
               default:
                   echo "Dev deploy"
                   break;
            }
        }

        stage('Init') {
            
            gitenv = git branch: '${APP_REPO_BRANCH}',
                         credentialsId: 'jenkins-projectname-github',
                         url: '${APP_REPO_URL}'
        
            GIT_COMMIT_SHORT = gitenv.GIT_COMMIT.take(8)
            RELEASE_VERSION = "${BUILD_NUMBER}"
            APP_VERSION = "${BUILD_NUMBER}.${GIT_COMMIT_SHORT}"
            AWS_EKS_REGION = "us-east-2"
            
            echo "APP_VERSION: ${APP_VERSION}"
            echo "RELEASE_VERSION: ${RELEASE_VERSION}"
            
            sh "aws eks update-kubeconfig --region ${AWS_EKS_REGION} --name ${AWS_EKS_CLUSTER}"
            sh "kubectl cluster-info"
            sh "helm version"
            sh "helm -n ${AWS_EKS_NAMESPACE} ls "
        }

        // install dependencies
        stage("Helm dependencies") {

            dir("projects/backend/services/${APP_CHART_NAME}") {
                sh "helm repo add bitnami https://charts.bitnami.com/bitnami"
                sh "helm dependency update"
            }
        }

        // lint the chart
        stage("Helm lint") {
            
            dir("projects/backend/services/") {
                sh "helm lint ${APP_CHART_NAME} -f ${APP_CHART_NAME}/env/${APP_ENV}/values.yaml"
            }
        }

        // just to create --app-version
        stage("Helm package") {

            dir("projects/backend/services/") {
                sh "helm package ${APP_CHART_NAME} --version ${RELEASE_VERSION} --app-version ${APP_VERSION}"
            }
        }
        
        // --dry-run first, if OK then run install
        stage("Helm install") {
            
            dir("projects/backend/services/") {
                sh "helm secrets upgrade --install --namespace ${AWS_EKS_NAMESPACE} --create-namespace --atomic ${APP_CHART_NAME} ${APP_CHART_NAME}-${RELEASE_VERSION}.tgz -f ${APP_CHART_NAME}/env/${APP_ENV}/values.yaml --set redis.global.redis.password=${REDIS_PASSWORD} --debug --dry-run"
                sh "helm secrets upgrade --install --namespace ${AWS_EKS_NAMESPACE} --create-namespace --atomic ${APP_CHART_NAME} ${APP_CHART_NAME}-${RELEASE_VERSION}.tgz -f ${APP_CHART_NAME}/env/${APP_ENV}/values.yaml --set redis.global.redis.password=${REDIS_PASSWORD} --debug"
            }
        }

        stage("Helm info") {
            
            dir("projects/backend/services/") {
                sh "helm ls -a -d --namespace ${AWS_EKS_NAMESPACE}"
                sh "helm get manifest --namespace ${AWS_EKS_NAMESPACE} ${APP_CHART_NAME}"
            }
        }
    }

    // send Slack notification if Jenkins build fails
    } catch (e) {
        currentBuild.result = 'FAILURE'
        notifySlack(currentBuild.result)
        throw e
    }
}

Увдомления в Slack описаны в Jenkins: уведомление в Slack из Jenkins Scripted Pipeline.

Запускаем:

Проверяем:

[simterm]

$ helm -n eks-dev-1-backend-redis-ns ls
NAME            NAMESPACE                       REVISION        UPDATED                                 STATUS          CHART                   APP VERSION
backend-redis   eks-dev-1-backend-redis-ns      2               2020-10-29 14:19:52.407047243 +0000 UTC deployed        backend-redis-13        13.6da15d3e

[/simterm]

Готово.