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

Метрики Qrator (Qrator Exporter)

В очередной раз пришлось настраивать сбор метрик с Qrator, прошлая моя заметка на этот счет жила в виде Issue в репозитории StupidScience/qrator-exporter (в проекте используются deprecated-методы), но там она пропала, поэтому опишу здесь, чтобы уж точно не потерялось.




Источник qratorlabs.medium.com




Сбор данных будет осуществляться через telegraf и, с помощью него же, отдаваться в виде метрик формата Prometheus.




Для начала потребуется получить API-токен для получения данных из Qrator, для этого переходим в раздел с ключами в личном кабинете и выпускаем токен.




Далее переходим в список доменов и сохраняем их идентификаторы, по ним будет обращение к методам API:







Здесь 11111 и 11222 – как раз те самые идентификаторы доменов, теперь описываем конфигурацию для телеграфа:




[[inputs.http]]
	name_prefix = "qrator_blocks_"
	method = "POST"
	urls = [
		"https://api.qrator.net/request/domain/11111",
		"https://api.qrator.net/request/domain/11222",
	]
	headers = {"X-Qrator-Auth" = "${QRATOR_API_KEY}", "Content-Type" = "application/json"}
	body = '{"method":"statistics_current_blocks"}'
	data_format = "json"
	timeout = "30s"

[[inputs.http]]
	name_prefix = "qrator_http_"
	method = "POST"
	urls = [
		"https://api.qrator.net/request/domain/11111",
		"https://api.qrator.net/request/domain/11222",
	]
	headers = {"X-Qrator-Auth" = "${QRATOR_API_KEY}", "Content-Type" = "application/json"}
	body = '{"method":"statistics_current_http"}'
	data_format = "json"
	timeout = "30s"

[[inputs.http]]
	name_prefix = "qrator_ip_"
	method = "POST"
	urls = [
		"https://api.qrator.net/request/domain/11111",
		"https://api.qrator.net/request/domain/11222",
	]
	headers = {"X-Qrator-Auth" = "${QRATOR_API_KEY}", "Content-Type" = "application/json"}
	body = '{"method":"statistics_current_ip"}'
	data_format = "json"
	timeout = "30s"

[[inputs.http]]
	name_prefix = "qrator_locations_"
	method = "POST"
	urls = [
		"https://api.qrator.net/request/domain/11111",
		"https://api.qrator.net/request/domain/11222",
	]
	headers = {"X-Qrator-Auth" = "${QRATOR_API_KEY}", "Content-Type" = "application/json"}
	body = '{"method":"statistics_current_locations"}'
	data_format = "json"
	timeout = "30s"

[[outputs.prometheus_client]]
	listen = ":9273"




В поле urls передается массив из ссылок на ресурсы (включают в себя идентификаторы доменов), в поле body – метод, а для передачи API-ключа используется переменная окружения QRATOR_API_KEY, нам нужно будет её дополнительно передать телеграфу, чтобы не хранять напрямую в конфигурации.




Осталось только запустить. Минифицированный Deployment для kustomize может выглядеть так:




---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: qrator-exporter
spec:
  template:
    spec:
      containers:
        - name: telegraf
          image: telegraf:1.21.4
          ports:
            - name: metrics
              containerPort: 9273
          env:
            - name: QRATOR_API_KEY
              value: CHANGE_ME
          securityContext:
            runAsUser: 1001
            capabilities:
              drop:
                - ALL
            readOnlyRootFilesystem: true
            runAsNonRoot: true
          volumeMounts:
            - name: config
              mountPath: "/etc/telegraf"
              readOnly: true
            - name: cache
              mountPath: "/.cache"
      volumes:
        - name: config
          secret:
            secretName: qrator-exporter
        - name: cache
          emptyDir: {}




Сам секрет qrator-exporter описывается в файле kustomization.yaml, например:




secretGenerator:
  - name: qrator-exporter
    files:
      - config/telegraf.conf




Не забываем описать сервис и Service Monitor:




---
apiVersion: v1
kind: Service
metadata:
  name: qrator-exporter
spec:
  type: ClusterIP
  ports:
    - name: metrics
      port: 9273
      targetPort: 9273
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: qrator-exporter
spec:
  endpoints:
    - interval: 30s
      path: /metrics
      port: metrics
  selector:
    matchLabels:
      app.kubernetes.io/name: qrator-exporter
      app.kubernetes.io/component: service
      app.kubernetes.io/part-of: monitoring
  namespaceSelector:
    any: true




Селектор по лейблам, которые заданы в kustomization.yaml:




---
commonLabels:
  app.kubernetes.io/name: qrator-exporter
  app.kubernetes.io/component: service
  app.kubernetes.io/part-of: monitoring




После этого мы начнем собирать метрики, однако в качестве url в метриках будет непонятный адрес ресурса Qrator, поэтому добавляем релейбл:




---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: qrator-exporter
spec:
  endpoints:
    - interval: 30s
      path: /metrics
      port: metrics
      metricRelabelings:
        - sourceLabels: ["url"]
          regex: https://api.qrator.net/request/domain/(.+)
          replacement: $1
          targetLabel: domain_id
          action: replace
        - sourceLabels: ["url"]
          regex: https://api.qrator.net/request/domain/11111
          replacement: domain.ru
          targetLabel: domain_name
          action: replace
        - sourceLabels: ["url"]
          regex: https://api.qrator.net/request/domain/11222
          replacement: super-domain.ru
          targetLabel: domain_name
          action: replace
  selector:
    matchLabels:
      app.kubernetes.io/name: qrator-exporter
      app.kubernetes.io/component: service
      app.kubernetes.io/part-of: monitoring
  namespaceSelector:
    any: true




Теперь в domain_name будет читаемый параметр, который можно использовать для селекторов в Grafana или в алертах.




Примеры алертов для Prometheus Operator:




---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: qrator-exporter
  labels:
    app: prometheus-operator
    release: "monitoring"
spec:
  groups:
    - name: QratorExporter
      rules:
        - alert: QratorHighBandwidthInput
          expr: qrator_ip_http_result_bandwidth_input > 5000000
          for: 5m
          labels:
            severity: warning
            domain: "{{ $labels.domain_name }}"
          annotations:
            summary: Большой входящий трафик на {{ $labels.domain_name }}
            description: На домене {{ $labels.domain_name }} в Qrator фиксируется повышенный входящий трафик, более 5Мбит/с
        - alert: QratorHighBandwidthOutput
          expr: qrator_ip_http_result_bandwidth_input > 5000000
          for: 5m
          labels:
            severity: warning
            domain: "{{ $labels.domain_name }}"
          annotations:
            summary: Большой исходящий трафик на {{ $labels.domain_name }}
            description: На домене {{ $labels.domain_name }} в Qrator фиксируется повышенный исходящий трафик, более 5Мбит/с
        - alert: QratorHigh5xxRate
          expr: qrator_http_http_result_errors_total >= 0.1
          for: 5m
          labels:
            severity: critical
            domain: "{{ $labels.domain_name }}"
          annotations:
            summary: В Qrator на {{ $labels.domain_name }} фиксируется рост числа ошибок
            description: В Qrator на домене {{ $labels.domain_name }} в течении 5 минут фиксируется рост числа 50x ошибок




Перед добавлением алертов стандартная рекомендация – пособирайте некоторые время метрики, чтобы определить для себя граничные значения, удобнее всего за этим наблюдать в Grafana, поэтому в качестве базового можно взять этот дашборд:




{
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": "-- Grafana --",
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "target": {
          "limit": 100,
          "matchAny": false,
          "tags": [],
          "type": "dashboard"
        },
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "gnetId": null,
  "graphTooltip": 1,
  "id": 106,
  "iteration": 1647973063127,
  "links": [],
  "liveNow": false,
  "panels": [
    {
      "datasource": null,
      "description": "Alerts:nn* QratorHighBandwidthInputn",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 0,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "bits"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 12,
        "x": 0,
        "y": 0
      },
      "id": 2,
      "options": {
        "legend": {
          "calcs": [
            "max"
          ],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "multi"
        }
      },
      "targets": [
        {
          "exemplar": true,
          "expr": "sum(qrator_ip_http_result_bandwidth_input{domain_name="$domain"})",
          "interval": "",
          "legendFormat": "input",
          "refId": "A"
        },
        {
          "exemplar": true,
          "expr": "sum(qrator_ip_http_result_bandwidth_output{domain_name="$domain"})",
          "hide": false,
          "interval": "",
          "legendFormat": "output",
          "refId": "B"
        }
      ],
      "title": "Traffic",
      "type": "timeseries"
    },
    {
      "datasource": null,
      "description": "Alerts:nn* QratorHigh5xxRate",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 0,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "decimals": 2,
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "reqps"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 12,
        "x": 12,
        "y": 0
      },
      "id": 4,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "multi"
        }
      },
      "targets": [
        {
          "exemplar": true,
          "expr": "sum({__name__=~"qrator_http_http_result_errors_.+", domain_name="$domain"})by(__name__)",
          "interval": "",
          "legendFormat": "{{ __name__ }}",
          "refId": "A"
        }
      ],
      "title": "Errors",
      "transformations": [
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_errors_(.*)",
            "renamePattern": "$1"
          }
        }
      ],
      "type": "timeseries"
    },
    {
      "datasource": null,
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 0,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "reqps"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 12,
        "x": 0,
        "y": 9
      },
      "id": 7,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "single"
        }
      },
      "targets": [
        {
          "exemplar": true,
          "expr": "sum(qrator_http_http_result_requests{domain_name="$domain"})",
          "interval": "",
          "legendFormat": "total",
          "refId": "A"
        }
      ],
      "title": "Requests",
      "transformations": [
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_0000_0(.*)",
            "renamePattern": "Less $1 ms"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_0(.*)_0(.*)",
            "renamePattern": "$1 - $2 ms"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_0(.*)_(.*)",
            "renamePattern": "$1 - $2 ms"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_1000_1500",
            "renamePattern": "1 - 1.5 s"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_1500_2000",
            "renamePattern": "1.5 - 2 s"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_2000_5000",
            "renamePattern": "2 - 5 s"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_5000_inf",
            "renamePattern": "More 5 s"
          }
        }
      ],
      "type": "timeseries"
    },
    {
      "datasource": null,
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 0,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "reqps"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 12,
        "x": 12,
        "y": 9
      },
      "id": 10,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "single"
        }
      },
      "targets": [
        {
          "exemplar": true,
          "expr": "sum({__name__=~"qrator_http_http_result_responses_.+", domain_name="$domain"})by(__name__)",
          "interval": "",
          "legendFormat": "{{ __name__ }}",
          "refId": "A"
        }
      ],
      "title": "Requests by response time",
      "transformations": [
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_0000_0(.*)",
            "renamePattern": "Less $1 ms"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_0(.*)_0(.*)",
            "renamePattern": "$1 - $2 ms"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_0(.*)_(.*)",
            "renamePattern": "$1 - $2 ms"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_1000_1500",
            "renamePattern": "1 - 1.5 s"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_1500_2000",
            "renamePattern": "1.5 - 2 s"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_2000_5000",
            "renamePattern": "2 - 5 s"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_5000_inf",
            "renamePattern": "More 5 s"
          }
        }
      ],
      "type": "timeseries"
    },
    {
      "datasource": null,
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 0,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "pps"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 12,
        "x": 0,
        "y": 18
      },
      "id": 11,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "multi"
        }
      },
      "targets": [
        {
          "exemplar": true,
          "expr": "sum(qrator_ip_http_result_packets_input{domain_name="$domain"})",
          "interval": "",
          "legendFormat": "input",
          "refId": "A"
        },
        {
          "exemplar": true,
          "expr": "sum(qrator_ip_http_result_packets_output{domain_name="$domain"})",
          "hide": false,
          "interval": "",
          "legendFormat": "output",
          "refId": "B"
        }
      ],
      "title": "Packets",
      "transformations": [
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_0000_0(.*)",
            "renamePattern": "Less $1 ms"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_0(.*)_0(.*)",
            "renamePattern": "$1 - $2 ms"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_0(.*)_(.*)",
            "renamePattern": "$1 - $2 ms"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_1000_1500",
            "renamePattern": "1 - 1.5 s"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_1500_2000",
            "renamePattern": "1.5 - 2 s"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_2000_5000",
            "renamePattern": "2 - 5 s"
          }
        },
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_http_http_result_responses_5000_inf",
            "renamePattern": "More 5 s"
          }
        }
      ],
      "type": "timeseries"
    },
    {
      "datasource": null,
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 0,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          }
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 12,
        "x": 12,
        "y": 18
      },
      "id": 5,
      "options": {
        "legend": {
          "calcs": [
            "max",
            "last"
          ],
          "displayMode": "table",
          "placement": "right"
        },
        "tooltip": {
          "mode": "single"
        }
      },
      "targets": [
        {
          "exemplar": true,
          "expr": "sum({__name__=~"qrator_locations_http_result_locations_.+", domain_name="$domain"}>0)by(__name__)",
          "interval": "",
          "legendFormat": "{{ __name__ }}",
          "refId": "A"
        }
      ],
      "title": "Black list",
      "transformations": [
        {
          "id": "renameByRegex",
          "options": {
            "regex": "qrator_locations_http_result_locations_(.*)",
            "renamePattern": "$1"
          }
        }
      ],
      "type": "timeseries"
    }
  ],
  "schemaVersion": 32,
  "style": "dark",
  "tags": [
    "WIP"
  ],
  "templating": {
    "list": [
      {
        "allValue": null,
        "current": {
          "selected": false,
          "text": "qlean.ru",
          "value": "qlean.ru"
        },
        "datasource": null,
        "definition": "label_values(qrator_http_http_id, domain_name)",
        "description": null,
        "error": null,
        "hide": 0,
        "includeAll": false,
        "label": "Domain",
        "multi": false,
        "name": "domain",
        "options": [],
        "query": {
          "query": "label_values(qrator_http_http_id, domain_name)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 1,
        "regex": "",
        "skipUrlSync": false,
        "sort": 1,
        "type": "query"
      }
    ]
  },
  "time": {
    "from": "now-12h",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "Qrator",
  "uid": "gM2arMHnk",
  "version": 23
}




TODO: Перенести дашборд в https://grafana.com/grafana/dashboards/.




Источник: https://levaminov.ru/-GTUP-QWYFV



2023-12-03T22:25:45
DevOps

Памятка по созданию Gitlab Pipelines

Шаблоны




В данном разделе напишем несколько готовых шаблонов, которые можно взять за основы при написании своего сценария.




1. Минимальный сценарий. Для небольших заданий, состоящих из пары заданий:




stages:
  - build

TASK_NAME:
  stage: build
  script:
    - ./build_script.sh




где:




  • stages — описание стадий нашего пайплайна. В данном примере, всего одна.



  • TASK_NAME — имя нашей задачи.

    • stage — стадия, к которой относится наше задание.



    • script — набор скриптов для выполнения.




2. Стандартный цикл при сборке. Обычно, процесс CI/CD включает следующие шаги:




  • Сборка пакета.



  • Тестирование.



  • Доставка.



  • Развертывание.




Для написания такого сценария за основу можно взять шаблон:




stages:
  - build
  - test
  - delivery
  - deploy

build-job:
  stage: build
  script:
    - echo "Start build job"
    - build-script.sh

test-job:
  stage: test
  script:
    - echo "Start test job"
    - test-script.sh

delivery-job:
  stage: delivery
  script:
    - echo "Start delivery job"
    - delivery-script.sh

deploy-job:
  stage: deploy
  script:
    - echo "Start deploy job"
    - deploy-script.sh




Задания




В данном разделе мы рассмотрим опции, которые могут применяться при описании задания. Общий синтаксис:




TASK_NAME>:
   OPTION1>: ...
   OPTION2>: ...




Мы перечислим лишь часто встречаемые опции. Полный список можно найти в официальной документации.




Stage





https://docs.gitlab.com/ee/ci/yaml/#stages




Опция указывает, к какой стадии относится задание. Например:




stages:
  - build
  - test

TASK_NAME:
  ...
  stage: build

TASK_NAME:
  ...
  stage: test




Сами же стадии описываются в общей директиве stages.




Также есть две стадии, которые не требуют предварительного определения в stages:




  • .pre — запускает до выполнения основных заданий конвейера.



  • .post — выполняется в конце, после выполнения основных заданий нашего пайплайна.




Например:




stages:
  - build
  - test

getVersion:
  stage: .pre
  script:
    - VERSION=$(cat VERSION_FILE")
    - echo "VERSION=${VERSION}" > variables.env
  artifacts:
    reports:
      dotenv: variables.env




* в этом примере мы до того, как начнем проходить по всем заданиям сборки, определим переменную с версией, прочитав ее из файла, и передадим артефактом в качестве системной переменной VERSION.




Image







Задаем имя образа, если наше задание выполняется в контейнере docker:




TASK_NAME:
  ...
  image: debian:11




Before_script







Данная опция определяет список команд, которые должны выполняться перед опцией script и после получения артефактов.




TASK_NAME:
  ...
  before_script:
    - echo "Run before_script"




Script







Основная часть, где выполняются задания сценария, описана в опции script. Рассмотрим ее подробнее.




1. Описание массива команд. Мы просто перечисляем списком те команды, которые необходимо последовательно выполнить нашему сценарию в конкретной задаче:




TASK_NAME:
  ...
  script:
    - command1
    - command2




2. Длинные команды, разбитые на несколько строк. Нам может потребоваться выполнить команды в виде скрипта (с операторами сравнения, например). В этом случае будет удобна многострочная запись. Ее можно указать разными способами.




Индикатор | :




TASK_NAME:
  ...
  script:
    - |
      command_line1
      command_line2




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




Индикатор > :




TASK_NAME:
  ...
  script:
    - >
      command_line1
      command_line1_continue

      command_line2
      command_line2_continue




* данный индикатор интерпретирует новую команду после пустой строки.




After_script







Набор команд, которые запускаются после scripts, даже, при неудачном завершении последнего.




TASK_NAME:
  ...
  script:
    ...
  after_script:
    - command1
    - command2




Artifacts







Артефакты представляют из себя промежуточные сборки или файлы, которые могут передаваться от одного этапа — другому.




1. Например, так можно указать, какие файлы или каталоги будут артефактами:




TASK_NAME:
  ...
  artifacts:
    paths:
      - ${PKG_NAME}.deb
      - ${PKG_NAME}.rpm
      - *.txt
      - configs/




* в артефакты попадут все файлы, название которых заканчивается на txt, файлы ${PKG_NAME}.deb и ${PKG_NAME}.rpm, а также каталог configs. Где ${PKG_NAME} — переменная (подробнее о переменных ниже).




В других заданиях, которые будут выполняться после, можно использовать артефакты, обращаясь к ним по именам, например:




TASK_NAME_2:
  ...
  script:
    - cat *.txt
    - yum -y localinstall ${PKG_NAME}.rpm
    - apt -y install ./${PKG_NAME}.deb




2. Также мы можем передать системные переменные, которые были нами переданы в файл:




TASK_NAME:
  ...
  script:
    - echo -e "VERSION=1.1.1" > variables.env
  ...
  artifacts:
    reports:
      dotenv: variables.env




* в этом примере мы передадим системную переменную VERSION со значением 1.1.1 через файл variables.env.




3. При необходимости, мы можем исключить из списка определенные файлы по названию или маске:




TASK_NAME:
  ...
  artifacts:
    paths:
      ...
    exclude:
      - ./.git/**/*




* в этом примере мы исключаем каталог .git, в котором, как правило, хранятся метаданные репозитория.
** обратите внимание, что в отличие от pathsexclude не берет файлы и каталоги рекурсивно и нужно явно указывать объекты.




Extends







Позволяет вынести часть сценария в отдельный блок и объединить его с заданием. Чтобы это лучше понять, рассмотрим конкретный пример:




.my_extend:
  stage: build
  variables:
    USERNAME: my_user
  script:
    - extend script

TASK_NAME:
  extends: .my_extend
  variables:
    VERSION: 123
    PASSWORD: my_pwd
  script:
    - task script




И так, в нашем задании TASK_NAME мы используем extends. В результате, задание примет такой вид:




TASK_NAME:
  stage: build
  variables:
    VERSION: 123
    PASSWORD: my_pwd
    USERNAME: my_user
  script:
    - task script




Что произошло:







Environment







Позволяет указать системную переменную, которая будет создана для выполнения задания.




TASK_NAME:
  ...
  environment:
    RSYNC_PASSWORD: rpass
    EDITOR: vi




Release







Публикует релиз на портале Gitlab для нашего проекта.




TASK_NAME:
  ...
  release:
    name: 'Release $CI_COMMIT_TAG'
    tag_name: '$CI_COMMIT_TAG'
    description: 'Created using the release-cli'
    assets:
      links:
        - name: "program-${VERSION}"
          url: "https://gitlab.com/master/project/-/jobs/${CI_JOB_ID}/artifacts/raw/program-${VERSION}.tar.gz"
  rules:
    - if: $CI_COMMIT_TAG




Директивы правил и ограничений




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




Rules







Правила, при соблюдении которых наше задание может быть запущено. Примеры работы с директивой rules приведены ниже.




When







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




Needs







Тоже набор правил, требующий определенных условий для запуска задачи. Все директивы условий и правил описаны ниже.




Правила и условия




Мы можем выполнять или пропускать задания, в зависимости от выполнения определенных условий. Для этого существует несколько полезных директив, которые мы рассмотрим в данном разделе.




Rules




Регламентирует различные правила, определенные с помощью:







1. Оператор if. Позволяем проверить условие, например, если переменная равна определенному значению:




TASK_NAME:
  ...
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'




* в этом примере commit должен быть выполнен в branch по умолчанию.




2. Изменения затронули определенные файлы. Проверка выполняется с помощью опции changes.




В данном примере, файл script.sql:




TASK_NAME:
  ...
  rules:
    - changes:
        - script.sql




3. Несколько условий. Условий для начала выполнения задания может быть несколько. Рассмотрим несколько примеров.




а) При условии, что commit выполнен в branch по умолчанию И изменения затронули файл script.sql:




TASK_NAME:
  ...
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      changes:
        - script.sql




б) При условии, что commit выполнен в branch по умолчанию ИЛИ изменения затронули файл script.sql




TASK_NAME:
  ...
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
    - changes:
        - script.sql




4. Проверка на существование файла. Определяется с помощью exists:




TASK_NAME:
  ...
  rules:
    - exists:
        - script.sql




* задание будет выполняться только в случае присутствия файла script.sql.




5. Разрешить сбой задания. Задается с помощью allow_failure:




TASK_NAME:
  ...
  rules:
    - allow_failure: true




* в этом примере наш конвейер продолжит работу, если задание TASK_NAME будет выполнено с ошибкой.




6. Определение переменной в зависимости от условия. Для этого используются вместе if и variables:




TASK_NAME:
  ...
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      variables:
        DEPLOY_VARIABLE: "production"
    - if: '$CI_COMMIT_BRANCH =~ demo'
      variables:
        DEPLOY_VARIABLE: "demo"




When




Определяет условия запуска задания. Возможные значения:







Разберем примеры.




1. Manual:




TASK_NAME:
  ...
  when: manual




* задание не начнет выполняться, пока мы не нажмем кнопку запуска в GitLab CI/CD.




2. Always:




TASK_NAME:
  ...
  when: always




* задание будет выполняться всегда. Удобно, например, если нужно сформировать отчетный файл, независимо от результатов сборки.




3. On_failure:




TASK_NAME:
  ...
  on_failure: always




* задание будет выполняться при наличии ошибки на ранних этапах. Можно использовать для отправки уведомления.




4. Delayed:




TASK_NAME:
  ...
  when: delayed
  start_in: 30 minutes




* запуск задания будет отложен на 30 минут.




5. Never:




TASK_NAME:
  ...
  when: never




* задание не будет выполняться.




6. Использование вместе с if:




TASK_NAME:
  ...
  rules:
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: manual




* в данном примере задание будет выполняться, если коммит прошел в основной ветке и если администратор подтвердил запуск.




Needs




Позволяет задавать условия выполнения заданий только при наличии определенного артефакта или выполненного задания. С помощью правил данного типа можно контролировать порядок выполнения задач.




Рассмотрим примеры.




1. Artifacts. Принимает значение true (по умолчанию) и false. Для запуска задания нужно, чтобы на предыдущих этапах были загружены артефакты. Запись:




TASK_NAME:
  ...
  needs:
    - artifacts: false




… позволяет начать выполнение задания без этого.




2. Job. Позволяет начать выполнение задачи только после завершения другого задания:




TASK_NAME:
  ...
  needs:
    - job: createBuild




* в нашем примере задача начнет выполняться не раньше завершения задачи с названием createBuild.




Переменные




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




Пользовательские переменные




Задаются с помощью директивы variables.




Можно определить глобально для всех заданий:




variables:
  PKG_VER: "1.1.1"




Или для конкретного задания:




TASK_NAME:
  ...
  variables:
    PKG_VER: "1.1.1"




Теперь можно использовать нашу переменную в сценарии. Для этого ставим перед ее названием знак доллара, а также стоит заключить его в фигурные скобки, например:




  script:
    - echo ${PKG_VER}




Переменные Gitlab




Данный тип переменных поможет нам управлять процессом сборки. Перечислим данные переменные с описанием их свойств:




ПЕРЕМЕННАЯОПИСАНИЕПРИМЕР
LOG_LEVELОпределяет уровень журнала. Варианты: debug, info, warn, error, fatal, и panic. Имеет более низкий приоритет, по сравнению с аргументами командной строки —debug, —log-level.LOG_LEVEL: warning
CI_DEBUG_TRACEВключение или отключение ведения журнала отладки. Принимает значения true или false.CI_DEBUG_TRACE: true
CONCURRENTОграничивает количество заданий, которые могут выполняться одновременно.CONCURRENT: 5
GIT_STRATEGYСпособ загрузки файлов из репозитория. Возможны варианты: clone, fetch, и none (не загружать).GIT_STRATEGY: none




Дополнительные параметры




В данном разделе мы рассмотрим различные опции, которые не вошли в другие разделы.




1. Workflow. Позволяем задать общие правила запуска для всего конвейера. Рассмотрим пример с официального сайта:




workflow:
  rules:
    - if: $CI_COMMIT_TITLE =~ /-draft$/
      when: never
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH




В данном примере конвейер:







2. Значения по умолчанию. Задаются в директиве default. Опции с данными значениями будут использоваться во всех заданиях или могут быть переопределены на уровне конкретного задания.




Пример:




default:
  image: centos:7
  tags:
    - ci-test




* мы определили опцию с именем образа (например, образа docker) и тег (теги могут быть необходимы для запуска некоторых runner).




3. Импорт конфигурации из другого файла yml. Может быть полезным, например, для написания общей части сценария, которую мы захотим применять ко всем pipeline или для разделения громоздкого сценария на составные части. Выполняется с помощью опции include. Имеет различные варианты подгрузки файлов. Рассмотрим их подробнее.




а) подключение локального файла (local):




include:
  - local: .gitlab/ci/build.yml




б) коллекции шаблонов (template):




include:
  - template: Custom/.gitlab-ci.yml




* в данном примере мы подключим к нашему сценарию содержимое файла Custom/.gitlab-ci.yml.




в) внешний файл доступный по HTTP (remote):




include:
  - remote: 'https://gitlab.dmosk.ru/main/build.yml'




г) другой проект:




include:
  - project: 'public-project'
    ref: main
    file: 'build.yml'




4. !reference tags. Позволяет описать сценарий и повторно его использовать для разных стадий и задач нашего CI/CD.




Например:




.apt-cache:
  script:
    - apt update
  after_script:
    - apt clean all

install:
  stage: install
  script:
    - !reference [.apt-cache, script]
    - apt install nginx

update:
  stage: update
  script:
    - !reference [.apt-cache, script]
    - apt upgrade
    - !reference [.apt-cache, after_script]




* давайте разберемся, что происходит в нашем примере.







Источник: https://kuzevanov.ru/devops/памятка-по-созданию-gitlab-pipelines.html



Проксирование внешнего веб-сайта с помощью Nginx Ingress

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




  • Внешний веб-сервер разработан не таким образом, чтобы вы могли (легко) запустить его в контейнере вашего кластера.



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



  • Вы хотите воспользоваться преимуществами автоматической настройки HTTPS вашего контроллера Nginx Ingress.




Оказывается, на самом деле это довольно просто настроить.




В этом примере мы предполагаем, что внешний веб-сайт размещен на IP-адресе 10.20.30.40 и прослушивается через порт 8080. Обратите внимание, что для этого примера мы предполагаем, что порт 8080 обслуживает незашифрованный простой HTTP.




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




Прежде всего, вам необходимо создать сервис с конечной точкой:




service.yaml




apiVersion: v1
kind: Service
metadata:
  name: <my-external-service>
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 8080
  clusterIP: None
  type: ClusterIP
---
apiVersion: v1
kind: Endpoints
metadata:
  name: <my-external-service>
subsets:
- addresses:
  - ip: 10.20.30.40
  ports:
  - name: http
    port: 8080
    protocol: TCP




По сути, мы сообщаем Kubernetes, что определяем службу, которая связана с внешним IP-адресом, прослушивающим определенный порт. Мы используем IP-адрес, чтобы избежать DNS-запросов, задействованных в этой настройке.




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




$ kubectl apply -f service.yaml




Для завершения настройки мы добавляем сервис в определение ingress точно так же, как мы бы поступили с обычным сервисом:




ingress.yaml




apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    kubernetes.io/ingress.class: nginx
    certmanager.k8s.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
  - hosts:
    - <my-domain-name.com>
    secretName: letsencrypt-prod
  rules:
  - host: <my-domain-name.com>
    http:
      paths:
      - backend:
          serviceName: <my-external-service>
          servicePort: 80




Примените это также, и все готово.




$ kubectl apply -f ingress.yaml




Если вы сейчас перейдете на https://my-domain-name.com, должно появиться правильное содержимое.




Источник: https://www.yellowduck.be/posts/k8s-proxy-an-external-site



2023-06-26T23:11:25
DevOps

Настройка доступа Jenkins

Создание пользователей




Сначала необходимо убедиться, что в качестве источника пользователей выбрана внутренняя БД дженкинса.










Далее идём в раздел Управление пользователями.







Там будет кнопка “Создать пользователя”







Настройка прав доступа




Должен быть установлен плагин Matrix Authorization Strategy. Он устанавливается в числе рекомендованных плагинов если вы выбрали этот путь в мастере установки дженкинса.




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







Важно: при выборе этого способа авторизации не забудьте сразу выдать пользователю admin полные права. Если всё же у вас пропал доступ к настройке системы, то его можно вернуть отредактировав файл /var/jenkins_home/config.xml. Необходимо в строчке <useSecurity>true</useSecurity> написать false и перезапустить дженкинс.



2023-06-20T01:02:03
DevOps

Nexus установка и настройка

Nexus является популярным менеджером репозиториев (repository manager). Он используется для хранения артефактов или прокси, т.е. всё что выкачивается через него, сохраняется в нём.




yum install java-1.8.0-openjdk java-1.8.0-openjdk-devel createrepo



wget http://download.sonatype.com/nexus/3/latest-unix.tar.gz



tar xvf latest-unix.tar.gz -C /opt/



ln -s /opt/nexus-*/ /opt/nexus

useradd -m -r -s /bin/false nexus



chown -R nexus:nexus /opt/nexus-* /opt/sonatype-work /opt/nexus




vim /opt/nexus/bin/nexus.rc

run_as_user="nexus"




vim /etc/systemd/system/nexus.service



[Unit]

Description=nexus service

After=network.target



[Service]

Type=forking

ExecStart=/opt/nexus/bin/nexus start

ExecStop=/opt/nexus/bin/nexus stop

User=nexus

Restart=on-abort



[Install]

WantedBy=multi-user.target







Уменьшить количество потребляемой оперативной памяти:





vim /var/nexus/nexus-3.19.1-01/bin/nexus.vmoptions
меняем
-Xms2703m
-Xmx2703m
на
-Xms512m
-Xmx512m




systemctl daemon-reload && systemctl enable nexus



systemctl start nexus && systemctl status nexus




WEB-интерфейс




http://<Nexus-server-ip-address>:8081




для авторизации в качестве логина используйте
admin
пароль можно посмотреть тут:




cat /opt/sonatype-work/nexus3/admin.password




также по умолчанию пароль может быть следующим:
Login: admin




Password: admin123




Настройка ротации логов Nexus




cat /etc/logrotate.d/nexus




/opt/sonatype-work/nexus3/log/*.log {
daily
dateext
copytruncate
missingok
rotate 3
compress
delaycompress
notifempty
}




отметим что в директории в которой установлен nexux должно быть не меньше 5Gb.




стартуем




systemctl start nexus




добавляем в автозапуск




systemctl enable nexus




логи хранятся тут:




/opt/sonatype-work/nexus3/log/




для удобства создадим симлинк
mkdir /var/log/nexus




ln -s /opt/sonatype-work/nexus3/log/ /var/log/nexus/




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




Описание работы с данной утилитой




Общий репозиторий:




не забываем включить анонимный доступ чтоб реп работал:
Anonymous — > Allow anonymous users to access the server





Далее переходим к списку репозиториев:




Создаём новый proxy репозиторий (create reposytory — > yum(proxy))




Name: test-repo-epel




ProxyRemote storage:  http://mirror.centos.org/centos-7/7/os/x86_64/




после чего нажимаем create reposytory




Создаём ещё один proxy репозиторий (create reposytory — > yum(proxy))
Name: yum-centos-7-repo_updates
Remote storage: http://mirror.centos.org/centos-7/7/updates/x86_64/




Создаём yum group




Name: yum-repo-group




добавляем в него 2 созданных нами прокси  и сохраняем.




всё, репозиторий создан.




Теперь чтобы использовать его необходимо пройти в раздел:
reposytory -> Repositories -> yum-repo-group
получаем следующую ссылку:
http://192.168.1.171:8081/repository/yum-repo-group/




и на целевой тачке создаем репозиторий:




cat /etc/yum.repos.d/Centos-7-Nexus.repo




[Centos-7-Nexus]

baseurl = http://192.168.1.177:8081/repository/yum-repo-group/

gpgcheck = 1

enabled=1

gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7

name = Centos-Nexus




сохраняем и проверяем.




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




Общий репозиторий docker:




создаём docker(hosted):
repositories -> create repository -> docker(hosted)




Name: docker-private
type hosted




http: 8083




можно ещё добавить для https




Нажимаем save




создаём docker(proxy):




repositories -> create repository -> docker(proxy)
Name: docker-hub
type proxy
remote storage: https://registry-1.docker.io
docker index: выбирем Use Docker Hub




Нажимаем save




создаём docker(group):




repositories -> create repository -> docker(group)
Name: docker-group
http:8082
и добавляем в него docker-private  docker-hub




Нажимаем save.




На целевой тачке добавляем:




cat /etc/docker/daemon.json
{
"insecure-registries": ["192.168.1.177:8081","192.168.1.177:8082","192.168.1.177:8083"],
"experimental": true
}




Далее необходимо залогиниться в nexus репозитории:
docker login http://192.168.1.177:8082/repository/docker-group




!!! ВАЖНО. Если вы используете прокси, то необходимо убрать его, перезапустить демон, и рестартануть docker(reload не хватит):




mv /etc/systemd/system/docker.service.d/http-proxy.conf /home/

systemctl daemon-reload

systemctl restart docker




проверить, что прокся не задействована вы можете с помощью команды:




docker info




в её выводе не должно быть прокси.




При логине в  192.168.1.177:8082/repository/docker-group в качестве логина и пароля надо указывать или тех пользователей которые вы создали в nexus или главного пользователя с которым вы авторизовались в nexus  admin admin123




чтобы выкачать образ необходимо указывать адрес источника, т.е.:
docker pull 192.168.1.177:8082/httpd




docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
192.168.1.177:8082/nginx latest e445ab08b2be 37 hours ago 126MB
192.168.1.177:8082/httpd latest ee39f68eb241 12 days ago 154MB




переименуем наш образ и загрузим его в nexus:
docker tag 192.168.1.177:8082/nginx 192.168.1.177:8082/nginx33:3




docker push 192.168.1.177:8082/nginx33:3




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




чтобы использовать nexus как прокси для скачивания обновлений или докер образов надо пройти в:
System -> HTTP -> HTTP(s) Proxy 
указываем ip и port нашего прокси, также можем задать список адресов которые исключаются из прокси(т.е. обращаясь к ним запрос не пойдёт через проксю)
Hosts to exclude from HTTP/HTTPS proxy
и нажимаем saveне забываем логиниться
docker login 192.168.1.177:8082




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




чтобы использовать nexus как прокси для установки через pip
надо создать репозиторий:
Create repository — pypi (proxy) задаём имя (произвольное) в качестве remote storage указываем
https://pypi.org
сохраняем, далее создаём pypi(group) задаём имя (произвольное) добавляем в группу ранее созданый прокси репозиторий и сохраняем
получаем ссылку группы:
http://192.168.1.177:8082/repository/pypi-repo/




далее на целевой тачке создаём файл pip.conf




cat /etc/pip.conf

[global]

index = http://192.168.1.177:8082/repository/pypi-repo/pypi

index-url = http://192.168.1.177:8082/repository/pypi-repo/simple




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




pip install pypi-install --trusted-host 192.168.1.177




Источник: https://sidmid.ru/nexus-install-and-settings/



2023-06-02T15:55:17
DevOps

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-автоскейлинг-приложений-при-помощ/