Архив метки: Kubernetes

Введение в Kubernetes и создание собственного кластера

Kubernetes (K8s) — это популярная и мощная система управления контейнерами, разработанная компанией Google. Она предоставляет средства для автоматизации развертывания, масштабирования и управления контейнеризированными приложениями. Kubernetes обеспечивает надежное и гибкое окружение для разработчиков и операционных специалистов, позволяя им сосредоточиться на разработке приложений, не беспокоясь о деталях инфраструктуры.

Одним из ключевых преимуществ Kubernetes является возможность создания собственного кластера при помощи https://cloud.obit.ru/, который можно настроить и настроить под конкретные потребности вашего проекта.

Кроме создания кластера cloud.obit.ru предоставляет услуги:

  • vStack cloud
  • VMware cloud
  • Хранилище 1С
  • Облако
  • Kubernetes
  • и другие услуги.

 

В этой статье мы рассмотрим процесс создания собственного кластера Kubernetes.

 

Шаг 1: Выбор платформы

Первым шагом в создании кластера kubernetes является выбор платформы, на которой он будет работать. Kubernetes может работать на различных облачных провайдерах, таких как Amazon Web Services (AWS), Google Cloud Platform (GCP) и Microsoft Azure. Вы также можете развернуть кластер Kubernetes на собственном оборудовании, используя физические серверы или виртуальные машины.

 

Шаг 2: Установка Kubernetes

После выбора платформы необходимо установить Kubernetes на выбранную инфраструктуру. В случае использования облачного провайдера это может включать в себя создание экземпляров виртуальных машин и настройку сетевых правил. Если вы развертываете Kubernetes на собственном оборудовании, вам понадобится установить и настроить необходимое программное обеспечение, включая контейнерный движок, такой как Docker.

 

Шаг 3: Конфигурация кластера

После установки Kubernetes необходимо настроить кластер, определив его параметры и поведение. Кластер Kubernetes состоит из нескольких узлов, включая мастер-узлы, ответственные за управление и координацию кластером, и рабочие узлы, на которых развертываются и запускаются контейнеры. Вам нужно будет указать количество и типы узлов, сетевые настройки и другие параметры, в зависимости от вашего проекта.

 

Шаг 4: Запуск и управление приложениями

После настройки кластера Kubernetes вы можете начать развертывать и управлять приложениями. Kubernetes предоставляет мощные механизмы для описания и управления приложениями в виде файлов конфигурации, называемых манифестами. Вы определяете требования к ресурсам, количество экземпляров, сетевые настройки и другие параметры приложения в манифесте, а затем Kubernetes берет на себя ответственность за развертывание и управление этими приложениями на узлах кластера.

 

Шаг 5: Масштабирование и обслуживание

Одним из основных преимуществ Kubernetes является его способность масштабировать приложения в зависимости от нагрузки. Вы можете настроить автомасштабирование, чтобы Kubernetes автоматически добавлял или удалял экземпляры приложения в зависимости от текущей нагрузки. Кроме того, Kubernetes обеспечивает возможность обновления приложений без прерывания обслуживания, позволяя развертывать новые версии приложений и переключаться на них без простоев.

 

Заключение

Создание собственного кластера Kubernetes может быть немного сложным процессом, но это предоставляет вам полный контроль над вашей инфраструктурой и приложениями. Kubernetes обладает богатым набором возможностей и инструментов, которые позволяют автоматизировать и упростить управление контейнеризированными приложениями в различных сценариях. Надеюсь, эта статья помогла вам получить общее представление о том, как создать свой собственный кластер Kubernetes и начать использовать его для развертывания и масштабирования ваших приложений.



2023-05-26T00:00:57
Программное обеспечение

Kubernetes. установка elk из helm чарта xpack ilm

Для начала создаём namespase elk




kubectl create namespace elk




ну или




cat namespace.yml




kind: Namespace

apiVersion: v1

metadata:

  name: elk




kubectl apply -f namespace.yml




выкачиваем чарт




git clone https://github.com/elastic/helm-charts.git
cd helm-charts/
git checkout 7.9




Далее нам надо сгенерировать сертификаты на основе которых будет всё работать.




[root@prod-vsrv-kubemaster1 helm-charts]# mkdir mkdir /certs
[root@prod-vsrv-kubemaster1 helm-charts]# cd /certs/




Создаём файл в котором укажем наши доменные имена (logstash требователен к наличию домена в сертификате)




cat cnf




[req]

distinguished_name = req_distinguished_name

x509_extensions = v3_req

prompt = no



[req_distinguished_name]

C = KG

ST = Bishkek

L = Bishkek

O = test

CN = elasticsearch-master



[v3_req]

subjectKeyIdentifier = hash

authorityKeyIdentifier = keyid,issuer

basicConstraints = CA:TRUE

subjectAltName = @alt_names



[alt_names]

DNS.1 = elasticsearch-master

DNS.2 = kibana-kibana

DNS.3 = elasticsearch-master-headless

DNS.4 = logstash-logstash-headless

DNS.5 = elk.prod.test.local

DNS.6 = elasticsearch-master-0

DNS.7 = elasticsearch-master-1

DNS.8 = elasticsearch-master-2





openssl genpkey -aes-256-cbc -pass pass:123456789 -algorithm RSA -out mysite.key -pkeyopt rsa_keygen_bits:3072




Тут надо будет ввести пароль при генерации а дальше заполнить данные сертификата.
Я везде задал пароль 123456789
openssl req -new -x509 -key mysite.key -sha256 -config cnf -out mysite.crt -days 7300
Enter pass phrase for mysite.key:




создаём сертификат p12 который нужен elastic




openssl pkcs12 -export -in mysite.crt -inkey mysite.key -out identity.p12 -name «mykey»
Enter pass phrase for mysite.key:   вот тут вводим наш пароль 123456789
Enter Export Password:   ТУТ ОСТАВЛЯЕМ БЕЗ ПАРОЛЯ
Verifying — Enter Export Password:  ТУТ ОСТАВЛЯЕМ БЕЗ ПАРОЛЯ




docker run —rm -v /certs:/certs -it openjdk:oracle keytool -import -file /certs/mysite.crt -keystore /certs/trust.jks -storepass 123456789




появится сообщение в котором мы соглашаемся, что доверяем сертификату





Вытаскиваем приватный ключ чтоб он у нас был без пароля
openssl rsa -in mysite.key -out mysite-without-pass.key
Enter pass phrase for mysite.key:
writing RSA key




Всё готово, все нужные сертификаты для elasticsearch сгенерированы:




[root@prod-vsrv-kubemaster1 certs]# ll
total 32
-rw-r—r— 1 root root 575 Feb 10 10:15 cnf
-rw-r—r— 1 root root 3624 Feb 10 10:15 identity.p12
-rw-r—r— 1 root root 1935 Feb 10 10:15 mysite.crt
-rw-r—r— 1 root root 2638 Feb 10 10:15 mysite.key
-rw-r—r— 1 root root 2459 Feb 10 10:39 mysite-without-pass.key
-rw-r—r— 1 root root 1682 Feb 10 10:15 trust.jks




Создаём секрет с сертификатами
[root@prod-vsrv-kubemaster1 certs]# kubectl create secret generic elastic-certificates -n elk —from-file=identity.p12 —from-file=mysite.crt —from-file=mysite.key —from-file=mysite-without-pass.key —from-file=trust.jks




Создаём секрет с логином и паролем для elastic:
kubectl create secret generic secret-basic-auth -n elk —from-literal=password=elastic —from-literal=username=elastic




отметим что у нас в кластере уже установлен nfs-provisioner




Перейдём к настройке переменных у elastic




elasticsearch/values.yaml
Тут включаем xpack добавляем сертификаты и указываем директорию для снапшотов




esConfig:

  elasticsearch.yml: |

    path.repo: /snapshot

    xpack.security.enabled: true

    xpack.security.transport.ssl.enabled: true

    xpack.security.transport.ssl.verification_mode: certificate

    xpack.security.transport.ssl.keystore.path: /usr/share/elasticsearch/config/certs/identity.p12

    xpack.security.transport.ssl.truststore.path: /usr/share/elasticsearch/config/certs/identity.p12

    xpack.security.http.ssl.enabled: true

    xpack.security.http.ssl.truststore.path: /usr/share/elasticsearch/config/certs/identity.p12

    xpack.security.http.ssl.keystore.path: /usr/share/elasticsearch/config/certs/identity.p12





path.repo: /snapshot   — это наша директория для снапшотов, они будут сохраняться в отдельном volume




также добавляем в переменные наш логин и пароль что мы указали при создании секрета




extraEnvs:

  - name: ELASTIC_PASSWORD

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: password

  - name: ELASTIC_USERNAME

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: username




также подключаем директорию для сертификатов




secretMounts:

  - name: elastic-certificates

    secretName: elastic-certificates

    path: /usr/share/elasticsearch/config/certs





также указываем наш nfs провижинер




volumeClaimTemplate:

  accessModes: [ "ReadWriteOnce" ]

  storageClassName: nfs-storageclass

  resources:

    requests:

      storage: 3Gi




также выставляем antiAffinity soft (так как у нас 2 воркера а эластик запускается в 3х подах он не может стартануть) эта настройка говорит что на одной ноде могут быть запущены 2 пода из кластера уэластика.




antiAffinity: «soft»




также правим протокол на https




protocol: https




весь файл будет иметь следующий вид:




[root@prod-vsrv-kubemaster1 helm-charts]# cat elasticsearch/values.yaml




---

clusterName: "elasticsearch"

nodeGroup: "master"



# The service that non master groups will try to connect to when joining the cluster

# This should be set to clusterName + "-" + nodeGroup for your master group

masterService: ""



# Elasticsearch roles that will be applied to this nodeGroup

# These will be set as environment variables. E.g. node.master=true

roles:

  master: "true"

  ingest: "true"

  data: "true"

  remote_cluster_client: "true"

# ml: "true" # ml is not availble with elasticsearch-oss



replicas: 3

minimumMasterNodes: 2



esMajorVersion: ""



# Allows you to add any config files in /usr/share/elasticsearch/config/

# such as elasticsearch.yml and log4j2.properties

esConfig:

  elasticsearch.yml: |

    path.repo: /snapshot

    xpack.security.enabled: true

    xpack.security.transport.ssl.enabled: true

    xpack.security.transport.ssl.verification_mode: certificate

    xpack.security.transport.ssl.keystore.path: /usr/share/elasticsearch/config/certs/identity.p12

    xpack.security.transport.ssl.truststore.path: /usr/share/elasticsearch/config/certs/identity.p12

    xpack.security.http.ssl.enabled: true

    xpack.security.http.ssl.truststore.path: /usr/share/elasticsearch/config/certs/identity.p12

    xpack.security.http.ssl.keystore.path: /usr/share/elasticsearch/config/certs/identity.p12



#    key:

#      nestedkey: value

#  log4j2.properties: |

#    key = value



# Extra environment variables to append to this nodeGroup

# This will be appended to the current 'env:' key. You can use any of the kubernetes env

# syntax here

extraEnvs:

  - name: ELASTIC_PASSWORD

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: password

  - name: ELASTIC_USERNAME

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: username





#  - name: MY_ENVIRONMENT_VAR

#    value: the_value_goes_here



# Allows you to load environment variables from kubernetes secret or config map

envFrom: []

# - secretRef:

#     name: env-secret

# - configMapRef:

#     name: config-map



# A list of secrets and their paths to mount inside the pod

# This is useful for mounting certificates for security and for mounting

# the X-Pack license

secretMounts:

  - name: elastic-certificates

    secretName: elastic-certificates

    path: /usr/share/elasticsearch/config/certs

#    defaultMode: 0755



image: "docker.elastic.co/elasticsearch/elasticsearch"

imageTag: "7.9.4-SNAPSHOT"

imagePullPolicy: "IfNotPresent"



podAnnotations: {}

  # iam.amazonaws.com/role: es-cluster



# additionals labels

labels: {}



esJavaOpts: "-Xmx1g -Xms1g"



resources:

  requests:

    cpu: "1000m"

    memory: "2Gi"

  limits:

    cpu: "1000m"

    memory: "2Gi"



initResources: {}

  # limits:

  #   cpu: "25m"

  #   # memory: "128Mi"

  # requests:

  #   cpu: "25m"

  #   memory: "128Mi"



sidecarResources: {}

  # limits:

  #   cpu: "25m"

  #   # memory: "128Mi"

  # requests:

  #   cpu: "25m"

  #   memory: "128Mi"



networkHost: "0.0.0.0"



volumeClaimTemplate:

  accessModes: [ "ReadWriteOnce" ]

  storageClassName: nfs-storageclass

  resources:

    requests:

      storage: 3Gi



rbac:

  create: false

  serviceAccountAnnotations: {}

  serviceAccountName: ""



podSecurityPolicy:

  create: false

  name: ""

  spec:

    privileged: true

    fsGroup:

      rule: RunAsAny

    runAsUser:

      rule: RunAsAny

    seLinux:

      rule: RunAsAny

    supplementalGroups:

      rule: RunAsAny

    volumes:

      - secret

      - configMap

      - persistentVolumeClaim



persistence:

  enabled: true

  labels:

    # Add default labels for the volumeClaimTemplate fo the StatefulSet

    enabled: false

  annotations: {}



extraVolumes: []

  #  - name: extras

  #    emptyDir: {}



extraVolumeMounts: []

  # - name: extras

  #   mountPath: /usr/share/extras

  #   readOnly: true





extraContainers: []

  # - name: do-something

  #   image: busybox

  #   command: ['do', 'something']



extraInitContainers: []

  # - name: do-something

  #   image: busybox

  #   command: ['do', 'something']



# This is the PriorityClass settings as defined in

# https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass

priorityClassName: ""



# By default this will make sure two pods don't end up on the same node

# Changing this to a region would allow you to spread pods across regions

antiAffinityTopologyKey: "kubernetes.io/hostname"



# Hard means that by default pods will only be scheduled if there are enough nodes for them

# and that they will never end up on the same node. Setting this to soft will do this "best effort"

antiAffinity: "soft"



# This is the node affinity settings as defined in

# https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#node-affinity-beta-feature

nodeAffinity: {}



# The default is to deploy all pods serially. By setting this to parallel all pods are started at

# the same time when bootstrapping the cluster

podManagementPolicy: "Parallel"



# The environment variables injected by service links are not used, but can lead to slow Elasticsearch boot times when

# there are many services in the current namespace.

# If you experience slow pod startups you probably want to set this to `false`.

enableServiceLinks: true



protocol: https

httpPort: 9200

transportPort: 9300



service:

  labels: {}

  labelsHeadless: {}

  type: ClusterIP

  nodePort: ""

  annotations: {}

  httpPortName: http

  transportPortName: transport

  loadBalancerIP: ""

  loadBalancerSourceRanges: []

  externalTrafficPolicy: ""



updateStrategy: RollingUpdate



# This is the max unavailable setting for the pod disruption budget

# The default value of 1 will make sure that kubernetes won't allow more than 1

# of your pods to be unavailable during maintenance

maxUnavailable: 1



podSecurityContext:

  fsGroup: 1000

  runAsUser: 1000



securityContext:

  capabilities:

    drop:

    - ALL

  # readOnlyRootFilesystem: true

  runAsNonRoot: true

  runAsUser: 1000



# How long to wait for elasticsearch to stop gracefully

terminationGracePeriod: 120



sysctlVmMaxMapCount: 262144



readinessProbe:

  failureThreshold: 3

  initialDelaySeconds: 10

  periodSeconds: 10

  successThreshold: 3

  timeoutSeconds: 5



# https://www.elastic.co/guide/en/elasticsearch/reference/7.9/cluster-health.html#request-params wait_for_status

clusterHealthCheckParams: "wait_for_status=green&timeout=1s"



## Use an alternate scheduler.

## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/

##

schedulerName: ""



imagePullSecrets: []

nodeSelector: {}

tolerations: []



# Enabling this will publically expose your Elasticsearch instance.

# Only enable this if you have security enabled on your cluster

ingress:

  enabled: false

  annotations: {}

    # kubernetes.io/ingress.class: nginx

    # kubernetes.io/tls-acme: "true"

  path: /

  hosts:

    - chart-example.local

  tls: []

  #  - secretName: chart-example-tls

  #    hosts:

  #      - chart-example.local



nameOverride: ""

fullnameOverride: ""



# https://github.com/elastic/helm-charts/issues/63

masterTerminationFix: false



lifecycle: {}

  # preStop:

  #   exec:

  #     command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]

  # postStart:

  #   exec:

  #     command:

  #       - bash

  #       - -c

  #       - |

  #         #!/bin/bash

  #         # Add a template to adjust number of shards/replicas

  #         TEMPLATE_NAME=my_template

  #         INDEX_PATTERN="logstash-*"

  #         SHARD_COUNT=8

  #         REPLICA_COUNT=1

  #         ES_URL=http://localhost:9200

  #         while [[ "$(curl -s -o /dev/null -w '%{http_code}n' $ES_URL)" != "200" ]]; do sleep 1; done

  #         curl -XPUT "$ES_URL/_template/$TEMPLATE_NAME" -H 'Content-Type: application/json' -d'{"index_patterns":['""$INDEX_PATTERN""'],"settings":{"number_of_shards":'$SHARD_COUNT',"number_of_replicas":'$REPLICA_COUNT'}}'



sysctlInitContainer:

  enabled: true



keystore: []



# Deprecated

# please use the above podSecurityContext.fsGroup instead

fsGroup: ""





========================================================================================




перейдём к настройке kibana:




kibana/values.yaml




Правим http на https
elasticsearchHosts: «https://elasticsearch-master:9200»




также




extraEnvs:

  - name: 'ELASTICSEARCH_USERNAME'

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: username

  - name: 'ELASTICSEARCH_PASSWORD'

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: password





также подключаем директорию с сертификатами




secretMounts:

  - name: elastic-certificates

    secretName: elastic-certificates

    path: /usr/share/kibana/config/certs





также добавляем в основной конфиг пути до сертификатов




kibanaConfig:

   kibana.yml: |

    server.ssl:

      enabled: true

      key: /usr/share/kibana/config/certs/mysite-without-pass.key

      certificate: /usr/share/kibana/config/certs/mysite.crt

    xpack.security.encryptionKey: "something_at_least_32_characters"

    elasticsearch.ssl:

      certificateAuthorities: /usr/share/kibana/config/certs/mysite.crt

      verificationMode: certificate





также настраиваем ingress чтобы по нашему домену открывалась кибана, отметим что строка:
nginx.ingress.kubernetes.io/backend-protocol: «HTTPS»
обязательна так как без неё ингрес по умолчанию проксирует всё на HTTP




ingress:

  enabled: true

  annotations:

    # kubernetes.io/ingress.class: nginx

    # kubernetes.io/tls-acme: "true"

     nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"

  path: /

  hosts:

    - elk.prod.test.local





весь файл будет иметь вид:




 cat kibana/values.yaml




---

elasticsearchHosts: "https://elasticsearch-master:9200"



replicas: 1



# Extra environment variables to append to this nodeGroup

# This will be appended to the current 'env:' key. You can use any of the kubernetes env

# syntax here

extraEnvs:

  - name: 'ELASTICSEARCH_USERNAME'

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: username

  - name: 'ELASTICSEARCH_PASSWORD'

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: password



#  - name: "NODE_OPTIONS"

#    value: "--max-old-space-size=1800"

#  - name: MY_ENVIRONMENT_VAR

#    value: the_value_goes_here



# Allows you to load environment variables from kubernetes secret or config map

envFrom: []

# - secretRef:

#     name: env-secret

# - configMapRef:

#     name: config-map



# A list of secrets and their paths to mount inside the pod

# This is useful for mounting certificates for security and for mounting

# the X-Pack license

secretMounts:

  - name: elastic-certificates

    secretName: elastic-certificates

    path: /usr/share/kibana/config/certs



#  - name: kibana-keystore

#    secretName: kibana-keystore

#    path: /usr/share/kibana/data/kibana.keystore

#    subPath: kibana.keystore # optional



image: "docker.elastic.co/kibana/kibana"

imageTag: "7.9.4-SNAPSHOT"

imagePullPolicy: "IfNotPresent"



# additionals labels

labels: {}



podAnnotations: {}

  # iam.amazonaws.com/role: es-cluster



resources:

  requests:

    cpu: "1000m"

    memory: "2Gi"

  limits:

    cpu: "1000m"

    memory: "2Gi"



protocol: https



serverHost: "0.0.0.0"



healthCheckPath: "/app/kibana"



# Allows you to add any config files in /usr/share/kibana/config/

# such as kibana.yml

kibanaConfig:

   kibana.yml: |

    server.ssl:

      enabled: true

      key: /usr/share/kibana/config/certs/mysite-without-pass.key

      certificate: /usr/share/kibana/config/certs/mysite.crt

    xpack.security.encryptionKey: "something_at_least_32_characters"

    elasticsearch.ssl:

      certificateAuthorities: /usr/share/kibana/config/certs/mysite.crt

      verificationMode: certificate



#   kibana.yml: |

#     key:

#       nestedkey: value



# If Pod Security Policy in use it may be required to specify security context as well as service account



podSecurityContext:

  fsGroup: 1000



securityContext:

  capabilities:

    drop:

    - ALL

  # readOnlyRootFilesystem: true

  runAsNonRoot: true

  runAsUser: 1000



serviceAccount: ""



# This is the PriorityClass settings as defined in

# https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass

priorityClassName: ""



httpPort: 5601



extraContainers: ""

# - name: dummy-init

#   image: busybox

#   command: ['echo', 'hey']



extraInitContainers: ""

# - name: dummy-init

#   image: busybox

#   command: ['echo', 'hey']



updateStrategy:

  type: "Recreate"



service:

  type: ClusterIP

  loadBalancerIP: ""

  port: 5601

  nodePort: ""

  labels: {}

  annotations: {}

    # cloud.google.com/load-balancer-type: "Internal"

    # service.beta.kubernetes.io/aws-load-balancer-internal: 0.0.0.0/0

    # service.beta.kubernetes.io/azure-load-balancer-internal: "true"

    # service.beta.kubernetes.io/openstack-internal-load-balancer: "true"

    # service.beta.kubernetes.io/cce-load-balancer-internal-vpc: "true"

  loadBalancerSourceRanges: []

    # 0.0.0.0/0



ingress:

  enabled: true

  annotations:

    # kubernetes.io/ingress.class: nginx

    # kubernetes.io/tls-acme: "true"

     nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"

  path: /

  hosts:

    - elk.prod.test.local

  tls: []

  #  - secretName: chart-example-tls

  #    hosts:

  #      - chart-example.local



readinessProbe:

  failureThreshold: 3

  initialDelaySeconds: 10

  periodSeconds: 10

  successThreshold: 3

  timeoutSeconds: 5



imagePullSecrets: []

nodeSelector: {}

tolerations: []

affinity: {}



nameOverride: ""

fullnameOverride: ""



lifecycle: {}

  # preStop:

  #   exec:

  #     command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]

  # postStart:

  #   exec:

  #     command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]



# Deprecated - use only with versions < 6.6

elasticsearchURL: "" # "http://elasticsearch-master:9200"





==========================================================================================




перейдём к настройке logstash




logstash/values.yaml




включаем xpack




logstashConfig:

  logstash.yml: |

    http.host: 0.0.0.0

    xpack.monitoring.enabled: true

    xpack.monitoring.elasticsearch.username: '${ELASTICSEARCH_USERNAME}'

    xpack.monitoring.elasticsearch.password: '${ELASTICSEARCH_PASSWORD}'

    xpack.monitoring.elasticsearch.hosts: [ "https://elasticsearch-master:9200" ]

    xpack.monitoring.elasticsearch.ssl.certificate_authority: /usr/share/logstash/config/certs/mysite.crt





также настраиваем  приём на порт 5045 так как 5044 поднимается автоматически и если оставить input с 5044 то будет конфликт портов, настраиваем также фильтр по неймспейсу если приходят логи из неймспейса terminal-soft мы к ним добавляем тэг, убираем пару лишних  полей и отправляем в ластик, указывая имя индекса имя ilm политики  -которая должна быть предварительно создана.




logstashPipeline:

  logstash.conf: |

    input {

       exec { command => "uptime" interval => 30 }

       beats {

       port => 5045

       }

    }



    filter {

        if [kubernetes][namespace] == "terminal-soft" {

         mutate {

          add_tag => "tag-terminal-soft"

          remove_field => ["[agent][name]","[agent][version]","[host][mac]","[host][ip]"] }

        }

       }



    output {

      if "tag-terminal-soft" in [tags] {

        elasticsearch {

            hosts => [ "https://elasticsearch-master:9200" ]

            cacert => "/usr/share/logstash/config/certs/mysite.crt"

            manage_template => false

            index => "terminal-soft-%{+YYYY.MM.dd}"

            ilm_rollover_alias => "terminal-soft"

            ilm_policy => "terminal-soft"

            user => '${ELASTICSEARCH_USERNAME}'

            password => '${ELASTICSEARCH_PASSWORD}'

        }

      }

    }





также указываем логин пароль с которым будем подключаться к elasticearch




extraEnvs:

  - name: 'ELASTICSEARCH_USERNAME'

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: username

  - name: 'ELASTICSEARCH_PASSWORD'

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: password





также




secretMounts:

  - name: elastic-certificates

    secretName: elastic-certificates

    path: /usr/share/logstash/config/certs





также




antiAffinity: «soft»




также раскоментим сервис и укажем в нём порт куда всё пересылать 5045 (его мы задали выше в input)




service:

  annotations: {}

  type: ClusterIP

  ports:

    - name: beats

      port: 5044

      protocol: TCP

      targetPort: 5045





весь файл имеет вид:




[root@prod-vsrv-kubemaster1 helm-charts]# cat logstash/values.yaml




---

replicas: 1



# Allows you to add any config files in /usr/share/logstash/config/

# such as logstash.yml and log4j2.properties

#

# Note that when overriding logstash.yml, `http.host: 0.0.0.0` should always be included

# to make default probes work.

logstashConfig:

  logstash.yml: |

    http.host: 0.0.0.0

    xpack.monitoring.enabled: true

    xpack.monitoring.elasticsearch.username: '${ELASTICSEARCH_USERNAME}'

    xpack.monitoring.elasticsearch.password: '${ELASTICSEARCH_PASSWORD}'

    xpack.monitoring.elasticsearch.hosts: [ "https://elasticsearch-master:9200" ]

    xpack.monitoring.elasticsearch.ssl.certificate_authority: /usr/share/logstash/config/certs/mysite.crt



#    key:

#      nestedkey: value

#  log4j2.properties: |

#    key = value



# Allows you to add any pipeline files in /usr/share/logstash/pipeline/

### ***warn*** there is a hardcoded logstash.conf in the image, override it first

logstashPipeline:

  logstash.conf: |

    input {

       exec { command => "uptime" interval => 30 }

       beats {

       port => 5045

       }

    }



    filter {

        if [kubernetes][namespace] == "terminal-soft" {

         mutate {

          add_tag => "tag-terminal-soft"

          remove_field => ["[agent][name]","[agent][version]","[host][mac]","[host][ip]"] }

        }

       }



    output {

      if "tag-terminal-soft" in [tags] {

        elasticsearch {

            hosts => [ "https://elasticsearch-master:9200" ]

            cacert => "/usr/share/logstash/config/certs/mysite.crt"

            manage_template => false

            index => "terminal-soft-%{+YYYY.MM.dd}"

            ilm_rollover_alias => "terminal-soft"

            ilm_policy => "terminal-soft"

            user => '${ELASTICSEARCH_USERNAME}'

            password => '${ELASTICSEARCH_PASSWORD}'

        }

      }

    }





#    input {

#      exec {

#        command => "uptime"

#        interval => 30

#      }

#    }

#    output { stdout { } }



# Extra environment variables to append to this nodeGroup

# This will be appended to the current 'env:' key. You can use any of the kubernetes env

# syntax here

extraEnvs:

  - name: 'ELASTICSEARCH_USERNAME'

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: username

  - name: 'ELASTICSEARCH_PASSWORD'

    valueFrom:

      secretKeyRef:

        name: secret-basic-auth

        key: password



#  - name: MY_ENVIRONMENT_VAR

#    value: the_value_goes_here



# Allows you to load environment variables from kubernetes secret or config map

envFrom: []

# - secretRef:

#     name: env-secret

# - configMapRef:

#     name: config-map



# Add sensitive data to k8s secrets

secrets: []

#  - name: "env"

#    value:

#      ELASTICSEARCH_PASSWORD: "LS1CRUdJTiBgUFJJVkFURSB"

#      api_key: ui2CsdUadTiBasRJRkl9tvNnw

#  - name: "tls"

#    value:

#      ca.crt: |

#        LS0tLS1CRUdJT0K

#        LS0tLS1CRUdJT0K

#        LS0tLS1CRUdJT0K

#        LS0tLS1CRUdJT0K

#      cert.crt: "LS0tLS1CRUdJTiBlRJRklDQVRFLS0tLS0K"

#      cert.key.filepath: "secrets.crt" # The path to file should be relative to the `values.yaml` file.





# A list of secrets and their paths to mount inside the pod

secretMounts:

  - name: elastic-certificates

    secretName: elastic-certificates

    path: /usr/share/logstash/config/certs





image: "docker.elastic.co/logstash/logstash"

imageTag: "7.9.4-SNAPSHOT"

imagePullPolicy: "IfNotPresent"

imagePullSecrets: []



podAnnotations: {}



# additionals labels

labels: {}



logstashJavaOpts: "-Xmx1g -Xms1g"



resources:

  requests:

    cpu: "100m"

    memory: "1536Mi"

  limits:

    cpu: "1000m"

    memory: "1536Mi"



volumeClaimTemplate:

  accessModes: [ "ReadWriteOnce" ]

  resources:

    requests:

      storage: 1Gi



rbac:

  create: false

  serviceAccountAnnotations: {}

  serviceAccountName: ""



podSecurityPolicy:

  create: false

  name: ""

  spec:

    privileged: true

    fsGroup:

      rule: RunAsAny

    runAsUser:

      rule: RunAsAny

    seLinux:

      rule: RunAsAny

    supplementalGroups:

      rule: RunAsAny

    volumes:

      - secret

      - configMap

      - persistentVolumeClaim



persistence:

  enabled: false

  annotations: {}



extraVolumes: ""

  # - name: extras

  #   emptyDir: {}



extraVolumeMounts: ""

  # - name: extras

  #   mountPath: /usr/share/extras

  #   readOnly: true



extraContainers: ""

  # - name: do-something

  #   image: busybox

  #   command: ['do', 'something']



extraInitContainers: ""

  # - name: do-something

  #   image: busybox

  #   command: ['do', 'something']



# This is the PriorityClass settings as defined in

# https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass

priorityClassName: ""



# By default this will make sure two pods don't end up on the same node

# Changing this to a region would allow you to spread pods across regions

antiAffinityTopologyKey: "kubernetes.io/hostname"



# Hard means that by default pods will only be scheduled if there are enough nodes for them

# and that they will never end up on the same node. Setting this to soft will do this "best effort"

antiAffinity: "soft"



# This is the node affinity settings as defined in

# https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#node-affinity-beta-feature

nodeAffinity: {}



# The default is to deploy all pods serially. By setting this to parallel all pods are started at

# the same time when bootstrapping the cluster

podManagementPolicy: "Parallel"



httpPort: 9600



# Custom ports to add to logstash

extraPorts: []

#  - name: beats

#    containerPort: 5044



updateStrategy: RollingUpdate



# This is the max unavailable setting for the pod disruption budget

# The default value of 1 will make sure that kubernetes won't allow more than 1

# of your pods to be unavailable during maintenance

maxUnavailable: 1



podSecurityContext:

  fsGroup: 1000

  runAsUser: 1000



securityContext:

  capabilities:

    drop:

    - ALL

  # readOnlyRootFilesystem: true

  runAsNonRoot: true

  runAsUser: 1000



# How long to wait for logstash to stop gracefully

terminationGracePeriod: 120



# Probes

# Default probes are using `httpGet` which requires that `http.host: 0.0.0.0` is part of

# `logstash.yml`. If needed probes can be disabled or overrided using the following syntaxes:

#

# disable livenessProbe

# livenessProbe: null

#

# replace httpGet default readinessProbe by some exec probe

# readinessProbe:

#   httpGet: null

#   exec:

#     command:

#       - curl

#      - localhost:9600



livenessProbe:

  httpGet:

    path: /

    port: http

  initialDelaySeconds: 300

  periodSeconds: 10

  timeoutSeconds: 5

  failureThreshold: 3

  successThreshold: 1



readinessProbe:

  httpGet:

    path: /

    port: http

  initialDelaySeconds: 60

  periodSeconds: 10

  timeoutSeconds: 5

  failureThreshold: 3

  successThreshold: 3



## Use an alternate scheduler.

## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/

##

schedulerName: ""



nodeSelector: {}

tolerations: []



nameOverride: ""

fullnameOverride: ""



lifecycle: {}

  # preStop:

  #   exec:

  #     command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]

  # postStart:

  #   exec:

  #     command: ["/bin/sh", "-c", "echo Hello from the postStart handler > /usr/share/message"]



service:

  annotations: {}

  type: ClusterIP

  ports:

    - name: beats

      port: 5044

      protocol: TCP

      targetPort: 5045

#    - name: http

#      port: 8080

#      protocol: TCP

#      targetPort: 8080



ingress:

  enabled: false

#  annotations: {}

#  hosts:

#    - host: logstash.local

#      paths:

#        - path: /logs

#          servicePort: 8080

#  tls: []





===========================================================================================




Перейдём к настройке filebeat




filebeat/values.yaml




filebeatConfig:

  filebeat.yml: |

    filebeat.inputs:

    - type: container

      paths:

        - /var/log/containers/*.log

      processors:

      - add_kubernetes_metadata:

          host: ${NODE_NAME}

          matchers:

          - logs_path:

              logs_path: "/var/log/containers/"



    output.logstash:

      enabled: true

      hosts: ["logstash-logstash:5044"]





полностью файл будет выглядеть следующим образом:




[root@prod-vsrv-kubemaster1 helm-charts]# cat filebeat/values.yaml




---

# Allows you to add any config files in /usr/share/filebeat

# such as filebeat.yml

filebeatConfig:

  filebeat.yml: |

    filebeat.inputs:

    - type: container

      paths:

        - /var/log/containers/*.log

      processors:

      - add_kubernetes_metadata:

          host: ${NODE_NAME}

          matchers:

          - logs_path:

              logs_path: "/var/log/containers/"



    output.logstash:

      enabled: true

      hosts: ["logstash-logstash:5044"]



# Extra environment variables to append to the DaemonSet pod spec.

# This will be appended to the current 'env:' key. You can use any of the kubernetes env

# syntax here

extraEnvs: []

#  - name: MY_ENVIRONMENT_VAR

#    value: the_value_goes_here



extraVolumeMounts: []

  # - name: extras

  #   mountPath: /usr/share/extras

  #   readOnly: true



extraVolumes: []

  # - name: extras

  #   emptyDir: {}



extraContainers: ""

# - name: dummy-init

#   image: busybox

#   command: ['echo', 'hey']



extraInitContainers: []

# - name: dummy-init

#   image: busybox

#   command: ['echo', 'hey']



envFrom: []

# - configMapRef:

#     name: configmap-name



# Root directory where Filebeat will write data to in order to persist registry data across pod restarts (file position and other metadata).

hostPathRoot: /var/lib

hostNetworking: false

dnsConfig: {}

# options:

#   - name: ndots

#     value: "2"

image: "docker.elastic.co/beats/filebeat"

imageTag: "7.9.4-SNAPSHOT"

imagePullPolicy: "IfNotPresent"

imagePullSecrets: []



livenessProbe:

  exec:

    command:

      - sh

      - -c

      - |

        #!/usr/bin/env bash -e

        curl --fail 127.0.0.1:5066

  failureThreshold: 3

  initialDelaySeconds: 10

  periodSeconds: 10

  timeoutSeconds: 5



readinessProbe:

  exec:

    command:

      - sh

      - -c

      - |

        #!/usr/bin/env bash -e

        filebeat test output

  failureThreshold: 3

  initialDelaySeconds: 10

  periodSeconds: 10

  timeoutSeconds: 5



# Whether this chart should self-manage its service account, role, and associated role binding.

managedServiceAccount: true



# additionals labels

labels: {}



podAnnotations: {}

  # iam.amazonaws.com/role: es-cluster



# Various pod security context settings. Bear in mind that many of these have an impact on Filebeat functioning properly.

#

# - User that the container will execute as. Typically necessary to run as root (0) in order to properly collect host container logs.

# - Whether to execute the Filebeat containers as privileged containers. Typically not necessarily unless running within environments such as OpenShift.

podSecurityContext:

  runAsUser: 0

  privileged: false



resources:

  requests:

    cpu: "100m"

    memory: "100Mi"

  limits:

    cpu: "1000m"

    memory: "200Mi"



# Custom service account override that the pod will use

serviceAccount: ""



# Annotations to add to the ServiceAccount that is created if the serviceAccount value isn't set.

serviceAccountAnnotations: {}

  # eks.amazonaws.com/role-arn: arn:aws:iam::111111111111:role/k8s.clustername.namespace.serviceaccount



# A list of secrets and their paths to mount inside the pod

# This is useful for mounting certificates for security other sensitive values

secretMounts: []

#  - name: filebeat-certificates

#    secretName: filebeat-certificates

#    path: /usr/share/filebeat/certs



# How long to wait for Filebeat pods to stop gracefully

terminationGracePeriod: 30



tolerations: []



nodeSelector: {}



affinity: {}



# This is the PriorityClass settings as defined in

# https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass

priorityClassName: ""



updateStrategy: RollingUpdate



# Override various naming aspects of this chart

# Only edit these if you know what you're doing

nameOverride: ""

fullnameOverride: ""





==========================================================================================




Filebeat multiline.




Появилась задача: логи с контейнера в котором джава приложение приходят построчно, т.е. каждая строка это отдельный месседж при отображении в kibana, это не удобно читать, чтоб их объеденить в одно сообщение, добавим в filebeat фильтр




multiline.pattern: ‘^([0-9]{4}-[0-9]{2}-[0-9]{2})’
multiline.negate: true
multiline.match: after




в общем виде:




vim filebeat/values.yaml




---

# Allows you to add any config files in /usr/share/filebeat

# such as filebeat.yml

filebeatConfig:

  filebeat.yml: |

    filebeat.inputs:

    - type: container

      paths:

        - /var/log/containers/*.log

      processors:

      - add_kubernetes_metadata:

          host: ${NODE_NAME}

          matchers:

          - logs_path:

              logs_path: "/var/log/containers/"

      multiline.pattern: '^([0-9]{4}-[0-9]{2}-[0-9]{2})'

      multiline.negate: true

      multiline.match: after



    output.logstash:

      enabled: true

      hosts: ["logstash-logstash:5044"]





и обновляем наш чарт:
helm upgrade —install filebeat -n elk —values filebeat/values.yaml filebeat/




всё теперь логи будут формироваться относительно даты в самом начале сообщения.




===========================================================================================




Дополнение для работы snapshot




Создаём PV который дальше будем подкидывать в template elasticsearch  (напоминаю что у меня nfs-provisioner)
cat pvc-snapshot.yml




apiVersion: v1

kind: PersistentVolumeClaim

metadata:

  name: elasticsearch-snapshot-dir

  namespace: elk

  labels:

   app: elasticsearch-snapshot

spec:

  storageClassName: nfs-storageclass

  accessModes:

    - ReadWriteMany

  resources:

    requests:

      storage: 5Gi





запускаем




kubectl apply -f pvc-snapshot.yml




смотрим имя созданного PV




kubectl get pv -n elk | grep elk

pvc-30e262ad-770c-45ad-8e3c-28d70a6400ef   5Gi        RWX            Delete           Bound    elk/elasticsearch-snapshot-dir                                                                                              nfs-storageclass              21h





вот наше имя




pvc-30e262ad-770c-45ad-8e3c-28d70a6400ef




теперь правим temaplate




vim elasticsearch/templates/statefulset.yaml




      volumes:

        - name: "pvc-30e262ad-770c-45ad-8e3c-28d70a6400ef"

          persistentVolumeClaim:

            claimName: elasticsearch-snapshot-dir





И в этом же файле в ещё одном месте:




        volumeMounts:

          - name: pvc-30e262ad-770c-45ad-8e3c-28d70a6400ef

            mountPath: "/snapshot"





на всякий случай файл имеет вид:




cat elasticsearch/templates/statefulset.yaml




---

apiVersion: {{ template "elasticsearch.statefulset.apiVersion" . }}

kind: StatefulSet

metadata:

  name: {{ template "elasticsearch.uname" . }}

  labels:

    heritage: {{ .Release.Service | quote }}

    release: {{ .Release.Name | quote }}

    chart: "{{ .Chart.Name }}"

    app: "{{ template "elasticsearch.uname" . }}"

    {{- range $key, $value := .Values.labels }}

    {{ $key }}: {{ $value | quote }}

    {{- end }}

  annotations:

    esMajorVersion: "{{ include "elasticsearch.esMajorVersion" . }}"

spec:

  serviceName: {{ template "elasticsearch.uname" . }}-headless

  selector:

    matchLabels:

      app: "{{ template "elasticsearch.uname" . }}"

  replicas: {{ .Values.replicas }}

  podManagementPolicy: {{ .Values.podManagementPolicy }}

  updateStrategy:

    type: {{ .Values.updateStrategy }}

  {{- if .Values.persistence.enabled }}

  volumeClaimTemplates:

  - metadata:

      name: {{ template "elasticsearch.uname" . }}

    {{- if .Values.persistence.labels.enabled }}

      labels:

        heritage: {{ .Release.Service | quote }}

        release: {{ .Release.Name | quote }}

        chart: "{{ .Chart.Name }}"

        app: "{{ template "elasticsearch.uname" . }}"

        {{- range $key, $value := .Values.labels }}

        {{ $key }}: {{ $value | quote }}

        {{- end }}

    {{- end }}

    {{- with .Values.persistence.annotations  }}

      annotations:

{{ toYaml . | indent 8 }}

    {{- end }}

    spec:

{{ toYaml .Values.volumeClaimTemplate | indent 6 }}

  {{- end }}

  template:

    metadata:

      name: "{{ template "elasticsearch.uname" . }}"

      labels:

        heritage: {{ .Release.Service | quote }}

        release: {{ .Release.Name | quote }}

        chart: "{{ .Chart.Name }}"

        app: "{{ template "elasticsearch.uname" . }}"

        {{- range $key, $value := .Values.labels }}

        {{ $key }}: {{ $value | quote }}

        {{- end }}

      annotations:

        {{- range $key, $value := .Values.podAnnotations }}

        {{ $key }}: {{ $value | quote }}

        {{- end }}

        {{/* This forces a restart if the configmap has changed */}}

        {{- if .Values.esConfig }}

        configchecksum: {{ include (print .Template.BasePath "/configmap.yaml") . | sha256sum | trunc 63 }}

        {{- end }}

    spec:

      {{- if .Values.schedulerName }}

      schedulerName: "{{ .Values.schedulerName }}"

      {{- end }}

      securityContext:

{{ toYaml .Values.podSecurityContext | indent 8 }}

        {{- if .Values.fsGroup }}

        fsGroup: {{ .Values.fsGroup }} # Deprecated value, please use .Values.podSecurityContext.fsGroup

        {{- end }}

      {{- if .Values.rbac.create }}

      serviceAccountName: "{{ template "elasticsearch.uname" . }}"

      {{- else if not (eq .Values.rbac.serviceAccountName "") }}

      serviceAccountName: {{ .Values.rbac.serviceAccountName | quote }}

      {{- end }}

      {{- with .Values.tolerations }}

      tolerations:

{{ toYaml . | indent 6 }}

      {{- end }}

      {{- with .Values.nodeSelector }}

      nodeSelector:

{{ toYaml . | indent 8 }}

      {{- end }}

      {{- if or (eq .Values.antiAffinity "hard") (eq .Values.antiAffinity "soft") .Values.nodeAffinity }}

      {{- if .Values.priorityClassName }}

      priorityClassName: {{ .Values.priorityClassName }}

      {{- end }}

      affinity:

      {{- end }}

      {{- if eq .Values.antiAffinity "hard" }}

        podAntiAffinity:

          requiredDuringSchedulingIgnoredDuringExecution:

          - labelSelector:

              matchExpressions:

              - key: app

                operator: In

                values:

                - "{{ template "elasticsearch.uname" .}}"

            topologyKey: {{ .Values.antiAffinityTopologyKey }}

      {{- else if eq .Values.antiAffinity "soft" }}

        podAntiAffinity:

          preferredDuringSchedulingIgnoredDuringExecution:

          - weight: 1

            podAffinityTerm:

              topologyKey: {{ .Values.antiAffinityTopologyKey }}

              labelSelector:

                matchExpressions:

                - key: app

                  operator: In

                  values:

                  - "{{ template "elasticsearch.uname" . }}"

      {{- end }}

      {{- with .Values.nodeAffinity }}

        nodeAffinity:

{{ toYaml . | indent 10 }}

      {{- end }}

      terminationGracePeriodSeconds: {{ .Values.terminationGracePeriod }}

      volumes:

        - name: "pvc-30e262ad-770c-45ad-8e3c-28d70a6400ef"

          persistentVolumeClaim:

            claimName: elasticsearch-snapshot-dir



        {{- range .Values.secretMounts }}

        - name: {{ .name }}

          secret:

            secretName: {{ .secretName }}

            {{- if .defaultMode }}

            defaultMode: {{ .defaultMode }}

            {{- end }}

        {{- end }}

        {{- if .Values.esConfig }}

        - name: esconfig

          configMap:

            name: {{ template "elasticsearch.uname" . }}-config

        {{- end }}

{{- if .Values.keystore }}

        - name: keystore

          emptyDir: {}

        {{- range .Values.keystore }}

        - name: keystore-{{ .secretName }}

          secret: {{ toYaml . | nindent 12 }}

        {{- end }}

{{ end }}

      {{- if .Values.extraVolumes }}

      # Currently some extra blocks accept strings

      # to continue with backwards compatibility this is being kept

      # whilst also allowing for yaml to be specified too.

      {{- if eq "string" (printf "%T" .Values.extraVolumes) }}

{{ tpl .Values.extraVolumes . | indent 8 }}

      {{- else }}

{{ toYaml .Values.extraVolumes | indent 8 }}

      {{- end }}

      {{- end }}

      {{- if .Values.imagePullSecrets }}

      imagePullSecrets:

{{ toYaml .Values.imagePullSecrets | indent 8 }}

      {{- end }}

      {{- if semverCompare ">1.13-0" .Capabilities.KubeVersion.GitVersion }}

      enableServiceLinks: {{ .Values.enableServiceLinks }}

      {{- end }}

      initContainers:

      {{- if .Values.sysctlInitContainer.enabled }}

      - name: configure-sysctl

        securityContext:

          runAsUser: 0

          privileged: true

        image: "{{ .Values.image }}:{{ .Values.imageTag }}"

        imagePullPolicy: "{{ .Values.imagePullPolicy }}"

        command: ["sysctl", "-w", "vm.max_map_count={{ .Values.sysctlVmMaxMapCount}}"]

        resources:

{{ toYaml .Values.initResources | indent 10 }}

      {{- end }}

{{ if .Values.keystore }}

      - name: keystore

        image: "{{ .Values.image }}:{{ .Values.imageTag }}"

        imagePullPolicy: "{{ .Values.imagePullPolicy }}"

        command:

        - sh

        - -c

        - |

          #!/usr/bin/env bash

          set -euo pipefail



          elasticsearch-keystore create



          for i in /tmp/keystoreSecrets/*/*; do

            key=$(basename $i)

            echo "Adding file $i to keystore key $key"

            elasticsearch-keystore add-file "$key" "$i"

          done



          # Add the bootstrap password since otherwise the Elasticsearch entrypoint tries to do this on startup

          if [ ! -z ${ELASTIC_PASSWORD+x} ]; then

            echo 'Adding env $ELASTIC_PASSWORD to keystore as key bootstrap.password'

            echo "$ELASTIC_PASSWORD" | elasticsearch-keystore add -x bootstrap.password

          fi



          cp -a /usr/share/elasticsearch/config/elasticsearch.keystore /tmp/keystore/

        env: {{ toYaml .Values.extraEnvs | nindent 10 }}

        envFrom: {{ toYaml .Values.envFrom | nindent 10 }}

        resources: {{ toYaml .Values.initResources | nindent 10 }}

        volumeMounts:

          - name: keystore

            mountPath: /tmp/keystore

          {{- range .Values.keystore }}

          - name: keystore-{{ .secretName }}

            mountPath: /tmp/keystoreSecrets/{{ .secretName }}

          {{- end }}

{{ end }}

      {{- if .Values.extraInitContainers }}

      # Currently some extra blocks accept strings

      # to continue with backwards compatibility this is being kept

      # whilst also allowing for yaml to be specified too.

      {{- if eq "string" (printf "%T" .Values.extraInitContainers) }}

{{ tpl .Values.extraInitContainers . | indent 6 }}

      {{- else }}

{{ toYaml .Values.extraInitContainers | indent 6 }}

      {{- end }}

      {{- end }}

      containers:

      - name: "{{ template "elasticsearch.name" . }}"

        securityContext:

{{ toYaml .Values.securityContext | indent 10 }}

        image: "{{ .Values.image }}:{{ .Values.imageTag }}"

        imagePullPolicy: "{{ .Values.imagePullPolicy }}"

        readinessProbe:

          exec:

            command:

              - sh

              - -c

              - |

                #!/usr/bin/env bash -e

                # If the node is starting up wait for the cluster to be ready (request params: "{{ .Values.clusterHealthCheckParams }}" )

                # Once it has started only check that the node itself is responding

                START_FILE=/tmp/.es_start_file



                # Disable nss cache to avoid filling dentry cache when calling curl

                # This is required with Elasticsearch Docker using nss < 3.52

                export NSS_SDB_USE_CACHE=no



                http () {

                  local path="${1}"

                  local args="${2}"

                  set -- -XGET -s



                  if [ "$args" != "" ]; then

                    set -- "$@" $args

                  fi



                  if [ -n "${ELASTIC_USERNAME}" ] && [ -n "${ELASTIC_PASSWORD}" ]; then

                    set -- "$@" -u "${ELASTIC_USERNAME}:${ELASTIC_PASSWORD}"

                  fi



                  curl --output /dev/null -k "$@" "{{ .Values.protocol }}://127.0.0.1:{{ .Values.httpPort }}${path}"

                }



                if [ -f "${START_FILE}" ]; then

                  echo 'Elasticsearch is already running, lets check the node is healthy'

                  HTTP_CODE=$(http "/" "-w %{http_code}")

                  RC=$?

                  if [[ ${RC} -ne 0 ]]; then

                    echo "curl --output /dev/null -k -XGET -s -w '%{http_code}' ${BASIC_AUTH} {{ .Values.protocol }}://127.0.0.1:{{ .Values.httpPort }}/ failed with RC ${RC}"

                    exit ${RC}

                  fi

                  # ready if HTTP code 200, 503 is tolerable if ES version is 6.x

                  if [[ ${HTTP_CODE} == "200" ]]; then

                    exit 0

                  elif [[ ${HTTP_CODE} == "503" && "{{ include "elasticsearch.esMajorVersion" . }}" == "6" ]]; then

                    exit 0

                  else

                    echo "curl --output /dev/null -k -XGET -s -w '%{http_code}' ${BASIC_AUTH} {{ .Values.protocol }}://127.0.0.1:{{ .Values.httpPort }}/ failed with HTTP code ${HTTP_CODE}"

                    exit 1

                  fi



                else

                  echo 'Waiting for elasticsearch cluster to become ready (request params: "{{ .Values.clusterHealthCheckParams }}" )'

                  if http "/_cluster/health?{{ .Values.clusterHealthCheckParams }}" "--fail" ; then

                    touch ${START_FILE}

                    exit 0

                  else

                    echo 'Cluster is not yet ready (request params: "{{ .Values.clusterHealthCheckParams }}" )'

                    exit 1

                  fi

                fi

{{ toYaml .Values.readinessProbe | indent 10 }}

        ports:

        - name: http

          containerPort: {{ .Values.httpPort }}

        - name: transport

          containerPort: {{ .Values.transportPort }}

        resources:

{{ toYaml .Values.resources | indent 10 }}

        env:

          - name: node.name

            valueFrom:

              fieldRef:

                fieldPath: metadata.name

          {{- if eq .Values.roles.master "true" }}

          {{- if ge (int (include "elasticsearch.esMajorVersion" .)) 7 }}

          - name: cluster.initial_master_nodes

            value: "{{ template "elasticsearch.endpoints" . }}"

          {{- else }}

          - name: discovery.zen.minimum_master_nodes

            value: "{{ .Values.minimumMasterNodes }}"

          {{- end }}

          {{- end }}

          {{- if lt (int (include "elasticsearch.esMajorVersion" .)) 7 }}

          - name: discovery.zen.ping.unicast.hosts

            value: "{{ template "elasticsearch.masterService" . }}-headless"

          {{- else }}

          - name: discovery.seed_hosts

            value: "{{ template "elasticsearch.masterService" . }}-headless"

          {{- end }}

          - name: cluster.name

            value: "{{ .Values.clusterName }}"

          - name: network.host

            value: "{{ .Values.networkHost }}"

          - name: ES_JAVA_OPTS

            value: "{{ .Values.esJavaOpts }}"

          {{- range $role, $enabled := .Values.roles }}

          - name: node.{{ $role }}

            value: "{{ $enabled }}"

          {{- end }}

{{- if .Values.extraEnvs }}

{{ toYaml .Values.extraEnvs | indent 10 }}

{{- end }}

{{- if .Values.envFrom }}

        envFrom:

{{ toYaml .Values.envFrom | indent 10 }}

{{- end }}

        volumeMounts:

          - name: pvc-30e262ad-770c-45ad-8e3c-28d70a6400ef

            mountPath: "/snapshot"

          {{- if .Values.persistence.enabled }}

          - name: "{{ template "elasticsearch.uname" . }}"

            mountPath: /usr/share/elasticsearch/data

          {{- end }}

{{ if .Values.keystore }}

          - name: keystore

            mountPath: /usr/share/elasticsearch/config/elasticsearch.keystore

            subPath: elasticsearch.keystore

{{ end }}

          {{- range .Values.secretMounts }}

          - name: {{ .name }}

            mountPath: {{ .path }}

            {{- if .subPath }}

            subPath: {{ .subPath }}

            {{- end }}

          {{- end }}

          {{- range $path, $config := .Values.esConfig }}

          - name: esconfig

            mountPath: /usr/share/elasticsearch/config/{{ $path }}

            subPath: {{ $path }}

          {{- end -}}

        {{- if .Values.extraVolumeMounts }}

        # Currently some extra blocks accept strings

        # to continue with backwards compatibility this is being kept

        # whilst also allowing for yaml to be specified too.

        {{- if eq "string" (printf "%T" .Values.extraVolumeMounts) }}

{{ tpl .Values.extraVolumeMounts . | indent 10 }}

        {{- else }}

{{ toYaml .Values.extraVolumeMounts | indent 10 }}

        {{- end }}

        {{- end }}

      {{- if .Values.masterTerminationFix }}

      {{- if eq .Values.roles.master "true" }}

      # This sidecar will prevent slow master re-election

      # https://github.com/elastic/helm-charts/issues/63

      - name: elasticsearch-master-graceful-termination-handler

        image: "{{ .Values.image }}:{{ .Values.imageTag }}"

        imagePullPolicy: "{{ .Values.imagePullPolicy }}"

        command:

        - "sh"

        - -c

        - |

          #!/usr/bin/env bash

          set -eo pipefail



          http () {

              local path="${1}"

              if [ -n "${ELASTIC_USERNAME}" ] && [ -n "${ELASTIC_PASSWORD}" ]; then

                BASIC_AUTH="-u ${ELASTIC_USERNAME}:${ELASTIC_PASSWORD}"

              else

                BASIC_AUTH=''

              fi

              curl -XGET -s -k --fail ${BASIC_AUTH} {{ .Values.protocol }}://{{ template "elasticsearch.masterService" . }}:{{ .Values.httpPort }}${path}

          }



          cleanup () {

            while true ; do

              local master="$(http "/_cat/master?h=node" || echo "")"

              if [[ $master == "{{ template "elasticsearch.masterService" . }}"* && $master != "${NODE_NAME}" ]]; then

                echo "This node is not master."

                break

              fi

              echo "This node is still master, waiting gracefully for it to step down"

              sleep 1

            done



            exit 0

          }



          trap cleanup SIGTERM



          sleep infinity &

          wait $!

        resources:

{{ toYaml .Values.sidecarResources | indent 10 }}

        env:

          - name: NODE_NAME

            valueFrom:

              fieldRef:

                fieldPath: metadata.name

        {{- if .Values.extraEnvs }}

{{ toYaml .Values.extraEnvs | indent 10 }}

        {{- end }}

        {{- if .Values.envFrom }}

        envFrom:

{{ toYaml .Values.envFrom | indent 10 }}

        {{- end }}

      {{- end }}

      {{- end }}

{{- if .Values.lifecycle }}

        lifecycle:

{{ toYaml .Values.lifecycle | indent 10 }}

{{- end }}

      {{- if .Values.extraContainers }}

      # Currently some extra blocks accept strings

      # to continue with backwards compatibility this is being kept

      # whilst also allowing for yaml to be specified too.

      {{- if eq "string" (printf "%T" .Values.extraContainers) }}

{{ tpl .Values.extraContainers . | indent 6 }}

      {{- else }}

{{ toYaml .Values.extraContainers | indent 6 }}

      {{- end }}

      {{- end }}





===========================================================================================




Начинаем установку:




[root@prod-vsrv-kubemaster1 helm-charts]# helm install elasticsearch -n elk —values elasticsearch/values.yaml elasticsearch/




[root@prod-vsrv-kubemaster1 helm-charts]# helm install kibana -n elk —values kibana/values.yaml kibana/




ждём когда запустится кибана и переходим по домену указанному в ingress у кибана в переменных








Сюда вбиваем наши логин и пароль из секрета созданного в самом начале, напомню elastic elastic




Переходим в
Stack Management Index Management index templates






на втором пункте нажимаем next, на третьем пишем @timespamp и выбираем date





потом нажимаем next далее:





теперь создаём политику на ролловер и удаление









Запускаем logstash и filebeat настраиваем snapshot




[root@prod-vsrv-kubemaster1 helm-charts]# helm install logstash -n elk —values logstash/values.yaml logstash/




[root@prod-vsrv-kubemaster1 helm-charts]# helm install filebeat -n elk —values filebeat/values.yaml filebeat/




теперь создаём репозиторий для снапшотов и их политику






тут указываем нашу директорию /snapshot она подключалась через отдельный вольюм






далее вводим
name terminal-soft




snapshot name:  <terminal-soft-{now/d}>    (кавычки обязательны)










Роли и пользователей можно настраивать тут:





======================================================================================================================================================




запускаем elastic в 1 instance




vim elasticsearch/values.yaml




replicas: 1

minimumMasterNodes: 1




[root@prod-vsrv-kubemaster1 helm-charts-7.9.4.elasticsearch-1-pod]# helm install elasticsearch -n elk —values elasticsearch/values.yaml elasticsearch/




[root@prod-vsrv-kubemaster1 helm-charts-7.9.4.elasticsearch-1-pod]# helm install kibana -n elk —values kibana/values.yaml kibana/




Создаём в kibana так же темплейт
Но задаём параметр number_of_replicas»: «0»





после запускаем logstash filebeat
[root@prod-vsrv-kubemaster1 helm-charts-7.9.4.elasticsearch-1-pod]# helm install logstash -n elk —values logstash/values.yaml logstash/
[root@prod-vsrv-kubemaster1 helm-charts-7.9.4.elasticsearch-1-pod]# helm install filebeat -n elk —values filebeat/values.yaml filebeat/
После этого настраиваем snapshot




====================================================================================================




Настройка доступов пользователю для индексов.




Создаём пространство:






указываем — что должно отображаться в пространстве:







Создаём роль для нашего индекса:





указываем привилегии как для кластера так и непосредственно для индекса terminal-soft





добавляем привилегии для пространства:





настраиваем доступы для пространства, — чтение/полный доступ/отключить






Создаём пользователя:





задаём пароль и созданную нами ранее роль:





Логинимся под нашим новым пользователем





Создаём index pattern







Проверяем:





как видим данные отображаются:





11.Восстановление из snapshot




Если требуется восстановить индексы из snapshot то делаем следующее:










Тут надо добавить параметр




«indexing_complete»: «true»
только в том случае если  при восстановлении возникает ошибка следующего вида:




Unable to restore snapshot




[illegal_state_exception] alias [terminal-soft] has more than one write index [terminal-soft-2021.03.04-000022,terminal-soft-2021.03.16-000034]








Поэтому когда производишь восстановление необходимо восстанавливать индекс из самого последнего снапшота.
т.е. Если нужен индекс за 10 число то снапшот смотрим где-то  за 15 число. Ну или добавляем пераметр «indexing_complete»: «true»




Источник: https://sidmid.ru/kubernetes-установка-elk-из-helm-чарта-xpack-ilm/



2023-05-03T01:28:39
Software

Kubernetes — Автоскейлинг приложений при помощи Prometheus и KEDA

Kubernetes позволяет автоматически масштабировать приложения (то есть Pod в развертывании или ReplicaSet) декларативным образом с использованием спецификации Horizontal Pod Autoscaler.




По умолчанию критерий для автоматического масштабирования — метрики использования CPU (метрики ресурсов), но можно интегрировать пользовательские метрики и метрики, предоставляемые извне.




Вместо горизонтального автомасштабирования подов, применяется Kubernetes Event Driven Autoscaling (KEDA) — оператор Kubernetes с открытым исходным кодом. Он изначально интегрируется с Horizontal Pod Autoscaler, чтобы обеспечить плавное автомасштабирование (в том числе до/от нуля) для управляемых событиями рабочих нагрузок. Код доступен на GitHub.




Краткий обзор работы системы





На схеме — краткое описание того, как все работает:




  1. Приложение предоставляет метрики количества обращений к HTTP в формате Prometheus.



  2. Prometheus настроен на сбор этих показателей.



  3. Скейлер Prometheus в KEDA настроен на автоматическое масштабирование приложения на основе количества обращений к HTTP.




Теперь подробно расскажу о каждом элементе.




KEDA и Prometheus




Prometheus — набор инструментов для мониторинга и оповещения систем с открытым исходным кодом, часть Cloud Native Computing Foundation. Собирает метрики из разных источников и сохраняет в виде данных временных рядов. Для визуализации данных можно использовать Grafana или другие инструменты визуализации, работающие с API Kubernetes.




KEDA поддерживает концепцию скейлера — он действует как мост между KEDA и внешней системой. Реализация скейлера специфична для каждой целевой системы и извлекает из нее данные. Затем KEDA использует их для управления автоматическим масштабированием.




Скейлеры поддерживают нескольких источников данных, например, Kafka, Redis, Prometheus. То есть KEDA можно применять для автоматического масштабирования развертываний Kubernetes, используя в качестве критериев метрики Prometheus.




KEDA Prometheus ScaledObject




Скейлер действует как мост между KEDA и внешней системой, из которой нужно получать метрики. ScaledObject — настраиваемый ресурс, его необходимо развернуть для синхронизации развертывания с источником событий, в данном случае с Prometheus.




ScaledObject содержит информацию о масштабировании развертывания, метаданные об источнике события (например, секреты для подключения, имя очереди), интервал опроса, период восстановления и другие данные. Он приводит к соответствующему ресурсу автомасштабирования (определение HPA) для масштабирования развертывания.




Когда объект ScaledObject удаляется, соответствующее ему определение HPA очищается.




Вот определение ScaledObject для нашего примера, в нем используется скейлер Prometheus:




apiVersion: keda.k8s.io/v1alpha1

kind: ScaledObject

metadata:

 name: prometheus-scaledobject

 namespace: default

 labels:

  deploymentName: go-prom-app

spec:

 scaleTargetRef:

   deploymentName: go-prom-app

 pollingInterval: 15

 cooldownPeriod:  30

 minReplicaCount: 1

 maxReplicaCount: 10

 triggers:

 - type: prometheus

  metadata:

    serverAddress:

http://prometheus-service.default.svc.cluster.local:9090

    metricName: access_frequency

    threshold: '3'

    query: sum(rate(http_requests[2m]))




Учтите следующие моменты:




  1. Он указывает на Deployment с именем go-prom-app.



  2. Тип триггера — Prometheus. Адрес сервера Prometheus упоминается вместе с именем метрики, пороговым значением и запросом PromQL, который будет использоваться. Запрос PromQL — sum(rate(http_requests[2m])).



  3. Согласно pollingInterval, KEDA запрашивает цель у Prometheus каждые пятнадцать секунд. Поддерживается минимум один под (minReplicaCount), а максимальное количество подов не превышает maxReplicaCount (в данном примере — десять).




Можно установить minReplicaCount равным нулю. В этом случае KEDA активирует развертывание с нуля до единицы, а затем предоставляет HPA для дальнейшего автоматического масштабирования. Возможен и обратный порядок, то есть масштабирование от единицы до нуля. В примере мы не выбрали ноль, поскольку это HTTP-сервис, а не система по запросу.




Магия внутри автомасштабирования




Пороговое значение используют в качестве триггера для масштабирования развертывания. В нашем примере запрос PromQL sum(rate (http_requests [2m])) возвращает агрегированное значение скорости HTTP-запросов (количество запросов в секунду), ее измеряют за последние две минуты.




Поскольку пороговое значение равно трем, значит, будет один под, пока значение sum(rate (http_requests [2m])) меньше трех. Если же значение возрастает, добавляется дополнительный под каждый раз, когда sum(rate (http_requests [2m])) увеличивается на три. Например, если значение от 12 до 14, то количество подов — четыре.




Теперь давайте попробуем настроить!




Установка KEDA




Вы можете развернуть KEDA несколькими способами, они перечислены в документации. Я использую монолитный YAML:




[root@kub-master-1 ~]# wget https://github.com/kedacore/keda/releases/download/v2.1.0/keda-2.1.0.yaml
[root@kub-master-1 ~]# kubectl apply -f keda-2.1.0.yaml




ну или можно установить через helm




helm repo add kedacore https://kedacore.github.io/charts
helm repo update
kubectl create namespace keda
helm install keda kedacore/keda —namespace keda




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




[root@kub-master-1 ~]# kubectl get all -n keda

NAME                                          READY   STATUS    RESTARTS   AGE

pod/keda-metrics-apiserver-57cbdb849f-w7rfg   1/1     Running   0          70m

pod/keda-operator-58cb545446-5rblj            1/1     Running   0          70m



NAME                             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE

service/keda-metrics-apiserver   ClusterIP   10.100.134.31   <none>        443/TCP,80/TCP   70m



NAME                                     READY   UP-TO-DATE   AVAILABLE   AGE

deployment.apps/keda-metrics-apiserver   1/1     1            1           70m

deployment.apps/keda-operator            1/1     1            1           70m



NAME                                                DESIRED   CURRENT   READY   AGE

replicaset.apps/keda-metrics-apiserver-57cbdb849f   1         1         1       70m

replicaset.apps/keda-operator-58cb545446            1         1         1       70m





3. Пример работы




создаём namespace




kubectl create ns my-site




запускаем обычное приложение например apache:




[root@kub-master-1 ~]# cat my-site.yaml




---

apiVersion: apps/v1

kind: Deployment

metadata:

  name: my-deployment-apache

  namespace: my-site

spec:

  replicas: 1

  selector:

    matchLabels:

      app: apache # по вот этому лейблу репликасет цепляет под

# тут описывается каким мокаром следует обновлять поды

  strategy:

    rollingUpdate:

      maxSurge: 1  # указывает на какое количество реплик можно увеличить

      maxUnavailable: 1 # указывает на какое количество реплик можно уменьшить

#т.е. в одно время при обновлении, будет увеличено на один (новый под) и уменьшено на один (старый под)

    type: RollingUpdate

## тут начинается описание контейнера

  template:

    metadata:

      labels:

        app: apache  # по вот этому лейблу репликасет цепляет под

    spec:

      containers:

        - image: httpd:2.4.43

          name: apache

          ports:

            - containerPort: 80

# тут начинаются проверки по доступности

          readinessProbe: # проверка готово ли приложение

            failureThreshold: 3 #указывает количество провалов при проверке

            httpGet:  # по сути дёргает курлом на 80 порт

              path: /

              port: 80

            periodSeconds: 10 #как часто должна проходить проверка (в секундах)

            successThreshold: 1 #сбрасывает счётчик неудач, т.е. при 3х проверках если 1 раз успешно прошло, то счётчик сбрасывается и всё ок

            timeoutSeconds: 1 #таймаут на выполнение пробы 1 секунда

          livenessProbe: #проверка на жизнь приложения, живо ли оно

            failureThreshold: 3

            httpGet:

              path: /

              port: 80

            periodSeconds: 10

            successThreshold: 1

            timeoutSeconds: 1

            initialDelaySeconds: 10 #означает что первую проверку надо сделать только после 10 секунд



# тут начинается описание лимитов для пода

          resources:

            requests: #количество ресурсов которые резервируются для pod на ноде

              cpu: 60m

              memory: 200Mi

            limits: #количество ресурсов которые pod может использовать(верхняя граница)

              cpu: 120m

              memory: 300Mi





[root@kub-master-1 ~]# cat my-site-service.yaml




---

apiVersion: v1

kind: Service

metadata:

  name: my-service-apache # имя сервиса

  namespace: my-site

spec:

  ports:

  - port: 80  # принимать на 80

    targetPort: 80 # отправлять на 80

  selector:

    app: apache  #отправлять на все поды с данным лейблом

  type: ClusterIP





[root@kub-master-1 ~]# cat my-site-ingress.yaml




---

apiVersion: extensions/v1beta1

kind: Ingress

metadata:

  name: my-ingress

  namespace: my-site

spec:

  rules:

  - host: test.ru  #тут указывается наш домен

    http:

      paths:  #список путей которые хотим обслуживать(он дефолтный и все запросы будут отпаврляться на бэкенд, т.е. на сервис my-service-apache)

      - backend:

          serviceName: my-service-apache  #тут указывается наш сервис 

          servicePort: 80 #порт на котором сервис слушает

#        path: /  все запросы на корень '/' будут уходить на наш сервис





применяем:




[root@kub-master-1 ~]# kubectl apply -f my-site.yaml -f my-site-service.yaml -f my-site-ingress.yaml




проверяем:




[root@kub-worker-1 ~]# curl test.ru
<html><body><h1>It works!</h1></body></html>




[root@kub-master-1 ~]# kubectl get all -n my-site




NAME                                        READY   STATUS    RESTARTS   AGE

pod/my-deployment-apache-859486bd8c-k6bql   1/1     Running   0          20m



NAME                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE

service/my-service-apache   ClusterIP   10.100.255.190   <none>        80/TCP    20m



NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE

deployment.apps/my-deployment-apache   1/1     1            1           20m



NAME                                              DESIRED   CURRENT   READY   AGE

replicaset.apps/my-deployment-apache-859486bd8c   1         1         1       20m





будем автоскейлить — для примера по метрике nginx nginx_ingress_controller_requests




запрос в prometheus будет следующий:




sum(irate( nginx_ingress_controller_requests{namespace=»my-site»}[3m] )) by (ingress)*10




т.е. считаем общее количество запросов в неймспейс my-site за 3 минуты




создаём keda сущность:




[root@kub-master-1 ~]# cat hpa-keda.yaml




apiVersion: keda.sh/v1alpha1

kind: ScaledObject

metadata:

  name: prometheus-scaledobject

  namespace: my-site

spec:

  scaleTargetRef:

    name: my-deployment-apache

  minReplicaCount: 1   # Optional. Default: 0

  maxReplicaCount: 8 # Optional. Default: 100

  triggers:

  - type: prometheus

    metadata:

      serverAddress: http://prometheus-kube-prometheus-prometheus.monitoring.svc.cluster.local:9090

      metricName: nginx_ingress_controller_requests

      threshold: '100'

      query: sum(irate(nginx_ingress_controller_requests{namespace="my-site"}[3m])) by (ingress)*10




тут мы указываем в каком namespace нам запускаться:
namespace: my-site




указываем цель, т.е. наш deployment:
name: my-deployment-apache




задаём минимальное и максимальное количество реплик
minReplicaCount: 1 # значение по умолчанию: 0
maxReplicaCount: 8 # значение по умолчанию: 100




есть ещё 2 стандартные переменные отвечающие за то когда поды будут подыматься и убиваться:
pollingInterval: 30 # Optional. Default: 30 seconds
cooldownPeriod: 300 # Optional. Default: 300 seconds




указываем адрес нашего prometheus




serverAddress: http://prometheus-kube-prometheus-prometheus.monitoring.svc.cluster.local:9090
адрес идёт в виде сервис.неймспейс.svc.имя_кластера




указываем нашу метрику:
metricName: nginx_ingress_controller_requests




указываем пороговое значение при котором начнётся автоскейлинг:
threshold: ‘100’




и соответственно наш запрос в prometheus:
query:




всё можно применять:




[root@kub-master-1 ~]# kubectl apply -f hpa-keda.yaml




проверяем:




[root@kub-master-1 ~]# kubectl get horizontalpodautoscalers.autoscaling -n my-site

NAME                               REFERENCE                         TARGETS       MINPODS   MAXPODS   REPLICAS   AGE

keda-hpa-prometheus-scaledobject   Deployment/my-deployment-apache   0/100 (avg)   1         8         1          68m





[root@kub-master-1 ~]# kubectl get pod -n my-site

NAME                                    READY   STATUS    RESTARTS   AGE

my-deployment-apache-859486bd8c-v59b8   1/1     Running   0          37m





а теперь накрутим запросов:




[root@kub-worker-1 ~]# for i in {1..5000}; do curl test.ru; done




проверяем:




[root@kub-master-1 ~]# kubectl get horizontalpodautoscalers.autoscaling -n my-site

NAME                               REFERENCE                         TARGETS            MINPODS   MAXPODS   REPLICAS   AGE

keda-hpa-prometheus-scaledobject   Deployment/my-deployment-apache   34858m/100 (avg)   1         8         7          71m





как видим количество запросов превысило наш лимит и стали создаваться новые поды:




[root@kub-master-1 ~]# kubectl get pod -n my-site

NAME                                    READY   STATUS    RESTARTS   AGE

my-deployment-apache-859486bd8c-6885f   1/1     Running   0          49s

my-deployment-apache-859486bd8c-6mcq4   1/1     Running   0          64s

my-deployment-apache-859486bd8c-cdb6z   1/1     Running   0          64s

my-deployment-apache-859486bd8c-kpwb8   1/1     Running   0          64s

my-deployment-apache-859486bd8c-rmw8d   1/1     Running   0          49s

my-deployment-apache-859486bd8c-v59b8   1/1     Running   0          39m

my-deployment-apache-859486bd8c-xmv28   1/1     Running   0          49s





прекращаем запросы и спустя 5 минут, указанное в переменной cooldownPeriod ненужные поды будут убиты:




[root@kub-master-1 ~]# kubectl get pod -n my-site

NAME                                    READY   STATUS        RESTARTS   AGE

my-deployment-apache-859486bd8c-6885f   0/1     Terminating   0          6m35s

my-deployment-apache-859486bd8c-6mcq4   1/1     Running       0          6m50s

my-deployment-apache-859486bd8c-cdb6z   0/1     Terminating   0          6m50s

my-deployment-apache-859486bd8c-kpwb8   0/1     Terminating   0          6m50s

my-deployment-apache-859486bd8c-rmw8d   0/1     Terminating   0          6m35s

my-deployment-apache-859486bd8c-v59b8   0/1     Terminating   0          44m

my-deployment-apache-859486bd8c-xmv28   0/1     Terminating   0          6m35s



[root@kub-master-1 ~]# kubectl get pod -n my-site

NAME                                    READY   STATUS    RESTARTS   AGE

my-deployment-apache-859486bd8c-6mcq4   1/1     Running   0          7m48s





Источник: https://sidmid.ru/kubernetes-автоскейлинг-приложений-при-помощ/



Установка, настройка и эксплуатация Ceph

Продолжаю цикл статей про кластерные решения, который был начат с установки kubernetes. Я расскажу как установить и настроить кластер ceph, также покажу, как им потом пользоваться. Статья с практическими примерами от и до — поднятие кластера и подключение дисков к конечным серверам.

Читать

Kubernetes: Ingress, ошибка 502, readinessProbe и livenessProbe

Имеется приложение на Go, API-бекенд.




Периодически начинает возвращать 502 ошибку, хотя сам под работает и в статусе Running.




Что бы рассмотреть, как и почему Ingress и Service могут возвращать 502, и как работают readinessProbe и livenessProbe в Kubernetes Deployment – напишем простой веб-сервер на Go, в котором опишем два ендпоинта – один будет возвращать нормальный ответ, а во втором – выполнение программы будет прерываться.




Затем задеплоим его в AWS Elastic Kubernetes, создадим Kubernetes Ingress, который создаст AWS Application Load balancer, и потрестируем работу приложения.




Golang HTTP server




Пишем приложение на Go, которое потом упакуем в Docker-контейнер, и запустим в Kubernetes:




package main



import (

    "fmt"

    "log"

    "net/http"

)

  

func main() {

    

    http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request){

        fmt.Fprintf(w, "pong")

    })

      

    http.HandleFunc("/err", func(w http.ResponseWriter, r *http.Request){

        panic("Error")

    })

      

    fmt.Printf("Starting server at port 8080n")

    if err := http.ListenAndServe(":8080", nil); err != nil {

        log.Fatal(err)

    }   

}




Тут мы запускаем http.ListenAndServe() на порту 8080, и определяем два роута:




  • /ping – при обращении сюда всегда возвращаем 200



  • /err – при обращении сюда прерываем выполнение функции с panic, что сэмулировать некорретный ответ приложения




Проверяем локально.




Запускаем:




go run http.go

Starting server at port 8080




Проверяем роут /ping:




curl -I localhost:8080/ping

HTTP/1.1 200 OK




И URI /err, который вызовет panic:




curl -I localhost:8080/err

curl: (52) Empty reply from server




Лог приложения:




go run http.go

Starting server at port 8080

2020/11/11 14:34:53 http: panic serving [::1]:43008: Error

goroutine 6 [running]:

...




Docker образ




Пишем Dockefile:




FROM golang:latest

WORKDIR /app

COPY . .

RUN go build -o main .

EXPOSE 8080

CMD ["./main"]




Собираем образ и пушим в Docker Hub:




docker build -t setevoy/go-http .

docker push setevoy/go-http




Kubernetes




Deployment




Описываем запуск пода с этим образом – создаём 1 под, Service для него, и Ingress, который создаст AWS Application Load Balancer.




Начнём с Deployment:




apiVersion: apps/v1

kind: Deployment

metadata:

  name: go-http

  labels:

    app: go-http

spec:

  replicas: 1

  strategy:

    type: Recreate

  selector: 

    matchLabels:

      app: go-http

  template:

    metadata:

      labels:

        app: go-http

    spec:

      containers:

      - name: go-http

        image: setevoy/go-http

        ports:

        - containerPort: 8080

        imagePullPolicy: Always

        livenessProbe:

          httpGet:

            path: /ping

            port: 8080

          initialDelaySeconds: 1

          periodSeconds: 1

        readinessProbe:

          httpGet:

            path: /ping

            port: 8080

          initialDelaySeconds: 1

          periodSeconds: 1

      restartPolicy: Always




Тут создаём один под, который слушает порт 8080.




В strategy деплоймента указываем Recreate, что бы при тестах не оставались старые поды.




Для него описываем проверки – livenessProbe и readinessProbe, обе проверки ходят на URI /ping, где получают ответ 200.




Позже мы поменяем путь в проверках, и посмотрим, к чему это приведёт.




Service




Создаём Kubernetes Service, который откроет на WorkerNode порт для ALB, и будет роутить трафик к нашему поду на порт 8080 (см. Kubernetes: Service, балансировка нагрузки, kube-proxy и iptables):




---

apiVersion: v1

kind: Service

metadata:

  name: go-http-svc

spec:

  type: NodePort

  ports:

  - port: 80

    targetPort: 8080

    protocol: TCP

  selector:

    app: go-http




Ingress




Добавляем Ingress, который создаст AWS Application Load Balancer, который будет направлять трафик к go-http-svc Service:




--- 

apiVersion: extensions/v1beta1

kind: Ingress

metadata:

  name: go-http-ingress

  annotations:    

    kubernetes.io/ingress.class: alb

    alb.ingress.kubernetes.io/scheme: internet-facing

    alb.ingress.kubernetes.io/healthcheck-path: /ping

    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'

  labels:   

    app: go-http

spec:         

  rules:

  - http: 

      paths:

      - path: /*

        backend: 

          serviceName: go-http-svc

          servicePort: 80




Создаём ресурсы:




kubectl apply -f go-http.yaml

deployment.apps/go-http created

service/go-http-svc created

ingress.extensions/go-http-ingress created




Проверяем под:




kubectl get pod -l app=go-http

NAME                      READY   STATUS    RESTARTS   AGE

go-http-8dc5b4779-7q4kw   1/1     Running   0          8s




И его Ingress:




kubectl get ingress -l app=go-http

NAME              HOSTS   ADDRESS                                                                 PORTS   AGE

go-http-ingress   *       e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com   80      31s




Ждём, пока наш DNS увидит новый URL e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com, и проверяем работу приложения – выполняем запрос к /ping:




curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping

HTTP/1.1 200 OK




Kubernetes Ingress 502




А теперь – обращаемся к ендпоинту /err, который в Go-приложении вызовет panic, и ловим 502 ошибку:




curl -vI e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/err

...

< HTTP/1.1 502 Bad Gateway

HTTP/1.1 502 Bad Gateway

< Server: awselb/2.0

Server: awselb/2.0




Логи пода:




kubectl logs go-http-8dc5b4779-7q4kw

Starting server at port 8080

2020/11/11 12:57:10 http: panic serving 10.3.39.145:8926: Error

goroutine 5169 [running]:

net/http.(*conn).serve.func1(0xc000260e60)

/usr/local/go/src/net/http/server.go:1801 +0x147

panic(0x654840, 0x6f0ba0)

/usr/local/go/src/runtime/panic.go:975 +0x47a

main.main.func2(0x6fa0a0, 0xc00012c540, 0xc000127700)

/app/http.go:16 +0x39

net/http.HandlerFunc.ServeHTTP(0x6bab98, 0x6fa0a0, 0xc00012c540, 0xc000127700)

/usr/local/go/src/net/http/server.go:2042 +0x44

net/http.(*ServeMux).ServeHTTP(0x8615e0, 0x6fa0a0, 0xc00012c540, 0xc000127700)

/usr/local/go/src/net/http/server.go:2417 +0x1ad

net/http.serverHandler.ServeHTTP(0xc0000ea000, 0x6fa0a0, 0xc00012c540, 0xc000127700)

/usr/local/go/src/net/http/server.go:2843 +0xa3

net/http.(*conn).serve(0xc000260e60, 0x6fa4e0, 0xc00011b8c0)

/usr/local/go/src/net/http/server.go:1925 +0x8ad

created by net/http.(*Server).Serve

/usr/local/go/src/net/http/server.go:2969 +0x36c




Тут всё логично – load-balancer отправил наш запрос к поду, под не ответил (вспомним curl: (52) Empty reply from server в наших первых тестах), и мы получили ответ 502 от балансировщика aka Ingress.




readinessProbe и livenessProbe




Теперь посмотрим, как изменение в readinessProbe и livenessProbe повлияют на ответы Ingress и работу самого пода.




readinessProbe




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




readinessProbe используется для проверки того, готов ли под принимать трафик.




Сейчас наш под в статусе Ready:




kubectl get pod -l app=go-http

NAME                      READY   STATUS    RESTARTS   AGE

go-http-8dc5b4779-7q4kw   1/1     Running   0          28m




Или так:




kubectl get pod -l app=go-http -o json | jq '.items[].status.containerStatuses[].ready'

true




И запрос к /ping возвращает нам ответ 200:




curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping

HTTP/1.1 200 OK




Меняем readinessProbe – задаём path=/err, что бы проверка постоянно фейлилась:




...

        readinessProbe:

          httpGet:

            path: /err

...




Передеплоиваем:




kubectl apply -f go-http.yaml

deployment.apps/go-http configured




И проверяем:




kubectl get pod -l app=go-http

NAME                      READY   STATUS    RESTARTS   AGE

go-http-5bd557544-2djcw   0/1     Running   0          4s




Если мы теперь отправим запрос даже на ендпоинт /ping – всё-равно получим 502, т.к. бекенд Сервиса, т.е. под, не принимает трафик, потому что не прошёл readinessProbe:




kubectl get pod -l app=go-http -o json | jq '.items[].status.containerStatuses[].ready'

false




Пробуем:




curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping

HTTP/1.1 502 Bad Gateway




livenessProbe




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




Вернём readinessProbe в /ping, что бы трафик на под пошёл, но изменим livenessProbe – зададим path в /err, а initialDelaySeconds и periodSeconds установим в 15 секунд, плюс добавим failureThreshold равным одной попытке:




...

        livenessProbe:

          httpGet:

            path: /err

            port: 8080

          initialDelaySeconds: 15

          periodSeconds: 15

          failureThreshold: 1

        readinessProbe:

          httpGet:

            path: /ping

...




Теперь после запуска пода Kubernetes выждет 15 секунд, затем выполнит livenessProbe и будет повторять её каждые следующие 15 секунд.




Передеплоиваем, и проверяем:




kubectl get pod -l app=go-http

NAME                       READY   STATUS    RESTARTS   AGE

go-http-78f6c66c8b-q6fkf   1/1     Running   0          6s




Проверяем запрос к ендпоинту /ping:




curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping

HTTP/1.1 200 OK




Всё хорошо.




И /err нам ожидаемо вернёт 502:




curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/err

HTTP/1.1 502 Bad Gateway




Через 15 секунд, после выполнения первой live проверки – под будет перезапущен:




kubectl get pod -l app=go-http

NAME                       READY   STATUS    RESTARTS   AGE

go-http-668c674dcb-4db9x   0/1     Running   1          19s




Events этого пода:




kubectl describe pod -l app=go-http

...

Normal   Created    6s (x4 over 81s)  kubelet, ip-10-3-55-229.us-east-2.compute.internal  Created container go-http                                                                                                                         

 Warning  Unhealthy  6s (x3 over 66s)  kubelet, ip-10-3-55-229.us-east-2.compute.internal  Liveness probe failed: Get http://10.3.53.103:8080/err: EOF                                                                                       

 Normal   Killing    6s (x3 over 66s)  kubelet, ip-10-3-55-229.us-east-2.compute.internal  Container go-http failed liveness probe, will be restarted                                                                                        

Normal   Started    5s (x4 over 81s)  kubelet, ip-10-3-55-229.us-east-2.compute.internal  Started container go-http




Контейнер в поде не прошёл проверку livenessProbe, и Kubernetes перезапускает под в попытке “починить” его.




Если получится попасть на сам момент перезапуска контейнера – увидим стаус CrashLoopBackOff, а запрос к /ping снова вернёт нам 502:




kubectl get pod -l app=go-http

NAME                       READY   STATUS             RESTARTS   AGE

go-http-668c674dcb-4db9x   0/1     CrashLoopBackOff   4          2m21s




Выводы




Используем readinessProbe для проверки того, что приложение запустилось, в данном случае – Go-бинарник начал прослушивать порт 8080, и на него можно направлять трафик, и используем livenessProbe во время работы пода для проверки того, что приложение в нём все ещё живо.




Если приложение начинает отдавать 502 на определённые запросы – то следует поискать проблему именно в запросах, т.к. если бы была проблема в настройках Ingress/Service – получали бы 502 постоянно.




Самое важное – понимать принципиальную разницу между readinessProbe и livenessProbe:




  • если фейлится readinessProbe – процесс aka контейнер в поде останется в том же состоянии, в котором был на момент сфейленой ready-проверки, но под будет отключен от трафика к нему



  • если фейлится livenessProbe – трафик на под продолжает идти, но контейнер будет перезапущен




Итак, имеем ввиду, что:




  • Если readinessProbe не задана вообще – kubelet будет считать, что под готов к работе, и направит к нему трафик сразу после старта пода. При этом если на  запуск пода уходит минута – то клиенты, которые к нему были направлены после его запуска, будут ждать эту минуту, пока он ответит.



  • Если приложение ловит ошибку, которую не может обработать – оно должно завершить свой процесс, и Kubernetes сам перезапустит контейнер.



  • Используем livenessProbe для проверки состояний, которые нельзя обработать в самом приложении, например – deadlock или бесконечный цикл, при которых контейнер не может ответить на ready-проверку. В таком случае если нет livenessProbe, которая может перезапустить процесс, то под будет отключен от Service – но останется в статусе Running, продолжая потреблять реурсы WorkerNode.




Источник: https://rtfm.co.ua/ru/kubernetes-ingress-oshibka-502-readinessprobe-i-livenessprobe/



2023-01-03T02:03:12
DevOps

Kubernetes: NGINX/PHP-FPM graceful shutdown – избавляемся от 502 ошибок

Имеется PHP-приложение, работает в Kubernetes в подах с двумя контейнерами – NGINX и PHP-FPM.




Проблема: во время скейлинга приложения начинают проскакивать 502 ошибки. Т.е. при остановке подов – некорректно отрабатывает завершение подключений.




Рассмотрим процесс остановки подов вообще, и особенности NGINX и PHP-FPM в частности.




Тестировать будем приложение в AWS Elastic Kubernetes Service с помощью Yandex.Tank.




Ingress создаёт AWS Application Load Balancer с помощью AWS ALB Ingress Controller.




Для управления контейнерами на Kubernetes WorkerNodes испольузется Docker.




Pod Lifecycle – Termination of Pods




Посмотрим, как вообще происходит процесс остановки и удаления подов.




Итак, под – это процесс(ы), запущенные на WorkerNode, для остановки которых используются стандартные сигналы IPC (Inter Process Communication).




Что бы дать возможность поду (процессу/ам) закончить все его текущие операции – система управления контейнерами (container runtime) пытается мягко завершить его работу (graceful shutdown), отправляя сигнал SIGTERM процессу с PID 1 в каждом контейнере этого пода (см. docker stop). При этом, кластер запускает отсчёт grace period перед тем, как жёстко убить под отправкой сигнала SIGKILL.




При этом, можно переопределить сигнал для мягкой остановки используя STOPSIGNAL в образе, из которого запускался контейнер.




Итак, процесс удаления пода выглядит следующим образом:




  1. мы выполняем kubectl delete pod или kubectl scale deployment – запускается процесс удаления подов и стартует таймер отсчёта grace period с дефолтным значением в 30 секунд



  2. API-сервер обновляет статус пода – из Running он становится Terminating (см. Container states). На WorkerNode, на которой этот под запущен, kubelet получает обновление статус этого пода и начинает процесс его остановки:

    1. если для контейнера(ов) в поде есть preStop hook – kubelet  его выполняет. Если хук продолжает выполнение после истечения grace period – добавляется ещё 2 секунды на его завершение. При необходимости можно изменить дефолтные 30 секунд используя terminationGracePeriodSeconds



    2. после завершения preStop хука – kubelet отправляет указание Docker на остановку контейнеров, и Docker отправляет сигнал SIGTERM процессу с PID 1 в каждом контейнере. При этом контейнеры в поде получают сигнал в случайном порядке.




  3. одновременно с началом процесса graceful shutdown – Kubernetes Control Plane (его kube-controller-manager) удаляет останавливаемый под из списка ендпоинтов (см. Kubernetes – Endpoints), и соответствующий Service перестаёт отправлять новые подключения на останавливаемый под



  4. по завершению grace period, kubelet триггерит force shutdown – Docker отправляет сигнал SIGKILL всем оставшимся процесам во всех контейнерах пода, который они проигнорировать не могут, и мгновенно умирают, аварийно завершая все свои операции



  5. kubelet триггерит удаление объекта пода из API-сервера



  6. API-сервер удаляет запись о поде из базы, и под становится недоступен




Наглядная табличка:





Собственно, тут возникает две проблемы:




  1. сам NGINX и PHP-FPM воспринимают SIGTERM как “жестокое убийство”, и завершают работу немедленно, не заботясь о корректном завершении текущих подключений (см. Controlling nginx и php-fpm(8) – Linux man page)



  2. шаги 2 и 3 – отправка SIGTERM и удаление ендпоинта – выполняются одновременно. Однако, обновление данных в Ingress Service происходит не моментально, и под может начать умирать раньше, чем Ingress перестанет на него слать трафик, соотвественно – получим 502, т.к. процесс в поде уже не может принимать подключения




Т.е. в первом случае, если у нас есть подключение к NGINX с keep-alive – то NGINX при выполнении fast shutdown просто обрубит его, а клиент получит 502 ошибку, см. Avoiding dropped connections in nginx containers with “STOPSIGNAL SIGQUIT”.




NGINX STOPSIGNAL и 502




Попробуем воспроизвести проблему с NGINX.




Возмём пример из поста по ссылке выше, и задеплоим его в Кубер.




Пишем Dockerfile:




FROM nginx

RUN echo 'server {n

    listen 80 default_server;n

    location / {n

      proxy_pass      http://httpbin.org/delay/10;n

    }n

}' > /etc/nginx/conf.d/default.conf

CMD ["nginx", "-g", "daemon off;"]




Тут NGINX выполняет proxy_pass на http://httpbin.org, который отвечает с задержкой в 10 секунд – эмулируем работу PHP-бекенда.




Собираем, пушим в репозиторий:




docker build -t setevoy/nginx-sigterm .

docker push setevoy/nginx-sigterm




  • Re-play



  • Copy to Clipboard



  • Pause



  • Full View




Пишем Deployment с 10 подами из собранного образа.




Тут приведу полный файл, с Namespace, Service и Ingress, далее только те части, которые будут меняться:




---

apiVersion: v1

kind: Namespace

metadata:

  name: test-namespace

---

apiVersion: apps/v1

kind: Deployment

metadata:

  name: test-deployment

  namespace: test-namespace

  labels:

    app: test

spec:

  replicas: 10

  selector:

    matchLabels:

      app: test

  template:

    metadata:

      labels:

        app: test

    spec:

      containers:

      - name: web

        image: setevoy/nginx-sigterm

        ports:

        - containerPort: 80

        resources:

          requests:

            cpu: 100m

            memory: 100Mi

        readinessProbe:

          tcpSocket:

            port: 80

---

apiVersion: v1

kind: Service

metadata:

  name: test-svc

  namespace: test-namespace

spec:

  type: NodePort

  selector:

    app: test

  ports:

    - protocol: TCP

      port: 80

      targetPort: 80

---

apiVersion: extensions/v1beta1

kind: Ingress

metadata:

  name: test-ingress

  namespace: test-namespace

  annotations:

    kubernetes.io/ingress.class: alb

    alb.ingress.kubernetes.io/scheme: internet-facing

    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'

spec:

  rules:

  - http:

      paths:

      - backend:

          serviceName: test-svc

          servicePort: 80




Деплоим:




kubectl apply -f test-deployment.yaml

namespace/test-namespace created

deployment.apps/test-deployment created

service/test-svc created

ingress.extensions/test-ingress created




Проверяем Ingress:




curl -I aadca942-testnamespace-tes-5874-698012771.us-east-2.elb.amazonaws.com

HTTP/1.1 200 OK




Запущено 10 подов:




kubectl -n test-namespace get pod

NAME                              READY   STATUS    RESTARTS   AGE

test-deployment-ccb7ff8b6-2d6gn   1/1     Running   0          26s

test-deployment-ccb7ff8b6-4scxc   1/1     Running   0          35s

test-deployment-ccb7ff8b6-8b2cj   1/1     Running   0          35s

test-deployment-ccb7ff8b6-bvzgz   1/1     Running   0          35s

test-deployment-ccb7ff8b6-db6jj   1/1     Running   0          35s

test-deployment-ccb7ff8b6-h9zsm   1/1     Running   0          20s

test-deployment-ccb7ff8b6-n5rhz   1/1     Running   0          23s

test-deployment-ccb7ff8b6-smpjd   1/1     Running   0          23s

test-deployment-ccb7ff8b6-x5dc2   1/1     Running   0          35s

test-deployment-ccb7ff8b6-zlqxs   1/1     Running   0          25s




Готовим load.yaml для Yandex.Tank:




phantom:

  address: aadca942-testnamespace-tes-5874-698012771.us-east-2.elb.amazonaws.com

  header_http: "1.1"

  headers:

     - "[Host: aadca942-testnamespace-tes-5874-698012771.us-east-2.elb.amazonaws.com]"

  uris:

    - /    

  load_profile:

    load_type: rps

    schedule: const(100,30m)

  ssl: false

console:

  enabled: true

telegraf:

  enabled: false

  package: yandextank.plugins.Telegraf

  config: monitoring.xml




Тут выполняем 1 запрос в секунду к подам за нашим Ingress (и NGINX в каждом будет ждать 10 секунд ответа от своего “бекенда” перед тем, как ответить 200 нашему Танку).




Запускаем тесты:





Пока всё хорошо.




Теперь скейлим поды с 10 до 1:




kubectl -n test-namespace scale deploy test-deployment --replicas=1

deployment.apps/test-deployment scaled




Поды перешли в Terminating:




kubectl -n test-namespace get pod

NAME                              READY   STATUS        RESTARTS   AGE

test-deployment-647ddf455-67gv8   1/1     Terminating   0          4m15s

test-deployment-647ddf455-6wmcq   1/1     Terminating   0          4m15s

test-deployment-647ddf455-cjvj6   1/1     Terminating   0          4m15s

test-deployment-647ddf455-dh7pc   1/1     Terminating   0          4m15s

test-deployment-647ddf455-dvh7g   1/1     Terminating   0          4m15s

test-deployment-647ddf455-gpwc6   1/1     Terminating   0          4m15s

test-deployment-647ddf455-nbgkn   1/1     Terminating   0          4m15s

test-deployment-647ddf455-tm27p   1/1     Running       0          26m

...




И ловим наши 502 ошибки:





Теперь в Dockerfile добавляем STOPSIGNAL SIGQUIT:




FROM nginx

RUN echo 'server {n

    listen 80 default_server;n

    location / {n

      proxy_pass      http://httpbin.org/delay/10;n

    }n

}' > /etc/nginx/conf.d/default.conf

STOPSIGNAL SIGQUIT

CMD ["nginx", "-g", "daemon off;"]




Билдим, пушим:




docker build -t setevoy/nginx-sigquit .

docker push setevoy/nginx-sigquit




Обновляем деплоймент:




...

    spec:

      containers:

      - name: web

        image: setevoy/nginx-sigquit

        ports:

        - containerPort: 80

...




Передеплоиваем, и проверяем ещё раз.




Запускаем тесты:





Скейлим деплоймент:




kubectl -n test-namespace scale deploy test-deployment --replicas=1

deployment.apps/test-deployment scaled




А ошибок по-прежнему нет:





Прекрасно.




Трафик, preStop и sleep




И всё-таки, если повторить тесты несколько раз, то иногда 502 ошибки всё-таки проскакивают:





Тут мы уже сталкиваемся со второй проблемой – обновление ендпоинтов выполняется параллельно с отправкой SIGTERM.




Добавим preStop хук со sleep, что бы дать время на обновление ендпоинтов – тогда после получения команды на удаление пода, kubelet выждет 5 секунд перед отправкой SIGTERM, за которые Ingress успеет обновить список ендпоинтов:




...

    spec:

      containers:

      - name: web

        image: setevoy/nginx-sigquit

        ports:

        - containerPort: 80

        lifecycle:

          preStop:

            exec:

              command: ["/bin/sleep","5"]

...




Повторяем тесты – всё хорошо.




Выкатили фикс в Production – всё работает замечательно, ошибки пропали, автотесты теперь работают без проблем.




С PHP-FPM проблемы, как оказалось, не было вообще,  т.к. исходный образ изначально был со STOPSIGNAL SIGQUIT.




Другие варианты решения




Конечно, по ходу поиска решения были переброваны самые разные варианты. См. ссылки на интересные материалы в конце, а тут опишу их кратко.




preStop и nginx -s quit




Одним из вариантов было добавить в preStop хук отправку сигнала QUIT в NGINX:




lifecycle:

  preStop:

    exec:

      command:

      - /usr/sbin/nginx

      - -s

      - quit




Но не помогло. Не очень понятно почему, потому как идея вроде бы правильная – вместо того, что бы ожидать TERM от Kubernetes/Docker – мы сами мягко стопаем NGINX, отправляя ему QUIT.




Но можно попробовать.




NGINX + PHP-FPM, supervisord и stopsignal




Наше приложение работает в двух отдельных контейнерах NGINX и PHP-FPM.




Но в процессе поиска решения пробовал использовать образ, в котором оба сервиса собраны в одном образе, например trafex/alpine-nginx-php7.




В нём пробовал добавлять параметр stopsignal и для NGINX, и для PHP-FPM со значением QUIT – но тоже не помогло, хотя идея тоже вроде правильная.




Тем не менее – как вариант на попытку.




PHP-FPM и process_control_timeout




В посте Graceful shutdown in Kubernetes is not always trivial и на Stackoveflow в вопросе Nginx / PHP FPM graceful stop (SIGQUIT): not so graceful есть упоминание о том, что мастер-процесс FPM убивается не дожидаясь своих дочерних процессов, что также может приводить к 502.




Не наш случай, но стоит обратить внимание на параметр process_control_timeout.




NGINX, HTTP и keep-alive session




В принципе, можно было бы указать приложению отправлять заголовок [Connection: close] – тогда клиент по завершении передачи данных закрывал бы соединение, и это уменьшило бы вероятность 502-х. Но они всё равно будут, если NGINX получит SIGTERM в процессе обработки запроса от клиента, а т.к. у нас “сзади” ещё PHP, который ходит в базу – то некоторые запросы могут обрабатываться по 5-10 и более секунд.




См. HTTP persistent connection.




Источник: https://rtfm.co.ua/ru/kubernetes-nginx-php-fpm-graceful-shutdown-izbavlyaemsya-ot-502-oshibok/



2023-01-03T01:50:56
DevOps