Terraform: remote state з AWS S3 та state locking з DynamoDB
0 (0)

29 Серпня 2023

Готуємось переводити управління інфрастуктурою з AWS CDK на Terraform.

Про планування того, як воно все може виглядати писав у Terraform: початок роботи та планування нового проекту – Dev/Prod та bootsrap, але тоді оминув одну досить важливу опцію – створення lock для state-файлів.

Блокування стейт-файлів використовується для того, щоб уникнути ситуацій, коли запускається кілька інстансів Terraform одночасно – інженерами або автоматично в CI/CD, і вони одночасно будуть намагатись внести зміни в один стейт-файл: при використанні lock, Terraform заблокує запуск іншого інстансу допоки перший інстанс не завершить свою роботу і не звільнить блокування.

У нашому випадку інфрастуктура вся в AWS, тому в ролі бекенду для зберігання стейтів буде використовуватись AWS S3, а для створення lock-файлів – таблиця в DynamoDB.

Документація – State Locking.

Отже, що ми зробимо:

  • таблиця DynamoDB та S3 бакет будуть менеджитись самим Терраформом
  • Terraform буде авторизуватись в AWS з AssumeRole
  • опишемо створення S3 bucket та таблиці DynamoDB
  • створимо ресурси використовуючи локальний стейт
  • імпортуємо локальний стейт в створений бакет
  • протестуємо, як працює State Lock

IAM Role

Terraform буде працювати через окрему IAM Role, див. Use AssumeRole to provision AWS resources across accounts.

Переходимо в IAM > Create Role, вибираємо Custom Trust Policy, і описуємо її:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Statement1",
      "Effect": "Allow",
      "Principal": {
          "AWS": "arn:aws:iam::123456789012:root"
          },
      "Action": "sts:AssumeRole"
    }
  ]
}

Замість 123456789012 вказуємо ID аккаунту, а в root позначаємо, що будь-який аутентифікований IAM User цього аккаунта зможете виконати sts:AssumeRole цієї ролі.

Поки задаємо AdministratorAccess, пізніше можна буде налаштувати права більш детально:

Зберігаємо роль:

Перевіримо, що вона працює.

В свій ~/.aws/config додаємо новий профайл:

[profile tf-admin]
role_arn = arn:aws:iam::492***148:role/tf-admin
source_profile = default

І виконуємо sts get-caller-identity з цим профайлом:

[simterm]

$ aws --profile tf-admin sts get-caller-identity
{
    "UserId": "ARO***ZEF:botocore-session-1693297579",
    "Account": "492***148",
    "Arn": "arn:aws:sts::492***148:assumed-role/tf-admin/botocore-session-1693297579"
}

[/simterm]

Окей, тепер можемо переходити до самого Terraform.

Налаштування Terraform-проекту

Додаємо версії модулів, котрі будемо використовувати.

Останню версію провайдеру AWS можна взяти тут>>>, а версію самого Terraform – тут>>>.

Створюємо файл versions.tf:

terraform {

  required_version = ">= 1.5"

  required_providers {
    aws = { 
      source  = "hashicorp/aws"
      version = ">= 5.14.0"
    }
  }
}

Додаємо файл providers.tf, де описуємо параметри підключення до AWS:

provider "aws" {
  region    = "us-east-1"
  assume_role {
    role_arn = "arn:aws:iam::492***148:role/tf-admin"
  }
}

Створюємо файл main.tf, поки що пустий, і перевіряємо, що Terraform може виконати AssumeRole.

Виконуємо ініціалізцію:

[simterm]

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching ">= 5.14.0"...
- Installing hashicorp/aws v5.14.0...
- Installed hashicorp/aws v5.14.0 (signed by HashiCorp)
...

[/simterm]

То робимо terraform plan:

[simterm]

$ terraform plan

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

[/simterm]

Все добре – Terraform підключився до нашого AWS-аккаунту.

Створення AWS S3 для бекенду

Для корзини, де будуть зберігатись state-файли, потрібно мати:

  1. encryption: для AWS S3 включено по дефолту, але можна налаштувати з власним ключем з AWS KMS
  2. access control: закрити публічний доступ до об’єктів в корзині
  3. versioning: налаштувати версіонування, щоб мати історію змін в стейт-файлах

Створюємо файл backend.tf, і описуємо створення KMS ключа та корзини:

resource "aws_kms_key" "tf_lock_testing_state_kms_key" {
  description             = "This key is used to encrypt bucket objects"
  deletion_window_in_days = 10
}

# create state-files S3 buket 
resource "aws_s3_bucket" "tf_lock_testing_state_bucket" {
  bucket = "tf-lock-testing-state-bucket"

  lifecycle {
    prevent_destroy = true
  }
}

# enable S3 bucket versioning
resource "aws_s3_bucket_versioning" "tf_lock_testing_state_versioning" {
  bucket = aws_s3_bucket.tf_lock_testing_state_bucket.id

  versioning_configuration {
    status = "Enabled"
  }
}

# enable S3 bucket encryption 
resource "aws_s3_bucket_server_side_encryption_configuration" "tf_lock_testing_state_encryption" {
  bucket = aws_s3_bucket.tf_lock_testing_state_bucket.id

  rule {
    apply_server_side_encryption_by_default {
      kms_master_key_id = aws_kms_key.tf_lock_testing_state_kms_key.arn
      sse_algorithm = "aws:kms"
    }
    bucket_key_enabled = true
  }
}

# block S3 bucket public access
resource "aws_s3_bucket_public_access_block" "tf_lock_testing_state_acl" {
  bucket                  = aws_s3_bucket.tf_lock_testing_state_bucket.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Далі, там же додаємо створення DynamoDB таблиці для state lock:

...
# create DynamoDB table
resource "aws_dynamodb_table" "tf_lock_testing_state_ddb_table" {
  name         = "tf-lock-testing-state-ddb-table"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Перевіряємо, чи все правильно описали:

[simterm]

$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_dynamodb_table.tf_lock_testing_state_ddb_table will be created
  + resource "aws_dynamodb_table" "tf_lock_testing_state_ddb_table" {
      + arn              = (known after apply)
      + billing_mode     = "PAY_PER_REQUEST"
      + hash_key         = "LockID"
...

Plan: 5 to add, 0 to change, 0 to destroy.

[/simterm]

І виконуємо terraform apply, щоб створити ресурси:

[simterm]

$ terraform apply
...
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes
...
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

[/simterm]

Перевіряємо корзину:

Та таблицю DynamoDB:

Налаштування Terraform Backend та State Lock

Тепер можемо додати бекенд з параметром dynamodb_table для створення lock.

До файлу backend.tf додаємо блок terraform.backend.s3:

terraform {
  backend "s3" {
    bucket         = "tf-lock-testing-state-bucket"
    key            = "tf-lock-testing-state-bucket.tfstate"
    region         = "us-east-1"
    dynamodb_table = "tf-lock-testing-state-ddb-table"
    encrypt        = true
  }  
}
...

Виконуємо terraform init ще раз, та імпортуємо локальний state в корзину:

[simterm]

$ terraform init

Initializing the backend...
Acquiring state lock. This may take a few moments...
Do you want to copy existing state to the new backend?
  ...

  Enter a value: yes

Releasing state lock. This may take a few moments...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v5.14.0

Terraform has been successfully initialized!

[/simterm]

Перевіряємо DynamoDB тепер – маємо ключ:

І стейт в S3:

Якщо переглянути таблицю DynamoDB під час виконання plan чи apply – можна побачити сам lock з полями Operation та хто саме виконує операцію:

Тестування State Lock

Додаємо файл main.tf с ресурсом EC2:

resource "aws_instance" "ec2_lock_test" {
    ami = "ami-0d2fcfe4f5c4c5b56"
    instance_type = "t2.micro"
    tags = {
      Name = "EC2 Instance with remote state"
    }
}

Копіюємо всі файли проекту в новий каталог:

[simterm]

$ mkdir test-lock
$ cp -r * test-lock/
cp: cannot copy a directory, 'test-lock', into itself, 'test-lock/test-lock'

[simterm]

В поточному каталозі запускаємо terraform apply, але не відповідаємо yes, щоб створений в DynamoDB lock залишався:

[simterm]

$ terraform apply 
Acquiring state lock. This may take a few moments...
...

[/simterm]

Переходимо в другий каталог, і там запускаємо init та apply ще раз:

[simterm]

$ cd test-lock/
$ terraform init && terraform apply
...
Acquiring state lock. This may take a few moments...
╷
│ Error: Error acquiring the state lock
│ 
│ Error message: ConditionalCheckFailedException: The conditional request failed
│ Lock Info:
│   ID:        98dd894b-065f-8f63-f695-d4dcea702807
│   Path:      tf-lock-testing-state-bucket/tf-lock-testing-state-bucket.tfstate
│   Operation: OperationTypeApply
...

[/simterm]

Та маємо помилку створення блокування, бо вже є процесс, який користується нашим state-файлом.

Terraform State Lock trics

force-unlock

Іноді буває, що Terraform не звільняє lock, наприклад, якщо при виконанні операції відвалився інтернет.

Тоді можемо звільти стейт за допомогою force-unlock, якому передаємо Lock ID:

[simterm]

$ terraform force-unlock 98dd894b-065f-8f63-f695-d4dcea702807
Do you really want to force-unlock?
  Terraform will remove the lock on the remote state.
...
  Enter a value: yes

Terraform state has been successfully unlocked!

[/simterm]

lock-timeout

Іноді треба, щоб Terraform не зупиняв роботу, як тільки побачить, що lock-запис вже є. Наприклад, в CI-пайплайні можуть бути одночасно запущені дві джоби, і тоді друга запиниться з полмилкою.

В такому випадку можемо додати lock-timeout – тоді Terraform зачекає заданий період часу, і спробує виконати lock ще раз:

[simterm]

$ terraform apply -lock-timeout=180s

[/simterm]

Готово.

Loading

VictoriaMetrics: VMAuth – проксі, аутентифиікація та авторизація
0 (0)

23 Серпня 2023

Продовжуємо розвивати наш стек моніторингу. Див. VictoriaMetrics: створення Kubernetes monitoring stack з власним Helm-чартом.

Що хочеться: зробити доступ девелоперам, щоб вони могли в Alertmanager самі виставляти Silence для алертів аби не спамити в Slack, див. Prometheus: Alertmanager Web UI и Silence алертов.

Для того, щоб забезпечити безпечний доступ до нього можна використати рішення від VictoriaMetrics – компонент VMAuth, який дозволяє створити єдиний ендпоінт, через який будуть ходити всі юзери і налаштувати відповідні бекенди для інших компонентів кластеру.

Кратко – що можна з VMAuth:

  • створити єдину точку входу для сервісів з Basic або Bearer user аутентифікацією та авторизацією
  • в залежності від юзера та роута/URI направляти його до відповідного сервіса (фактично, ви можете створити один Ingress і всі запити обслуговувати через нього замість того, щоб створювати Ingress та аутентифікацію для кожного сервіса окремо)
  • мати простий round-robin load balancer
  • налаштувати IP фільтри з Allow та Deny листами
  • керувати додаванням власних хедерів до запитів

Деплоїти будемо у AWS EKS з Helm-чарту victoria-metrics-auth, але можна робити через yaml-маніфести, див. документацію та інші приклади на Authorization and exposing components та VMAuth.

Встановлення чарту VMAuth

Так як ми маємо umbrella-chart, то додаємо в Chart.yaml в блок dependecy новий сабчарт:

...
- name: victoria-metrics-auth
  version: ~0.3.3
  repository: https://victoriametrics.github.io/helm-charts/ 
...

Дефолтні вальюси – values.yaml.

У власних values.yaml описуємо конфіг VMAuth – створення Ingress, ім’я користувача, пароль, та куди перенаправляти його запити – тут це буде Kubernetes Service для Alertmanager:

...
victoria-metrics-auth:
  ingress:
    enabled: true
    annotations:
      kubernetes.io/ingress.class: alb
      alb.ingress.kubernetes.io/target-type: ip
      alb.ingress.kubernetes.io/scheme: internet-facing
      alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:49***148:certificate/66e3050e-7f27-4f0c-8ad4-0733a6d8071a
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
      alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=600    
    hosts:
      - name: vmauth.dev.example.co
        path: /
        port: http
  config:
    users:
      - username: "vmadmin"
        password: "p@ssw0rd"
        url_prefix: "http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093"
...

Вновлюмо Helm dependency:

[simterm]

$ helm dependency update

[/simterm]

І деплоїмо чарт:

[simterm]

$ helm -n dev-monitoring-ns upgrade --install atlas-victoriametrics . -f values/dev/atlas-monitoring-dev-values.yaml

[/simterm]

Перевіряємо чи додався Ingress і AWS ALB до нього:

[simterm]

$ kk -n dev-monitoring-ns get ingress
NAME                                          CLASS    HOSTS                   ADDRESS                   PORTS   AGE
atlas-victoriametrics-victoria-metrics-auth   <none>   vmauth.dev.example.co   k8s-***elb.amazonaws.com  80      3m12s

[/simterm]

Чекаємо поки оновляться DNS, і відкриваємо https://vmauth.dev.example.co:

Логінимось, і попадаємо прямо в Алертменеджер:

Конфіг в Kubernetes Secret

Замість того, щоб тримати конфіг в values чарту можно створити Kubernetes Secret. Це додатково дасть можливість передавати пароль, якщо він у вас один, через helm install --set:

apiVersion: v1
kind: Secret
metadata:
  name: vmauth-config-secret
stringData:
  auth.yml: |-
    users:
      - username: vmadmin
        password: {{ .Values.vmauth_password }}
        url_map:
        url_prefix: http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093/

VMAuth, users та routes

Є можливість створити одного користувача, і з url_map йому налаштувати кілька роутів – в залежності від URI запиту, він буде перенаправлений на відповідний бекенд, а з default_url задати URL, куди будуть перенаравлені запроси, для яких не задано роута. При цьому в роутах можна використовувати регулярки.

Наприклад:

...
    users:
      - username: vmadmin
        password: {{ .Values.vmauth_password }}
        url_map:
        - src_paths:
          - /alertmanager.*
          url_prefix: http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093/
        - src_paths:
          - /vmui.*
          url_prefix: http://vmsingle-vm-k8s-stack.dev-monitoring-ns.svc:8429
        default_url:
          - https://google.com

Якщо плануєте додавати доступ до інстансу VMSingle – додайте блок для Prometheus, бо інакше будуть помилки виду:

{“ts”:”2023-08-22T14:37:43.363Z”,”level”:”warn”,”caller”:”VictoriaMetrics/app/vmauth/main.go:159″,”msg”:”remoteAddr: \”10.0.0.74:25806, X-Forwarded-For: 217.***.***.253\”; requestURI: /prometheus/vmui/custom-dashboards; missing route for \”/prometheus/v
mui/custom-dashboards\””}
{“ts”:”2023-08-22T14:37:43.396Z”,”level”:”warn”,”caller”:”VictoriaMetrics/app/vmauth/main.go:159″,”msg”:”remoteAddr: \”10.0.0.74:25806, X-Forwarded-For: ***.***.165.253\”; requestURI: /prometheus/api/v1/label/__name__/values; missing route for \”/promet
heus/api/v1/label/__name__/values\””}

Для Prometheus блок виглядає аналогічно:

...
      - src_paths:
        - /prometheus.*
        url_prefix: http://vmsingle-vm-k8s-stack.dev-monitoring-ns.svc:8429

Для того, щоб сам Alertmanager працював через URI /alertmanager – в його values налаштовуємо routePrefix:

...
  alertmanager:
    enabled: true 
    spec:
      configSecret: "alertmanager-config"
      routePrefix: "/alertmanager"
...

І не забудьте в такому випадку змінити дефолтний URL для VMAlert у його values:

...
  vmalert:
    annotations: {}
    enabled: true
    spec:  
      notifier:
        url: "http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093/alertmanager"
...

Деплоїмо зміни, а щоб застосувати зміни конфіг в самому інстансі VMAuth, виконуємо запит до ендпоінту /-/reload, тобто https://vmauth.dev.example.co/-/reload.

Тепер Alertmanager доступний за адресою https://vmauth.dev.example.co/alertmanager:

Насправді, настройка src_paths може бути трохи геморною, бо, наприклад, в документації роути вказані просто як /uri/path:

url_map:
- src_paths:
  - /api/v1/query
  - /api/v1/query_range

Але коли я почав це робити, то виявилось, що при виконанні редіректу з VMAuth на внутрішній сервіс в кінці додається зайвий слеш, і доступ до Alertmanager не працював.

Саме тому в моїх прикладах вище роути задані з “.*“.

Години дві спілкувався з саппортом в VictoriaMetrcis Slack, намагались знайти причину проблем з доступом к Alertmanager, наче знайшли, завів GitHub issue, подивимось, як воно буде далі.

Взагалі, підтримку VictoriaMetrics варто згадати окремо, бо працює вона чудово і досить швидко. Є Slack, є Telegram-канал.

Basic Auth vs Bearer token

Замість звичайного логіна:пароля можемо використати ServiceAccount токен.

Створюємо ServiceAccount та Secret для нього з типом kubernetes.io/service-account-token:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: vmauth-sa
  namespace: dev-monitoring-ns
secrets:
- name: vmauth-token-secret
---
apiVersion: v1
kind: Secret
metadata:
  name: vmauth-token-secret
  namespace: dev-monitoring-ns
  annotations:
    kubernetes.io/service-account.name: vmauth-sa
type: kubernetes.io/service-account-token

Деплоїмо, отримуємо токен для цього ServicAccount:

[simterm]

$ kk -n dev-monitoring-ns create token vmauth-sa 
eyJhbGciOi***gfeNGWVjJn5-LWd2aslxAwnUTpQ

[/simterm]

Додаємо bearer_token в конфіг VMAuth:

...
    users:
    - username: vmadmin
      password: {{ .Values.vmauth_password }}
      url_map:
      - src_paths:
        - /alertmanager.*
        url_prefix: http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093
      - src_paths:
        - /vmui.*
        url_prefix: http://vmsingle-vm-k8s-stack.dev-monitoring-ns.svc:8429
      - src_paths:
        - /prometheus.*
        url_prefix: http://vmsingle-vm-k8s-stack.dev-monitoring-ns.svc:8429
    - bearer_token: "eyJhbGciOiJSUzI1NiIsImtpZ***gfeNGWVjJn5-LWd2aslxAwnUTpQ"
      url_prefix: http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093

Деплоїмо, знов робимо /-/reload, та перевіряємо доступ.

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

[simterm]

$ token="eyJhbGciOiJSUzI1NiIsImt***-LWd2aslxAwnUTpQ"wnUTpQ

[/simterm]

І з curl відкриваємо ендпоінт:

[simterm]

$ curl -H "Authorization: Bearer ${token}" https://vmauth.dev.example.co/
<a href="/alertmanager">Found</a>.

[/simterm]

VMAuth та “AnyService”

Ну і на останнє – VMAuth можна використовувати для аутентифікації не тільки VictoriaMetrics та її сервісів, а (майже) будь-яких.

Наприклад, маємо под з Nginx Demo:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  labels:
    app: my-pod
spec:
  containers:
    - name: my-container
      image: nginxdemos/hello
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: my-pod
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

У VMAuth додаємо роут:

...
      - src_paths:
        - /nginxdemo.*
        url_prefix: http://my-service.default.svc:80

І тепер за адресою https://vmauth.dev.example.co/nginxdemo попадаемо на Nginx:

А от для стандартної Kubernetes Dashboard так не вийде, бо вона використовує self-signed TLS сертифиікат, і VMAuth не підключається до відповідного сервісу, бо не може провалідувати сертификат. Можливо, є рішення, але не шукав, бо в принципі не потрібно.

VMAuth Self-Security

Див. документацію.

Закрийте аутентифікацією “службові” роути самого VMAuth.

У values.yaml додаємо ключі:

...
  extraArgs:
    reloadAuthKey: password
    flagsAuthKey: password
    metricsAuthKey: password
    pprofAuthKey: password
...

Деплоїмо, і тепер якщо викликати /-/reload без ключа – буде помилка:

[simterm]

$ curl https://vmauth.dev.example.co/-/reload
The provided authKey doesn't match -reloadAuthKey

[/simterm]

Щоб передати ключ для аутентифікації – використовуємо форму /-/reload?authKey=password:

[simterm]

$ curl -I https://vmauth.dev.example.co/-/reload?authKey=password
HTTP/2 200 

[/simterm]

Поки що наче немає можливості передачи ключі через Kubertes Secret, тільки хардкодити у values.yaml, але вже є фіча-реквест.

Loading

AWS: знайомство з Karpenter для автоскейлінгу в EKS, та встановлення з Helm-чарту
0 (0)

18 Серпня 2023

На всіх попередніх проектах, де був Kubernetes я використовував AWS Elastic Kubernetes Service, а для скейлінгу його WorkerNodes – Cluster Autoscaler (CAS), бо в принципі інших варіантів раньше не було.

В цілому, CAS працював добре, проте в листопаді 2020 AWS випустив власне рішення для скейлінгу нод для EKS – Karpenter, і якщо спочатку відгуки були неоднозначні, то останні його версії дуже хвалять, а тому вирішив на новому проекті спробувати його.

Karpenter overview та Karpenter vs Cluster Autoscaler

Отже, що таке Karpenter? Це автоскейлер, який запускає нові WorkerNodes, коли Kubernetes має поди, які не може запустити через нестачу ресурсів на існуючих WorkerNodes.

На відміну від CAS, він вміє автоматично вибирати найбільш відповідний тип інстансу в залежності від потреб подів, які треба запустити.

Крім того, він може керувати подами на нодах, щоб оптимізувати їх розміщення по серверам для того, щоб виконати де-скейлінг WorkerNodes, які можна зупинити для оптимізації вартості кластеру.

Ще з приємних можливостей це те, що на відміну від CAS вам не потрібно створювати декілька WorkerNodes groups з різними типами інстансів – Karpenter сам може визначити необхідний для поду/ів тип ноди, і створити нову ноду – більше ніяк мук вибора “Managed чи Self-managed нод-групи” – ви просто описуєте конфигурацію того, які типи інстасів можна використовувати, і Karpenter сам створить ту ноду, яка потрібна для кожного нового поду.

Фактично, ви взагалі лишаєте осторонь потребу у взаємодії з AWS по менеджменту EC2 – це все бере на себе єдиний компонент, Karpenter.

Також, Karpenter вміє обробляти Terminating та Stopping Events на ЕС2, і переміщати поди з нод, які будуть зупинені – див. native interruption handling.

Karpenter Best Practices

Повний список є на сторінці Karpenter Best Practices, рекомендую його проглянути. Там же є й EKS Best Practices Guides – теж цікаво ознайомитись.

Тут тезісно основні корисні поради:

  • Керучий под Karpenter треба запускати або у Fargate, або на звичайній ноді з Autoscale NodeGroup (скоріш за все, я буду створювати одну звичайну ASG для всіх крітікал-сервісів с лейблою типу “critcal-addons” – Karpenter, aws-load-balancer-controller, coredns, ebs-csi-controller, external-dns, etc.)
  • налаштуйте Interruption Handling – тоді Karpeneter буде переносити існуючі поди з ноди, яку буде видалено або запинено Амазоном
  • якщо Kubernetes API не доступен ззовні (а так і має бути), то налаштуйте AWS STS VPC endpoint для VPC кластеру
  • створіть різні provisioners для різних команд, які користуються різними типами інстансів (наприклад, для Bottlerocket та Amazon Linux)
  • налаштуйте consolidation для ваших provisioners – тоді Karpeneter буде намагатись переміщати запущені поди на існучі ноди, або на меншу ноду, яка буде дешевше існуючої
  • використовуйте Time To Live для нод, створених Karpenter, щоб видаляти ноди, які не використовуються, див. How Karpenter nodes are deprovisioned
  • додавайте аннотацію karpenter.sh/do-not-evict для подів, які небажано зупиняти – тоді Karpenter не буде видялти ноду, на якій такі поди запущені навіть після закінчення TTL цієї ноди
  • використовуйте Limit Ranges для налаштування дефолтних обмежень на resources подів

Виглядає все досить цікаво – давайте пробувати запускати його.

Встановлення Krapenter

Будемо використовувати Krapenter Helm-чарт.

Пізніше зробимо нормально, через автоматизацію, поки що для знайомства – руками.

AWS IAM

KarpenterInstanceNodeRole Role

Переходимо в АІМ Roles, створюємо нову роль для менеджменту WorkerNodes:

Додаємо Amazon-managed полісі:

  • AmazonEKSWorkerNodePolicy
  • AmazonEKS_CNI_Policy
  • AmazonEC2ContainerRegistryReadOnly
  • AmazonSSMManagedInstanceCore

Зберігаємо як KarpenterInstanceNodeRole:

KarpenterControllerRole Role

Додаємо другу роль – для самого Karpenter, тут політику описуємо самі у JSON.

Переходимо у IAM > Policies, створюємо власну полісі:

{
    "Statement": [
        {
            "Action": [
                "ssm:GetParameter",
                "iam:PassRole",
                "ec2:DescribeImages",
                "ec2:RunInstances",
                "ec2:DescribeSubnets",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeLaunchTemplates",
                "ec2:DescribeInstances",
                "ec2:DescribeInstanceTypes",
                "ec2:DescribeInstanceTypeOfferings",
                "ec2:DescribeAvailabilityZones",
                "ec2:DeleteLaunchTemplate",
                "ec2:CreateTags",
                "ec2:CreateLaunchTemplate",
                "ec2:CreateFleet",
                "ec2:DescribeSpotPriceHistory",
                "pricing:GetProducts"
            ],
            "Effect": "Allow",
            "Resource": "*",
            "Sid": "Karpenter"
        },
        {
            "Action": "ec2:TerminateInstances",
            "Condition": {
                "StringLike": {
                    "ec2:ResourceTag/Name": "*karpenter*"
                }
            },
            "Effect": "Allow",
            "Resource": "*",
            "Sid": "ConditionalEC2Termination"
        }
    ],
    "Version": "2012-10-17"
}

Зберігаємо як KarpenterControllerPolicy:

Створюємо другу IAM Role з цією політикою.

IAM OIDC identity provider вже повинні мати, якщо нема – то йдемо у документацію Creating an IAM OIDC provider for your cluster.

На початку створення ролі у Select trusted entity вибираємо Web Identity, а в Identity provider – OpenID Connect provider URL нашого кластеру. В Audience вибираємо sts.amazonaws.com:

Далі, підключаємо політику, яку робили вище:

Зберігаємо як KarpenterControllerRole.

Trusted Policy має виглядати так:

IAM Service Account з ролью KarpenterControllerRole буде створено самим чартом.

Security Groups та Subnets tags для Karpenter

Далі треба додати тег Key=karpenter.sh/discovery,Value=${CLUSTER_NAME} до SecurityGroups та Subnets, які використовуються існуючими WorkerNodes, і в яких потім Karpenter буде створювати нові.

В How do I install Karpenter in my Amazon EKS cluster? є приклад, як це зробити двома командами, але я як завжди перший раз вважаю за краще зробити це руками.

Знаходимо SecurityGroups та Subnets нашої WorkerNode AutoScaling Group – вона у нас зараз одна, тож це буде просто:

Додаємо теги:

Повторюємо для Subnets.

aws-auth ConfigMap

Додаємо в aws-auth новую роль для майбутніх WorkerNodes, щоб вони могли приєднатися до кластеру.

Див. Enabling IAM principal access to your cluster.

Бекапимо ConfigMap:

[simterm]

$ kubectl -n kube-system get configmap aws-auth -o yaml > aws-auth-bkp.yaml

[/simterm]

Редагуємо її:

[simterm]

$ kubectl -n kube-system edit configmap aws-auth

[/simterm]

В блок mapRoles додаємо новий мапінг – нашої ролі для WorkerNodes до RBAC-груп system:bootstrappers та system:nodes, в rolearn вказуємо IAM роль KarpenterInstanceNodeRole, яку робили для майбутніх WorkerNodes:

...
- groups:
  - system:bootstrappers
  - system:nodes
  rolearn: arn:aws:iam::492***148:role/KarpenterInstanceNodeRole
  username: system:node:{{EC2PrivateDNSName}}
...

В мене чомусь додано однією строкою, можливо, це кривий CDK криво створив, бо з eksctl наскільки пам’ятаю створювалось нормально:

Перепишемо трохи, і додаємо новий мапінг.

Будьте тут уважні, бо можна розвалити кластер. В Production такого руками краще не робити – це все повинно бути в коді автоматизації Terraform/CDK/Pulumi/etc:

Перевіряємо, що не зламали доступи – глянемо ноди:

[simterm]

$ kk get node
NAME                         STATUS   ROLES    AGE   VERSION
ip-10-0-2-173.ec2.internal   Ready    <none>   28d   v1.26.4-eks-0a21954
ip-10-0-2-220.ec2.internal   Ready    <none>   38d   v1.26.4-eks-0a21954
...

[/simterm]

Працює? ОК.

Встановлення Karpenter Helm chart

В How do I install Karpenter in my Amazon EKS cluster? знов пропонується якесь збочення с helm template, хоча робоче.

Ми просто створимо власний values.yaml – це буде корисно для майбутньої автоматизації, де задамо nodeAffinity та інші параметри для чарту.

Дефолтний values самого чарту – тут>>>.

Перевіряємо labels нашої ноди:

[simterm]

$ kk get node ip-10-0-2-173.ec2.internal -o json | jq -r '.metadata.labels."eks.amazonaws.com/nodegroup"'
EKSClusterNodegroupNodegrou-zUKXsgSLIy6y

[/simterm]

В своєму файлі values.yaml описуємо affinity – першу частину не міняємо, в другій – в key=eks.amazonaws.com/nodegroup задаємо ім’я нод-групи, EKSClusterNodegroupNodegrou-zUKXsgSLIy6y:

affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: karpenter.sh/provisioner-name
          operator: DoesNotExist
      - matchExpressions:
        - key: eks.amazonaws.com/nodegroup
          operator: In
          values:
          - EKSClusterNodegroupNodegrou-zUKXsgSLIy6y

В serviceAccount додаємо аннотацію з ARN нашої IAM-ролі KarpenterControllerRole:

...
serviceAccount:
  create: true
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::492***148:role/KarpenterControllerRole

Додаємо блок settings – тут в принципі все зрозуміло з назв параметрів.

Єдине, що в defaultInstanceProfile задаємо не повний ARN ролі, а тільки її ім’я:

...
settings:
  aws:
    clusterName: eks-dev-1-26-cluster
    clusterEndpoint: https://2DC***124.gr7.us-east-1.eks.amazonaws.com
    defaultInstanceProfile: KarpenterInstanceNodeRole

Тепер ми готові к деплою.

Знаходимо актуальну версію Karpenter на сторінці релізів.

Так як деплоїмо для тесту, то можна взяти останню на сьогодні – v0.30.0-rc.0.

Деплоїмо з Helm OCI registry:

[simterm]

$ helm upgrade --install --namespace dev-karpenter-system-ns --create-namespace -f values.yaml karpenter oci://public.ecr.aws/karpenter/karpenter --version v0.30.0-rc.0 --wait

[/simterm]

Перевіряємо поди:

[simterm]

$ kk -n dev-karpenter-system-ns get pod
NAME                         READY   STATUS    RESTARTS   AGE
karpenter-78f4869696-cnlbh   1/1     Running   0          44s
karpenter-78f4869696-vrmrg   1/1     Running   0          44s

[/simterm]

Ок, все є.

Створення Default Provisioner

Тепер ми можемо починати налаштовувати автоскейлінг.

Для цього першим додаємо Provisioner, див. Create Provisioner.

В ресурсі Provisioner описуємо які типи EC2-інстансів використовувати, у providerRef задаємо значення імені ресурсу AWSNodeTemplate, у consolidation – включаємо переміщення подів для оптимізації використання WorkerNodes.

Всі параметри є у Provisioners – дуже корисно їх подивитись.

Готові приклади є в репозиторії – examples/provisioner.

В ресурсі AWSNodeTemplate описується де саме створювати нові ноди – по тегу karpenter.sh/discovery=eks-dev-1-26-cluster, який ми завали раніше на SecurityGroups та Subnets.

Всі параметри для AWSNodeTemplate є у Node Templates.

Отже, що треба:

  • використовувати тільки T3 small, medium або large
  • тільки в AvailabilityZone us-east-1a та us-east-1b

Створюємо маніфест:

apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: default
spec: 
  requirements:
    - key: karpenter.k8s.aws/instance-family
      operator: In
      values: [t3]
    - key: karpenter.k8s.aws/instance-size
      operator: In
      values: [small, medium, large]
    - key: topology.kubernetes.io/zone
      operator: In
      values: [us-east-1a, us-east-1b]
  providerRef:
    name: default
  consolidation: 
    enabled: true
  ttlSecondsUntilExpired: 2592000
  ttlSecondsAfterEmpty: 30
---
apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
  name: default
spec:
  subnetSelector:
    karpenter.sh/discovery: eks-dev-1-26-cluster
  securityGroupSelector:
    karpenter.sh/discovery: eks-dev-1-26-cluster

Створюємо ресурси:

[simterm]

$ kk -n dev-karpenter-system-ns apply -f provisioner.yaml 
provisioner.karpenter.sh/default created
awsnodetemplate.karpenter.k8s.aws/default created

[/simterm]

Перевірка роботи автоскейлінгу з Karpenter

Щоб перевірити що все працює – можна заскейлити існуючу NodeGroup, видаливши з неї вілька EC2-інстансів.

В цьому Kubenetes зараз працює nskmrb наш моніторинг – трохи поломаємо його ^-)

Міняємо параметриAutoScale Group:

Або створити Deployment, подам якого задати багато requests і кількість replicas:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 50
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-container
          image: nginx
          resources:
            requests:
              memory: "2048Mi"
              cpu: "1000m"
            limits:
              memory: "2048Mi"
              cpu: "1000m"
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: my-app

Дивимось логи Karpenter – створено новий інстанс:

[simterm]

2023-08-18T10:42:11.488Z        INFO    controller.provisioner  computed 4 unready node(s) will fit 21 pod(s)   {"commit": "f013f7b"}
2023-08-18T10:42:11.497Z        INFO    controller.provisioner  created machine {"commit": "f013f7b", "provisioner": "default", "machine": "default-p7mnx", "requests": {"cpu":"275m","memory":"360Mi","pods":"9"}, "instance-types": "t3.large, t3.medium, t3.small"}
2023-08-18T10:42:12.335Z        DEBUG   controller.machine.lifecycle    created launch template {"commit": "f013f7b", "machine": "default-p7mnx", "provisioner": "default", "launch-template-name": "karpenter.k8s.aws/15949964056112399691", "id": "lt-0288ed1deab8c37a7"}
2023-08-18T10:42:12.368Z        DEBUG   controller.machine.lifecycle    discovered launch template      {"commit": "f013f7b", "machine": "default-p7mnx", "provisioner": "default", "launch-template-name": "karpenter.k8s.aws/10536660432211978551"}
2023-08-18T10:42:12.402Z        DEBUG   controller.machine.lifecycle    discovered launch template      {"commit": "f013f7b", "machine": "default-p7mnx", "provisioner": "default", "launch-template-name": "karpenter.k8s.aws/15491520123601971661"}
2023-08-18T10:42:14.524Z        INFO    controller.machine.lifecycle    launched machine        {"commit": "f013f7b", "machine": "default-p7mnx", "provisioner": "default", "provider-id": "aws:///us-east-1b/i-060bca40394a24a62", "instance-type": "t3.small", "zone": "us-east-1b", "capacity-type": "on-demand", "allocatable": {"cpu":"1930m","ephemeral-storage":"17Gi","memory":"1418Mi","pods":"11"}}

[/simterm]

Та за хвилину перевіряємо ноди в кластері:

[simterm]

$ kk get node
NAME                         STATUS   ROLES    AGE     VERSION
ip-10-0-2-183.ec2.internal   Ready    <none>   6m34s   v1.26.6-eks-a5565ad
ip-10-0-2-194.ec2.internal   Ready    <none>   19m     v1.26.4-eks-0a21954
ip-10-0-2-212.ec2.internal   Ready    <none>   6m38s   v1.26.6-eks-a5565ad
ip-10-0-3-210.ec2.internal   Ready    <none>   6m38s   v1.26.6-eks-a5565ad
ip-10-0-3-84.ec2.internal    Ready    <none>   6m36s   v1.26.6-eks-a5565ad
ip-10-0-3-95.ec2.internal    Ready    <none>   6m35s   v1.26.6-eks-a5565ad

[/simterm]

Або в AWS Console по тегу karpenter.sh/managed-by:

Готово.

Що лишилось зробити:

  • для дефолтної Node Group, яка створюється з кластером з AWS CDK додати тег critical-addons=true та tains на NoExecute і NoSchedule – це буде саме окрема група для всякіх контролерів (див. Kubernetes: Pods та WorkerNodes – контроль розміщення подів на нодах)
  • в автоматизації створення кластеру для WorkerNodes SecurityGroups та Private Subnets додати теги Key=karpenter.sh/discovery,Value=${CLUSTER_NAME}
  • у values чартів для деплою AWS ALB Controller, ExternalDNS та власне Karpenter додати tolerations на тег critical-addons=true та taints  NoExecute і NoSchedule

На разі наче все.

Всі поди піднялись, все працює.

І пара корисних команд для перевірки статусу подів/нод.

Вивести кількість подів на кожній ноді:

[simterm]

$ kubectl get pods -A -o jsonpath='{range .items[?(@.spec.nodeName)]}{.spec.nodeName}{"\n"}{end}' | sort | uniq -c | sort -rn

[/simterm]

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

[simterm]

$ kubectl get pods --all-namespaces -o wide --field-selector spec.nodeName=ip-10-0-2-212.ec2.internal

[/simterm]

Окремо ще можна додати плагінів для kubectl, які відображають зайняті ресурси на нодах – див. Kubernetes: менеджер плагинов Krew и полезные плагины для kubectl.

О, і ще треба погратись з Vertical Pod Autoscaler – як Karpenter буде робити з ним.

Корисні посилання

Loading

Kubernetes: Pods та WorkerNodes – контроль розміщення подів на нодах
0 (0)

17 Серпня 2023

Kubernetes дозволяє дуже гнучко керувати тим, як його Pods будуть розташовані на серверах, тобто WorkerNodes.

Це може бути корисним, якщо вам треба запускати под на специфічній конфігурації ноди, наприклад – WorkerNode повинна мати GPU, або SSD замість HDD. Інший приклад, це коли вам потрібно розміщати окремі поди поруч, щоб зменшити затримку їхньої комунікації, або зменшити cross Availability-zone трафік (див. AWS: Grafana Loki, InterZone трафік в AWS, та Kubernetes nodeAffinity).

І, звісно, це важливе для побудування High Availability та Fault Tolerance архітектури, коли вам потрібно розділити поди по окремим нодам або Availability-зонам.

Ми маємо чотири основних підходи для контролю того, як Kubernetes Pods будуть розміщатись на WorkerNodes:

  • налаштувати Nodes таким чином, що вони будуть приймати тільки окремі поди, які відповідають заданим на ноді крітеріям
    • taints and tolerations: на ноді задаємо taint, для якого поди повинні мати відповідний toleration, щоб запустись на цій ноді
  • нашалтувати сам Pod таким чином, що він буде вибирати тільки окремі Nodes, які відповідають заданим у поді крітеріям
    • для цього використовуємо nodeName – вибирається нода тільки с заданним ім’ям
    • або nodeSelector для вибору ноди з відповідними labels і їх значеннями
    • або nodeAffinity та nodeAntiAffinity – правила, за якими Kubernetes Scheduler буде вибирати ноду, на якій запустить под, в залежності від параметрів цієї ноди
  • налаштувати сам Pod таким чином, що він буде вибирати Node в залежності від того, як запущені інші Pods
    • для цього використовуємо podAffinity та podAntiAffinity – правила, за якими Kubernetes Scheduler буде вибирати ноду, на якій запустить под, в залежності від інших подів на цій ноді
  • і окрема тема – Pod Topology Spread Constraints, тобто правила розміщення Pods по failure-domains – регіонам, Availability-зонам чи нодам

kubectl explain

Ви завжди можете прочитати відповідну документацію по будь-якому параметру або ресурсу, використовуючи kubectl explain:

[simterm]

$ kubectl explain pod
KIND:       Pod
VERSION:    v1

DESCRIPTION:
    Pod is a collection of containers that can run on a host. This resource is
    created by clients and scheduled onto hosts.
...

[/simterm]

Або:

[simterm]

$ kubectl explain Pod.spec.nodeName
KIND:       Pod
VERSION:    v1

FIELD: nodeName <string>

DESCRIPTION:
    NodeName is a request to schedule this pod onto a specific node. If it is
    non-empty, the scheduler simply schedules this pod onto that node, assuming
    that it fits resource requirements.

[/simterm]

Node Taints та Pods Tolerations

Отже, перший варіант – це задати на нодах обмеження того, які поди на ній можуть бути запущені з використанням Taints та Tolerations.

Тут taint “відштовхує” поди які не мають відповідної toleration від ноди, а toleration – “тягне” под до специфічної ноди, яка має відповідний taint.

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

Задаємо tain з effect: NoSchedule – тобто, забороняємо створювати нові поди на цій ноді:

[simterm]

$ kubectl taint nodes ip-10-0-3-133.ec2.internal critical-addons=true:NoSchedule
node/ip-10-0-3-133.ec2.internal tainted

[/simterm]

Далі, створюємо под, якому вказуємо toleration з ключем "critical-addons":

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx:latest
  tolerations:
    - key: "critical-addons"
      operator: "Exists"
      effect: "NoSchedule"

І перевіряємо поди на цій ноді:

[simterm]

$ kubectl get pod --all-namespaces -o wide --field-selector spec.nodeName=ip-10-0-3-133.ec2.internal
NAMESPACE           NAME                                                              READY   STATUS    RESTARTS   AGE     IP           NODE                         NOMINATED NODE   READINESS GATES
default             my-pod                                                            1/1     Running   0          2m11s   10.0.3.39    ip-10-0-3-133.ec2.internal   <none>           <none>
dev-monitoring-ns   atlas-victoriametrics-loki-logs-zxd9m                             2/2     Running   0          10m     10.0.3.8     ip-10-0-3-133.ec2.internal   <none>           <none>
...

[/simterm]

Але звідки тут Loki? Тому що поки задали Taint – Scheduler встиг перенести на цю ноди под з Loki.

Щоб запобігти цьому – в Tain додаємо ключ NoExecute – тоді scheduler виконає Pod eviction, тобто “виселить” вже запущені поди з цієї ноди:

[simterm]

$ kubectl taint nodes ip-10-0-3-133.ec2.internal critical-addons=true:NoExecute

[/simterm]

Перевіряємо taints тепер:

[simterm]

$ kubectl get node ip-10-0-3-133.ec2.internal -o json | jq '.spec.taints'
[
  {
    "effect": "NoExecute",
    "key": "critical-addons",
    "value": "true"
  },
  {
    "effect": "NoSchedule",
    "key": "critical-addons",
    "value": "true"
  }
]

[/simterm]

Для нашого поду додаємо другий toleration, інакше і він буде evicted з цієї ноди:

...
  tolerations:
    - key: "critical-addons"
      operator: "Exists"
      effect: "NoSchedule"
    - key: "critical-addons"
      operator: "Exists"
      effect: "NoExecute"

Деплоїмо, та перевіряємо поди ще раз:

[simterm]

$ kubectl get pod --all-namespaces -o wide --field-selector spec.nodeName=ip-10-0-3-133.ec2.internal
NAMESPACE     NAME                                               READY   STATUS    RESTARTS   AGE   IP           NODE                         NOMINATED NODE   READINESS GATES
default       my-pod                                             1/1     Running   0          3s    10.0.3.246   ip-10-0-3-133.ec2.internal   <none>           <none>
kube-system   aws-node-jrsjz                                     1/1     Running   0          16m   10.0.3.133   ip-10-0-3-133.ec2.internal   <none>           <none>
kube-system   csi-secrets-store-secrets-store-csi-driver-cctbj   3/3     Running   0          16m   10.0.3.144   ip-10-0-3-133.ec2.internal   <none>           <none>
kube-system   ebs-csi-node-46fts                                 3/3     Running   0          16m   10.0.3.187   ip-10-0-3-133.ec2.internal   <none>           <none>
kube-system   kube-proxy-6ztqs                                   1/1     Running   0          16m   10.0.3.133   ip-10-0-3-133.ec2.internal   <none>           <none>

[/simterm]

Тепер на цій ноді тільки наш под, та поди з DaemonSets, які по дефолту мають запускатись на всіх нодах і мають відповідні tolerations, див. How Daemon Pods are scheduled.

Окрім Equal в умовах toleration, яка тільки перевіряє наявність заданої лейбли, можна виконати і перевірку значення ціїє лейбли.

Для цього в operator замість Exists вказуємо Equal, і додаємо value з потрібним значенням:

...
  tolerations:
    - key: "critical-addons"
      operator: "Equal"
      value: "true"
      effect: "NoSchedule"
    - key: "critical-addons"
      operator: "Equal"
      value: "true"
      effect: "NoExecute"

Щоб видалити tain – додаємо в кінці мінус:

[simterm]

$ kubectl taint nodes ip-10-0-3-133.ec2.internal critical-addons=true:NoSchedule-
node/ip-10-0-3-133.ec2.internal untainted
$ kubectl taint nodes ip-10-0-3-133.ec2.internal critical-addons=true:NoExecute-
node/ip-10-0-3-133.ec2.internal untainted

[/simterm]

Вибір ноди подом – nodeName, nodeSelector та nodeAffinity

Інший підхід, коли ми налаштовуємо пд таким чином, що “він” вибирає на якій ноді йому запускатись.

Для цього маємо nodeName, nodeSelector, nodeAffinity та nodeAntiAffinity. Див. Assign Pods to Nodes.

nodeName

Найпростіший способ. Має перевагу над всіма іншими:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx:latest
  nodeName: ip-10-0-3-133.ec2.internal

nodeSelector

З nodeSelector можемо вибирати ті ноди, які мають відповідні labels.

Додаємо лейблу:

[simterm]

$ kubectl label nodes ip-10-0-3-133.ec2.internal service=monitoring
node/ip-10-0-3-133.ec2.internal labeled

[/simterm]

Перевіряємо її:

[simterm]

$ kubectl get node ip-10-0-3-133.ec2.internal -o json | jq '.metadata.labels'
{
  ...
  "kubernetes.io/hostname": "ip-10-0-3-133.ec2.internal",
  "kubernetes.io/os": "linux",
  "node.kubernetes.io/instance-type": "t3.medium",
  "service": "monitoring",
  ...

[/simterm]

В маніфесті поду задаємо nodeSelector:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx:latest
  nodeSelector:
    service: monitoring

Якщо поду задано кілька лейбл в nodeSelector – то відповідна нода повинна мати всі ці лейбли, щоб на ній запустився цей под.

nodeAffinity та nodeAntiAffinity

nodeAffinity та nodeAntiAffinity діють так само, як і nodeSelector, але мають більш гнучкі можливості.

Наприклад, можна задати hard або soft ліміти запуску – для soft-ліміту scheduler спробує запустити под на відповідній ноді, а якщо не зможе – то запустить на іншій. Відповідно, якщо задати hard-ліміт, і scheduler не зможе запустити под на обраній ноді – то под залишиться в статусі Pending.

Hard-ліміт задається в полі .spec.affinity.nodeAffinity за допомогою requiredDuringSchedulingIgnoredDuringExecution, а soft – з preferredDuringSchedulingIgnoredDuringExecution.

Наприклад, можемо запустити под в AvailabiltyZone us-east-1a або us-east-1b, використовуючи node-label topology.kubernetes.io/zone:

[simterm]

$ kubectl get node ip-10-0-3-133.ec2.internal -o json | jq '.metadata.labels'
{
  ...
  "topology.kubernetes.io/region": "us-east-1",
  "topology.kubernetes.io/zone": "us-east-1b"
}

[/simterm]

Задаємо hard-limit:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx:latest
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - us-east-1a
            - us-east-1b

Або софт-ліміт. Наприклад, з неіснуючою лейблою:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx:latest
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: non-exist-node-label
            operator: In
            values:
            - non-exist-value

У такому випидаку под все одно буде запущено на будь-якій найбільш вільній ноді.

Також можна комбінувати умови:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx:latest
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - us-east-1a
            - us-east-1b
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: non-exist-node-label
            operator: In
            values:
            - non-exist-value

При використанні декількох умов в requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms – буде обрано перше, що збіглось з лейблами ноди.

При використанні декількох умов в matchExpressions – вони мають відповідати всі.

В operator можна використовувати оператори In, NotIn, Exists, DoesNotExist, Gt (greater than) та Lt (less than).

soft-limit та weight

У preferredDuringSchedulingIgnoredDuringExecution за допомогою weight можна задати “вагу” умови, задавши значення від 1 до 100.

В такому випадку, якщо всі інші умови співпали, scheduler вибере ту ноду, умова якої має найбільший weight:

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: nginx:latest
  affinity:
    nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - us-east-1a
      - weight: 100
        preference:
          matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - us-east-1b

Цей под буде запущено на ноді в регіоні us-east-1b:

[simterm]

$ kubectl get pod my-pod -o wide
NAME     READY   STATUS    RESTARTS   AGE   IP           NODE                         NOMINATED NODE   READINESS GATES
my-pod   1/1     Running   0          3s    10.0.3.245   ip-10-0-3-133.ec2.internal   <none>           <none>

[/simterm]

І зона цієї ноди:

[simterm]

$ kubectl get node ip-10-0-3-133.ec2.internal -o json | jq -r '.metadata.labels."topology.kubernetes.io/zone"'
us-east-1b

[/simterm]

podAffinity та podAntiAffinity

Аналогічно до вибору ноди за допомогою hard- та soft-лімітів, можна налаштувати Pod Affinity в залежності від того, які лейбли будуть у подів, які вже запущені на ноді. Див. Inter-pod affinity and anti-affinity.

Наприклад, є три поди Grafana Loki – Read, Write та Backend.

Ми хочемо запускати Read та Backend в одній AvailabilityZone, щоб уникнути cross-AZ трафіку, але при цьому хочемо, що вони не запускались на тих нодах, де є поди з Write.

Поди Loki мають відповідні до компоненту лейбли – app.kubernetes.io/component=read, app.kubernetes.io/component=backend та app.kubernetes.io/component=write.

Тож для Read задаємо podAffinity до подів з лейблою app.kubernetes.io/component=backend, та podAntiAffinity до подів з лейблою app.kubernetes.io/component=read:

...
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app.kubernetes.io/component
                operator: In
                values:
                - backend
            topologyKey: "topology.kubernetes.io/zone"
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app.kubernetes.io/component
                operator: In
                values:
                - write
            topologyKey: "kubernetes.io/hostname"
...

Тут в podAffinity.topologyKey ми вказуємо, що хочемо розміщати поди, використовуючи домен topology.kubernetes.io/zone – тобто topology.kubernetes.io/zone для Read має співпадати з подом Backend.

А в podAntiAffinity.topologyKey задаємо kubernetes.io/hostname – тобто не розміщати на WorkerNodes, де є поди з лейблою app.kubernetes.io/component=write.

Деплоїмо, та перевіряємо де є под з Write:

[simterm]

$ kubectl -n dev-monitoring-ns get pod loki-write-0 -o json | jq '.spec.nodeName'
"ip-10-0-3-53.ec2.internal"

[/simterm]

Та в якій AvailabilityZone ця нода:

[simterm]

$ kubectl -n dev-monitoring-ns get node ip-10-0-3-53.ec2.internal -o json | jq -r '.metadata.labels."topology.kubernetes.io/zone"'
us-east-1b

[/simterm]

Перевіряємо де знаходиться под з Backend:

[simterm]

$ kubectl -n dev-monitoring-ns get pod loki-backend-0 -o json | jq '.spec.nodeName'
"ip-10-0-2-220.ec2.internal"

[/simterm]

І його зона:

[simterm]

$ kubectl -n dev-monitoring-ns get node ip-10-0-2-220.ec2.internal -o json | jq -r '.metadata.labels."topology.kubernetes.io/zone"'
us-east-1a

[/simterm]

І тепер под з Read:

[simterm]

$ kubectl -n dev-monitoring-ns get pod loki-read-698567cdb-wxgj5 -o json | jq '.spec.nodeName'
"ip-10-0-2-173.ec2.internal"

[/simterm]

Нода інша, ніж у пода Write або Backend, але:

[simterm]

$ kubectl -n dev-monitoring-ns get node ip-10-0-2-173.ec2.internal -o json | jq -r '.metadata.labels."topology.kubernetes.io/zone"'
us-east-1a

[/simterm]

Та сама AvailabilityZone, що й в поду Backend.

Pod Topology Spread Constraints

Ми можемо налаштувати Kubernetes Scheduler таким чином, щоб він розподіляв под по “доменам”, тобто – по нодам, регіонам або Availability-зонам. Див. Pod Topology Spread Constraints.

Для цього в полі spec.topologySpreadConstraints задаються параметри, які описують як саме будуть створені поди.

Наприклад, у нас є 5 WorkerNode в двох AvailabilityZone.

Ми хочемо запустити 5 подів, і задля fault tolerance ми хочемо, щоб кожен под був розміщений на окремій ноді.

Тоді наш конфіг для Deployment може виглядати так:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 5
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-container
          image: nginx:latest
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: my-app

Тут:

  • maxSkew: максимальна різниця в кількості подів в одному домені (topologyKey)
    • грає роль тільки якщо whenUnsatisfiable=DoNotSchedule, при whenUnsatisfiable=ScheduleAnyway под буде створено незалежно від умов
  • whenUnsatisfiable: може мати значення DoNotSchedule – не дозволяти створювати поди, або ScheduleAnyway
  • topologyKey: WorkerNode label, по якій буде обрано домен, тобто по якій лейблі групуємо ноди, на яких розраховується розміщення подів
  • labelSelector: які поди враховувати при розміщенні подів (наприклад, якщо поди з різних Deployment, але мають розміщувати однаково – то в обох Deployment налаштовуємо topologySpreadConstraints з взаємними labelSelector)

Крім того, можна задати параметри nodeAffinityPolicy та/або nodeTaintsPolicy зі значеннями Honor або Ignore – враховувати nodeAffinity або nodeTaints при розрахунку розміщення подів, чи ні.

Деплоїмо та перевіряємо ноди цих подів:

[simterm]

$ kk get pod -o json | jq '.items[].spec.nodeName'
"ip-10-0-3-53.ec2.internal"
"ip-10-0-3-22.ec2.internal"
"ip-10-0-2-220.ec2.internal"
"ip-10-0-2-173.ec2.internal"
"ip-10-0-3-133.ec2.internal"

[/simterm]

Всі розміщені на окремих нодах.

Loading

Grafana: values з записів в логах Loki, та dual-Y-axes графіки в Grafana
0 (0)

15 Серпня 2023

Фукція в AWS Lambda пише логи в CloudWatch Logs, звідки ми через lambda-promtail забираємо їх в Grafana Loki, звідки потім можемо використати в графіках Grafana.

Що треба зробити: в логах пишеться час “Init duration” та “Max Memory Used”.

В CloudWatch таких метрик нема, а нам цікаво мати графік по цим данним, бо це може бути ознакою cold start, які ми хочемо відслідковувати.

Тож треба:

  • отримати ці дані і використати як values в графіку
  • побудувати графік, де зліва будуть відображатись мілісекунди на запуск, а справа – скільки пам’яті при цьому було використано

Grafana Loki і values з labels

Тож що можемо зробити:

  • зі stram selector вибираємо файл логу потрібної фунції
  • через log filter вибираємо  записи, які містять строку “Init Duration”
  • з log parcer regex отримуємо значення з Max Memory Used або Init duration, і створюємо нову label з цим значенням

Тобто для створеня лейбли max_mem_use повністю запит буде таким:

{__aws_cloudwatch_log_group="/aws/lambda/app-prod-ApiHandler-v3"} |~ "Init Duration"| regexp ".*Max Memory Used[\\s\\S]{2}(?P<max_mem_use>.*) MB.*"

Окей, лейблу отримали – далі  треба побудувати графік, тобто створити metric query і використати значення з лейбли max_mem_use як value.

Для цього беремо unwrap expression, і вказуємо ім’я лейбли, значення котрої хочемо відобразити:

sum(sum_over_time({__aws_cloudwatch_log_group="/aws/lambda/app-prod-ApiHandler-v3"} |= "Init Duration"| regexp ".*Max Memory Used[\\s\\S]{2}(?P<max_mem_use>.*) MB.*" | unwrap max_mem_use [15m]))

Та отримуємо суму всіх записів в лог-файлі – між 08:07:28 і 08:08:29 маємо сумарно 1773 мегабайти:

Перевіряємо в логах – вибираємо записи за цей проміжок, 08:07:00 і 08:09:00:

Отримуємо 9 записів, в кожній 197 мегабайт – сумарно 1773.

Grafana panel і dual-Y-axes

В графіках Grafana є можливість відображати на одному графіку результати двох запитів по різним осям і з різним розташуванням.

Тобто з лівої сторони по осі Y (вертикалі) виводити одні значення, з правої по Y – інші, а по осі X (горизонталі) – треті, як правило тут час.

Проте налаштовується це не зовсім очевидно, а спроби загуглити приводять до посту Learn Grafana: How to use dual axis graphs за 2020 рік, який застарів, бо зараз це робиться через Overrides.

Отже, маємо графік з двома Query:

Далі, Init Duration в мілісекундах хочемо відображати зліва як Unit > miliseconds, а Memory Used – справа як Unit > megabytes.

З Init Duration все просто – налаштовуємо Standard options > Unit > ms:

А для Memory – йдемо в Overrides і додаємо нові параметри для поля max_mem_use:

В Property вибираємо Axis > Placement:

І встановлюємо значення Right:

Далі, щоб відображати юніт як мегабайти – додаємо другий Override property – Unit:

І встановлюємо значення megabytes:

Тепер на графіку з однієї сторони маємо час запуску функції, а з другої – скільки пам’яті вона при цьому споживала, і явно бачимо кореляцію між цими значеннями:

Єдине, що ці дані все ж не зовсім вірно допоможуть з визначенням саме cold starts, так як в цей проміжок просто було багато запитів до API Gateway > Lambda, і вона запускалась в декількох інстансах – тому і маємо спайк на графіку Init duration та Memory:

Тому треба трохи переробити: запити з Loki винести в Recording Rules та писати у вигляді звичайних метрик в Prometheus/VictoriaMetrics, а потім в Query графіку отримане з Loki значення ділити на кількість отриманиз записів з логу в цей період.

Додаємо два Recording Rules:

- name: Backend-Lambda

  rules:

  - record: aws:backend:lambda:init_duration:ms
    expr: sum(sum_over_time({__aws_cloudwatch_log_group=~"/aws/lambda/app-(dev|staging|prod)-ApiHandler-v3"} |= "Init Duration"| regexp ".*Init Duration[\\s\\S]{2}(?P<init_duration>.*) ms.*" | unwrap init_duration [15m])) by (__aws_cloudwatch_log_group)
          /
          sum(count_over_time({__aws_cloudwatch_log_group=~"/aws/lambda/app-(dev|staging|prod)-ApiHandler-v3"} |= "Init Duration" [15m])) by (__aws_cloudwatch_log_group)

  - record: aws:backend:lambda:max_mem_use:mb
    expr: sum(sum_over_time({__aws_cloudwatch_log_group=~"/aws/lambda/app-(dev|staging|prod)-ApiHandler-v3"} |= "Init Duration" | regexp ".*Max Memory Used[\\s\\S]{2}(?P<max_mem_use>.*) MB.*" | unwrap max_mem_use [15m])) by (__aws_cloudwatch_log_group)
          /
          sum(count_over_time({__aws_cloudwatch_log_group=~"/aws/lambda/app-(dev|staging|prod)-ApiHandler-v3"} |= "Init Duration" [15m])) by (__aws_cloudwatch_log_group)

В другій частині запиту через count_over_time отримуємо загальну кількість записів, які були отримані.

Робимо Query в Grafana:

aws:backend:lambda:init_duration:ms{__aws_cloudwatch_log_group="/aws/lambda/app-$environment-ApiHandler-v3"}

Та:

aws:backend:lambda:max_mem_use:mb{__aws_cloudwatch_log_group="/aws/lambda/app-$environment-ApiHandler-v3"}

І маємо більш корректний графік:

Готово.

Loading

Helm: multiple деплой одного чарта з Chart’s dependency
0 (0)

14 Серпня 2023

Для покращення перформансу Grafana Loki треба встановити декілька майже однакових інстансів Memcached, див. Grafana Loki: оптимізація роботи – Recording Rules, кешування та паралельні запити.

Сам стек моніторингу деплоїться з одного Хельм-чарту, в який через dependency файлу Chart.yaml додані залежності – Promtail, Loki, etc, див. VictoriaMetrics: створення Kubernetes monitoring stack з власним Helm-чартом.

Як це зробити, щоб не встановлювати окремими Helm-релізами, а деплоїти разом зі стеком, щоб і Memcached просто додати в dependencies головного чарту?

Как варіант – через alias.

Додаємо новий чарт три рази – для кожного інстансу Memcahced, і вкузуємо їм aliaschunk-cache, results-cache та index-cache:

apiVersion: v2
name: atlas-victoriametrics
description: A Helm chart for Atlas Victoria Metrics kubernetes monitoring stack
type: application
version: 0.1.0
appVersion: "1.16.0"
dependencies:
...
- name: prometheus-blackbox-exporter
  version: ~8.2.0
  repository: https://prometheus-community.github.io/helm-charts
- name: memcached
  version: ~6.5.6
  repository: https://charts.bitnami.com/bitnami
  alias: chunk-cache
- name: memcached
  version: ~6.5.6
  repository: https://charts.bitnami.com/bitnami
  alias: results-cache
- name: memcached
  version: ~6.5.6
  repository: https://charts.bitnami.com/bitnami
  alias: index-cache

Обновлюємо залежності локально:

[simterm]

$ helm dependency update

[/simterm]

Деплоїмо, та перевіряємо чи з’явились Memcached Services:

[simterm]

$ kk -n dev-monitoring-ns get svc | grep cache
atlas-victoriametrics-chunk-cache                      ClusterIP   172.20.120.24    <none>        11211/TCP                    15m
atlas-victoriametrics-index-cache                      ClusterIP   172.20.75.206    <none>        11211/TCP                    15m
atlas-victoriametrics-results-cache                    ClusterIP   172.20.212.198   <none>        11211/TCP                    14m

[/simterm]

Готово.

Loading

Підготовка до зими 2023-2024: електрохарчування
0 (0)

13 Серпня 2023

“The Winter is coming!” (c)

Що ж – зима наближається. Пора починати думати про забезпечення себе електрохарчуванням (с), бо пам’ятаючи минулу зиму – забезпечити себе електрикою треба, та й ціни на всяке електрообладнання почнуть рости дуже скоро.

Загальна задумка – забезпечити себе автономність на тиждень блекауту – будемо брати найбільш песимістичний варіант. На тиждень має вистачити енергії для роботи, тобто по мінімуму – ноутбук та живлення медіаконвертора з роутером.

Минулої зими я приблизно так і мав запасу – писав про ту підготовку у пості Підготовка до зими 2022-2023: інтернет, електрика, опалення, їжа та вода.

Окрема подяка @artygan за допомогу у розрахунках 🙂

Що таке Вольт, Ампер та Ват

Згадаймо школу 🙂 Бо я всі ці речі почав розбирати заново минулої зими, коли писав перший такий пост (але він лишився у чернетках).

Вольти: напруга. Це фізична величина, що характеризує величину відносини роботи електричного поля в процесі перенесення заряду з однієї точки A в іншу точку B до величини цього заряду.

Простіше кажучи це різниця потенціалів між двома точками. Вимірюється у Вольтах. Напруга схоже по суті з величиною тиску води в трубі – чим воно вище, тим швидше вода тече з крана.

Позначається як В або V.

Ампер: сила струму. Це фізична величина, рівна відношенню кількості заряду за певний проміжок часу, що протікає через провідник до величини цього самого проміжку часу.

Силу струму можна порівняти з потоком води з крана – чим більше ми його відкриваємо, тим більше води виливається за одиницю часу або навпаки.

Позначається як А.

Ват: потужність. Це швидкість виробництва або передачі енергії, це кількість енергії на одиницю часу. 1 Ват – величина потужності, при якій за одну секунду відбувається робота дорівнює одному джоулів. Отже, Ват – це похідна від інших величин одиниця. Так, наприклад, потужність співвідноситься з напругою в такий спосіб: Вт = В • А, де В – показник величини напруги, Вольти, а А – показник величини сили струму, Ампери.

Потужність можна порівняти з кількістю води в літрах, яке виллється з крана.

Позначається як W (Вт).

Не пам’ятаю звідки взяв, але в старій чернетці був такий опис:

– Напруга (V): ширина річки
– Сила струму (A): швидкість течії води в річці
Тому одну і ту ж потужність може дати й широка ріка (висока напруга, Вольт) з повільною течією (слабкий струм, Ампер) – і вузька річка (низька напруга, Вольт) зі швидкою течією (сильний струм, Ампер).
Чим швидше ріка і чим вона ширше – тим більше води (Ват) за одиницю часу.

Розрахунок часу роботи батареї/акумулятора

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

Можна скористатись онлайн-калькуляторами типу Калькулятор перетворення mAh в Wh, або порахувати самому.

Павербанки

Див. Як визначити реальну ємність повербанка і чому вона не відповідає заявленій та How many times can I recharge a cell phone with a power bank?

Головна мета покупки павербанок для мене – це живлення ноутбуку, тому давайте рахувати по ньому.

Наприклад візьмемо ноутбук ThinkPad 4750U.

На зарядному маємо показники напруги та сили току – 20 вольт та 3.25 ампери:

Формула вихідної потужності:

Ампер * вольти

Тобто максимальна потужність зарядного – 20 вольт * 3.25 ампери буде 65 ват, як і показано на самому зарядному.

Отже, якщо ноутбук буде працювати на повну потужність – це буде 65 ват-годин.

Далі, беремо банку, наприклад на 30.000 мА/г (30 А/г) з виходом 3.8 вольт, і рахуємо ват-години.

Формула розрахунку Ампер/годин в Ват/години:

мА/г * вольт / 1000

Тобто наша банка на 30.000 мА/г при робочій напрузі у 3.8 вольт має:

[simterm]

30000*3.8/1000
114

[/simterm]

114 ват/годин.

Насправді це теж вказано прямо на самій банці:

 

Тобто, ноутбук на повній потужності у 65 ват має працювати:

[simterm]

>>> 114/65
1.75

[/simterm]

Але реально він споживає близько 15 ват (можна глянути утілітою типу Powertop або Upower):

Тобто цієї банки має вистачити на 7.5 годин. Хоча насправді за 2 години ноут з цієї банки зжер 50%.

Але якщо рахувати так, як говориться у вищезгаданій статі:

Якщо поруч із ємністю вказаний стандартний для повербанка вольтаж (від 3,6 до 3,8 вольта), спочатку виконайте конвертацію напруги в 5 вольт. Для цього помножте номінальну ємність на вказаний для неї вольтаж і поділіть результат на 5. Далі, щоб врахувати втрати при конвертації та передачі енергії, помножте отриманий на попередньому етапі результат на 85% — це середній ККД при зарядці. У результаті отримаєте теоретичну реальну ємність свого повербанка.

То вийде:

[simterm]

>>> 30000*3.8/5*0.85
19380.0

[/simterm]

А це вже виходить 73 Вт/год, і тоді ноут на 15 ватах пропрацює ~4.5 годин – як і було на тесті.

Автомобільні акумулятори

Для AGM або гелевих акумуляторів формула така ж, але треба враховувати коефіцієнт розряду – 0.65, бо висажувати акум в нуль не можна, і інвертор має відключити живлення при падінні заряду до мінімально допустимого, або подати звуковий сигнал, що його треба виключати.

Див. Як вибрати акумулятор для ДБЖ.

Формула розрахунку часу роботи автомобільного акумулятору буде:

А/г * вольтр * 0.65

Тобто акумулятор на 100 А/г за формулою А/г * Вольти * 0.65 буде мати:

[simterm]

100*12*0.65
780.00

[/simterm]

780 ват-годин “корисного” заряду:

Якщо взяти акумулятор на 100 А/г який живить котел опалення зі споживаною потужністю у 130 ват, то він має працювати близько 6 годин.

Потужність споживання енергії домашніми пристроями

Щоб розуміти скільки ж треба мати запасу в акумуляторах – треба мати уяву скільки енергії будуть витрачати прилади вдома.

Заміряти можна ватметром, наприклад мій холодильник:

В мене є:

  • газовий котел (опалення, гаряча вода): 130 Вт/год
  • холодильник: 90 Вт/год
  • ПК – 120-220 Вт/год (хоча навряд чи я його буду включати при блекаутах)
  • монітор: 35 Вт/год
  • ноутбук: 15-20 Вт/год
  • роутер: 10-12 Вт/год
  • медіаконвертор для оптики (GPON): близько 8 Вт/год
  • настільна лампа: 8 Вт/год

Тобто все разом без ПК близько 300 ват в годину.

На добу це буде 7200, на тиждень – 50400 Вт/год, якщо все буде працювати цілодобово.

Що маю з електрики з минулої зими

Тоді купувалось “аби дістати”, бо кинувся робити досить піздно – коли відключення вже почалися, тому вийшла “збірна солянка” всього підряд:

  • 2 зарядні станції Kseon по 168000 mAh
    • 620 Вт/год х2
  • павербанк 2Project на 60.000 mAh, вихід 65 ват
    • 228 Вт/год
  • 2 павербанки Baseus по 30.000 mAh, вихід 65 ват
    • 114 Вт/год х2
  • 2 “зарядні станції” на балконі:
    • 1 інвертор PowerWare 5115, вихід 600 Ват
      • до нього підключені 2 автомобільних акумулятори InterTab на 60 А/г
      • 468 Вт/год х2
    • 1 інвертор CyberPower CPS1000E, вихід 700 Ват
      • до нього підключено 1 автомобільний акумулятор Exide AGM на 72 А/г
      • 561 Вт/год
  • кілька дрібних павербанків для світильників/медіаконвертора/роутера тощо

Загалом запас виходить 3193 Вт/год, тобто якщо будуть включені всі прилади, то запасу лише на 10 з половиною годин.

Але котел, холодильник та монітор (130+90+35 Вт/год == 225 Вт/год)  можуть живитись тільки від зарядних на балконі, де інвертори на 600-700 Ват з розетками schuko і в яких сумарно є 1497 Вт/год, тобто їх запасу вистачить лише на 6.5 годин. Але можна докупити інвертор для станцій Kseon ват на 500 і живити з цих станцій – в мене зараз для них китайський інвертор на 200 ват, на який підключати котел стрьомно, бо цей інвертор скоріш за все згорить. Та й взагалі через такі інвертори живити прилади – це зайві витрати, краще зберегти для ноутбука і інших приладів, які можна через заживити через USB.

Якщо відключити холодильник (минулої зими так і робив – зберігав на балконі, де було 10-14 градусів, а заморозку вивішував в пакеті за вікно), то газовому котлу для опалення акумуляторів на балконі вистачить на 11.5 годин.

Ноутбук (15-20 Вт/год), роутер (10-12 Вт/год), медіаконвертор (8 Вт/год) можуть живитись від станцій Kseon та павербанок – разом в них 1696 Вт/год, тобто 42.4 години, або ~3 днів, якщо користуватись 14 годин на день, бо вночі  все ж краще спати, або на 5 днів, якщо користуватись виключно для роботи 8 годин на день.

Швидкість розряду

Математика математикою, але частину батарей перевірив “на живу”, щоб більш-менш точно знати чого на скільки вистачить:

  • інвертор CyberPower + 1х акумулятор Exide AGM на 72 А/г
    • 72.000 мА/г на 12 вольтах * 0.65 це 561 Вт/год
    • має вистачити на ~4.5 годин роботи
    • ПК пропрацював з 8.30 – 12.14, тобто 3 години 45 хвилин. Потім інвертор почав пищати, бо напруга впала до 11.4 вольти, хоча на екрані інвертор показував ще половину заряду батареї. В принципі, десь так і виходить – 4.5 години, якщо садити “в нуль”
  • інвертор Powerware + 2х акумулятори на 60 А/г
    • 120.000 мА/г на 12 вольтах * 0.65 це 936 Вт/год
    • по розрахунках котлу має вистачити на 6-7 годин, не заміряв (минулої зими пам’ятаю, що було менше – годин на 4-6, але це ще залежить від температури на вулиці – як швидко охолоджуються квартира, і як часто котел буде включатись для підігріву)
  • 2х банки Baseus:
    • 30.000 мА/г на 3.8 вольта 114 Вт/год
    • обох банок ноутбуку має вистачити на 12 годин – по 6 годин з кожної банки, хоча насправді її вистачило десть на 4.5 години
  • банка 2Project:
    • 60.000 мА/г на 3.8 вольтах 228 Вт/г
    • ноутбуку має вистачити на 11 годин (знову ж таки – реально буде годин мабуть 7-8)

Швидкість зарядки

В принципі, це все працює, але головна проблема, яка проявилась тієї зими – це швидкість зарядки, і це треба враховувати наступної зими.

Формула для розрахунку та ж сама: беремо вольтаж зарядного пристрою, його силу струму, перемножуємо – отримаємо Ват/години, котрі він видає, а знаючи ємкість батареї в ват/годинах – можемо легко прикинути швидкість зарядки.

Тобто зарядка на 16.8 вольт з 2 амперами дасть 32 Вт/год, і зарядить Kseni на 620 Вт/год за 19 годин.

Частину батарей заміряв реальною зарядкою, частину прикинув по формулі:

  • зарядні Kseni по документації на дефолтних зарядках на 2А 16,8 В заряджаються 16 годин
    • до них на цю зиму купив додатково зарядні LiitoKala на 5А
    • тож мають заряджатись за ~7 годин
  • для павербанків Baseus і 2Project минулої зими купував зарядні Baseus GaN3 з виходом на 30 ват (20 Вольт і 1.5 ампера – 20В*1.5А=30Вт)
    • банка Baseus – включив зарядку з нуля о 13.45 – в 15.50 було 50%, тобто за дві години залило 15.000 мА/г (~57 Вт/г) на 30 ватах – якщо рахувати повну ємкість як 114 Вт/г, то так і виходить – 3.8 години на 30 ватах
    • банка 2Project – пишуть, що “30 годин через вхід 10 Вт“, тож має бути близько 10 годин на 30 ватах
  • акумулятори на балконі – тут важко рахувати, бо ніде в документації не знайшов скільки реально йде на акумулятори під час зарядкки, тож просто розрядив і запустив зарядку:
    • 1х72 А/г через CyberPower заряджається:
      • включив у 12.15, к 16.00 напруга піднялась з 11.4 (коли інвертор почав пищати, що пора виключати, бо низький заряд батареї) до 12.5, а максимальна 13.8, тобто зарядився десь наполовину за 4 години, значить повний заряд 72 А/г буде близько 8 годин
    • 2х60 А/г через PowerWare – не заміряв, але пам’ятаю, що десь також близько 10 годин

Що треба докупити

Саме складне питання.

Можна, звісно, взяти нормальну зарядну станцію типу EcoFlow Delta 2 Max – 2400W, 2048Wh за 83.000 гривень – це на сьогоднішній день, середина серпня.  Цього вистачить для котла 130 Вт + холодильника 90 Вт + монітора 30 Вт на 8 годин роботи, а заряджається Delta 2 max за 2 години – те, що треба, якщо світло знов буде кілька годин на добу.

В принципі – дуже достойне рішення, але ж і ціна… Достойна.

Інший варіант – купити два гелевих акумулятори на 100 А/г кожний – зараз це буде в районі 16.000 грн за дві штуки, і докупити другий інвертор CyberPower – ще 18.000 грн. Тоді можна підключити кожен з CyberPower до 1 акуму, і вони будуть їх заряджати за ~10 годин. Разом це вийде 34.000 грн – дешевше, ніш Delta 2 Max, але при тих же 2400 Вт/год запасу енергії і заряджається набагато довше.

Крім того, мабуть, докуплю:

  • ще пару банок Baseus на 30.000 – показали себе непогано, ноутбук від них працює, вартість зараз 3900 грн (в листопаді чи грудні минулої зими брав по чи то 6000, чи 7000 грн)
  • докупити зарядні Baseus GaN3 на 30 ват для кожної банки, щоб можна було заряджати одночасно
  • Type-C шнурки, бо маю тільки два

Ще все ж може таки куплю на балкон датчик диму, бо там акумулятори та інвертори – буду спокійніше спати. Вогнегасник брав ще минулої зими, 2 штуки ВП-3.

Також думаю на цю зиму вже ж взяти газовий обігрівач та газову плитку на балончиках – на випадок, якщо газ все ж будуть відключати.

Так наче й все.

Loading

Grafana Loki: оптимізація роботи – Recording Rules, кешування та паралельні запити
0 (0)

12 Серпня 2023

Отже, маємо Loki, встановленую з чарту у simple-scale mode, див. Grafana Loki: архітектура та запуск в Kubernetes з AWS S3 storage та boltdb-shipper.

Працює Loki все в AWS Elastic Kubernetes Service, встановлено з Loki Helm chart, в ролі long-term store використовуємо AWS S3, а для роботи з індексами Loki – BoltDB Shipper.

У Loki в 2.8 для роботи з індексами з’явився механізм TSDB, який мабуть скоро замінить BoltDB Shipper, але я його ще пробував. Див. Loki’s new TSDB Index.

І загалом все працює, все наче добре, але при отримані даних за тиждень або місяць в Grafana дуже часто отримуємо помилки 502/504 або “too many outstanding requests“.

Тож сьогодні трохи поглянемо на те, як можна оптимізувати Loki для кращого перфомансу.

Насправді, витратив дуже багато часу на те, щоб більш-менш розібратись з усім, що буде в цьому пості, бо документація Loki… Вона є. Її багато. Але зрозуміти з цієї документації якісь деталі реалізації, або як різні компоненти один з одним працюють місцями досить складно.

Хоча я не погоджусь з осноною тезою з I can’t recommend serious use of an all-in-one local Grafana Loki setup, проте згоден, що:

In general I’ve found the Loki documentation to be frustratingly brief in important areas such as what all of the configuration file settings mean and what the implications of setting them to various values are.

Тим не менш, якщо все ж витратити трохи часу на “причісування”, то загалом система працює дуже добре (принаймні, поки ми не маємо террабайтів логів в день, але зустрічав обговорення, де люди мають такі навантаження).

Отже, що ми можемо зробити, щоб пришвидшити процесс роботи для обробки запросів в Grafana dashboards та/або алертів з логів:

  • оптимізація запросів
  • використати Record Rules
  • включеня кешування запитів, індексів та chunks
  • оптимізувати роботу Queries

Поїхали.

Loki Pods && Components

Перед тим, як братись за оптимізацію давайте згадаємо що там в Loki взагалі є і як воно все разом працює.

Отже, маємо такі компоненти:

Тобто коли ми деплоїмо Loki Helm chart у simple-scale mode та без legacyReadTarget – то маємо поди Read, Write, Backend та Gateway:

  • Read:
    • querier: обробляє запити на отримання даних – спочатку намагається взяти дані з пам’яті Ingester, якщо там їх нема – то йде до long-term store
    • query frontend: опціональний сервіс для покращення швидкості роботи Querier: запити на отримання даних спочатку йдуть на Query Frontend, який розбиває велики запити на менші і виконує  формує чергу запитів, а Querier з цієї чегри бере запити на обробку. Крім того, Query Frontend може виконувати кешування відповідей, і части запитів обробляти зі свого кешу замість того, щоб виконувати цей запит на воркері, тобто на Querier
    • query scheduler: опціональний сервіс для покращеня скейлінгу Querier та Query Frontend, який бере на себе формування черги запитів, та передає їх до декількох Query Frontend
    • ingester: у Read-path відповідає на запити від Querier даними, які має в пам’яті (ті, що ще не було відправлені до long-term store)
  • Write:
    • distributor: приймає вхідні логи від клієнтів (Promtail, Fluent Bit, etc), перевіряє їх та відправляє до Ingester
    • ingester (again): приймає дані від Distributor, і формує chunks (блоки даних або фрагменти), які відправляє до long-term store
  • Backend:
    • ruler: перевіряє дані в логах по expressions, заданим в рулах, та створює алерти або метрики в Prometheus/VictoriaMetrics
    • compactor: відповідає за компресію індекс-файлів і retention даних у long-term storage
  • Gateway: звичайний Nginx, який відповідає за роутінг запитів до відповідних сервісів Loki

Table Manager, BoltDB Shipper та індекси

Окремо варто згадати про створення індексів.

По-перше – Table manager, бо особисто мені з його документацій було не дуже зрозуміло використовується він зараз, чи ні. Бо з одного боку в values.yaml він має enabled=false, з іншого – в логах Write-інстансів він подекуди з’являється.

Отже, що маємо про індекси:

  • Table Manager вже depreacted, і використовується тільки у випадку, якщо індекси зберігається у зовнішніх сховищах – DynamoDB, Cassandra, etc
  • файли індексів створються Ingester в каталозі active_index_directory (по-дефолту /var/loki/index), коли chunks з пам’яті готові до відправки до long-term storage – див. Ingesters
  • механізм boltdb-shipper відповідає за відправку індексів з інстансів Ingester до long-term store (S3)

Loki queries optimization

Переглянув Best practices, і спробував рекомендації на практиці, але насправді не помітив різниці.

Проте все ж додам сюди кратко, бо в принципі вони виглядають досить логічно.

Перевіряв за допомогою запросів типу:

[simterm]

$ time logcli query '{app="loki"} |="promtail" | logfmt' --since=168h

[/simterm]

І час виконання все одно був дуже різний навіть при виконанні одного й того ж запросу, незалежно від спроб оптимізації запросу за рахунок використання селекторів чи фільтрів.

Label or log stream selectors

На відміну від ELK, Loki не індексує весь текст в логах, а тільки timestamp та labels:

Тож запит у вигляді {app=~".*"}  буде виконуватись довше, ніж при використанні точного stream selector, тобто {app="loki"}.

Чим більш точний stream selector буде використано – тим менше даних Loki буде вигружати даних з long-term store та обробляти для відповіді – запит {app="loki", environment="prod"} буде швидшим, ніж просто вибрати всі стріми з {app="loki"}.

Line Filters та regex

Використовуйте Line filters, та уникайте регулярок в запитах.

Тобто запит {app="loki"} |= "promtail" буде швидшим, ніж просто {app="loki"}, і швидшим, аніж {app="loki"} |~ "prom.+".

LogQL Parsers

Парсери по швидкості роботи:

  1. pattern
  2. logfmt
  3. JSON
  4. regex

І не забувайте про Log Filter: запит {app="loki"} |= "promtail" | logftm буде швидшим, ніж {app="loki"} | logfmt.

А тепер перейдемо до параметрів Loki, які дозволять пришвидшити обробку запитів та зменшать використання CPU/Memory його компонентами.

Ruler та Recording Rules

А ось Recording Rules дали прям дуже відчутний буст.

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

Він чудово підходить для будь яких запитів, бо ми можемо створити Recording Rule, а результати відправляти Prometheus/VicrtoriaMetrics через remote_write, після чого виконувати запити на алерти або в дашбордах Grafana прямо з Prometheus/VicrtoriaMetrics замість того, щоб кожного разу виконувати їх в Loki, і працює це набагато швидше, ніж описувати запит в самій Grafana або алерт-рул у файлі конфігу Ruler.

Отже, щоб зберігати результати в Prometheus/VicrtoriaMetrics – в параметрах Ruler додаємо WAL-директорію, куди Ruler буде записувати результати запитів, та налаштовуємо remote_write, куди він буде зберігати результати запитів:

...
    rulerConfig:
      storage:
        type: local
        local:
          directory: /var/loki/rules
      alertmanager_url: http://vmalertmanager-vm-k8s-stack.dev-monitoring-ns.svc:9093
      enable_api: true
      ring:
        kvstore:
          store: inmemory
      wal:
        dir: /var/loki/ruler-wal      
      remote_write:
        enabled: true
        client:
          url: http://vmsingle-vm-k8s-stack.dev-monitoring-ns.svc:8429/api/v1/write
...

І за хвилинку перевіряємо метрику в VicrtoriaMetrics:

В Grafana результат швидкості побудови графіків просто вражаючий.

Якщо запит:

sum(
  rate({__aws_cloudwatch_log_group=~"app-prod-approdapiApiGatewayAccessLogs.*"} | json | path!="/status_monitor" | status!~"200" [5m])
)
by (
  status, __aws_cloudwatch_log_group
)

Записати в Recording Rule як:

- record: overview:backend:apilogs:prod:sum_rate
  expr: |
  sum(
    rate({__aws_cloudwatch_log_group=~"app-prod-appprodapiApiGatewayAccessLogs.*"} | json | path!="/status_monitor" | status!~"200" [5m])
)
by (
  status, __aws_cloudwatch_log_group
)
  labels:
    component: backend

То швидкість його виконання в дашборді 148 ms:

А якщо робити запит напряму з дашборди – то іноді по кілька секунд:

Кешування

Loki може зберігати дані в кеші, щоб потім віддавати дані з пам’яті або диску, а не виконувати запит “з нуля” і не завантажувати файли індексів та блоків даних з S3.

Теж дало досить відчутний результат по швидкості виконання запросів.

Документація – Caching.

Loki має три типи кешу:

  • Index Cache: boltdb-shipper може тримати індекси для Queriers локально, щоб не завантажувати їх кожного разу з S3, див. Queriers
  • Query results cache: збегірати результати запросів в кеші
  • Chunk cache: зберігати дані з S3 в локальному кеші

Що маємо в параметрах, див Grafana Loki configuration parameters:

  • query_range: параметри розділення великих запитів на менші і кешування запитів в Loki query-frontend
    • cache_results: виставляємо в true
    • results_cache: налаштування бекенду кешу
  • boltdb_shipper:
    • cache_location: шлях для збереження індексів BoltDB для використання в запитах
  • storage_config:
    • index_queries_cache_config: параметри кешування індексів
  • chunk_store_config:
    • chunk_cache_config: налаштування бекенду кешу

Query та Chunk cache

Документація – Caching.

У values Helm-чарту вже маємо блок для memcached – можна брати для прикладу.

В ролі бекенду кешу може бути embedded_cache, Memcached, Redis або fifocache (deprecated – зараз це embedded_cache).

Спробуємо з Memcached, бо Redis в Kubernetes колись крутив – більше не хочу 🙂

Додаємо репозиторій:

[simterm]

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

[/simterm]

Вставновлюємо інстанс Memcached для chunks chache:

[simterm]

$ helm -n dev-monitoring-ns upgrade --install chunk-cache bitnami/memcached

[/simterm]

І для результатів запитів:

[simterm]

$ helm -n dev-monitoring-ns upgrade --install results-cache bitnami/memcached

[/simterm]

Знаходимо Services:

[simterm]

$ kk -n dev-monitoring-ns get svc | grep memcache
chunk-cache-memcached                                  ClusterIP   172.20.120.199   <none>        11211/TCP                    116s
results-cache-memcached                                ClusterIP   172.20.53.0      <none>        11211/TCP                    86s

[/simterm]

Оновлюємо values нашого чарту:

...
loki:
  ...
  memcached:
    chunk_cache:
      enabled: true
      host: chunk-cache-memcached.dev-monitoring-ns.svc
      service: memcache
      batch_size: 256
      parallelism: 10
    results_cache:
      enabled: true
      host: results-cache-memcached.dev-monitoring-ns.svc
      service: memcache
      default_validity: 12h
...

Деплоїмо та перевіряємо конфіг в подах Loki:

[simterm]

$ kk -n dev-monitoring-ns exec -ti loki-write-0 -- cat /etc/loki/config/config.yaml
...
chunk_store_config:
  chunk_cache_config:
    memcached:
      batch_size: 256
      parallelism: 10
    memcached_client:
      host: chunk-cache-memcached.dev-monitoring-ns.svc
      service: memcache
...
query_range:
  align_queries_with_step: true
  cache_results: true
  results_cache:
    cache:
      default_validity: 12h
      memcached_client:
        host: results-cache-memcached.dev-monitoring-ns.svc
        service: memcache
        timeout: 500ms
...

[/simterm]

Перевіримо чи пішли дані в Memcached – встановлюємо telnet:

[simterm]

$ sudo pacman -S inetutils

[/simterm]

Відкриваємо порт:

[simterm]

$ kk -n dev-monitoring-ns port-forward svc/chunk-cache-memcached 11211

[/simterm]

Підключаємось:

[simterm]

$ telnet 127.0.0.1 11211

[/simterm]

І перевіряємо ключі:

[simterm]

stats slabs
STAT 10:chunk_size 752
STAT 10:chunks_per_page 1394
STAT 10:total_pages 2
STAT 10:total_chunks 2788
STAT 10:used_chunks 1573
...

[/simterm]

Є, значить дані з Loki йдуть.

Ну і глянемо чи дало кешування якийсь результат.

До включення кешування – 600 мілісекунд:

Після включення кешування – 144 мілісекунди:

Index cache

Додаємо ще один інстанс Memcached:

[simterm]

$ helm -n dev-monitoring-ns upgrade --install index-cache bitnami/memcached

[/simterm]

Оновлюємо values (от не знаю, чому є query_range та chunk_cache, але не зробили якийсь index_cache):

...
    storage_config:
      ...
      boltdb_shipper:
        active_index_directory: /var/loki/botldb-index
        shared_store: s3
        cache_location: /var/loki/boltdb-cache
      index_queries_cache_config:
        memcached:
            batch_size: 256
            parallelism: 10
        memcached_client:
            host: index-cache-memcached.dev-monitoring-ns.svc
            service: memcache
...

Деплоїмо, перевіряємо конфіг в поді ще раз:

[simterm]

$ kk -n dev-monitoring-ns exec -ti loki-read-5c4cf65b5b-gl64t cat /etc/loki/config/config.yaml
...
storage_config:
  ...
  index_queries_cache_config:
    memcached:
      batch_size: 256
      parallelism: 10
    memcached_client:
      host: index-cache-memcached.dev-monitoring-ns.svc
      service: memcache
...

[/simterm]

І результат виконання того ж самого запиту тепер 66 ms:

Query Frontend та паралельні запити

Документація – Query Frontend.

Query Frontend працює як load balancer для Queriers, і розбиває запроси за великий проміжок часу на частини, після чого віддає їх Queriers для виконання паралельно, а після виконання запросу збирає результати обратно в одну відповідь.

Для цього в limits_config задається split_queries_by_interval з дефолтом в 30 хвилин.

Параметри паралелізму задаються через querier max_concurrent – кількість одночасних потоків для виконання запитів. Пишуть, що можна ставити х2 від ядер CPU.

Крім того в limits_config задається ліміт на загальну кількість одночасних виконань через max_query_parallelism, яке має бути кількість Queriers (read-поди) помножена на max_concurrent. Хоча поки не знаю, як це настраювати якщо для read-подів включати автоскейлінг.

У нас моніторинг працює на t3.medium з 4 vCPU, тож поставимо max_concurrent == 8:

...
    querier:
      max_concurrent: 8

    limits_config:
      max_query_parallelism: 8
      split_queries_by_interval: 15m
...

Логування slow queries

Для дебагу повільних запросів можна налаштувати параметр log_queries_longer_than (default 0 – відключено):

...
    frontend:
      log_queries_longer_than: 1s
...

Тоді в логах Readers будуть такі запроси з param_query:

Результати оптимізації

В цілому це все, що знайшов по оптимізації, і результат дійсно приємний.

Якщо тестовий запит до всіх описаних вище налаштувань виконувався 5.35 секунди:

То після – 98 мілісекунд:

Timeouts та 502/504

Окремо варто сказати про таймаути на виконання запитів.

Хоча після всіх налаштувань по перформансу я 502/504 не бачу, але якщо вони виникають то можна спробувати підвищити ліміти таймаутів:

...
    server:
      # Read timeout for HTTP server
      http_server_read_timeout: 3m
      # Write timeout for HTTP server
      http_server_write_timeout: 3m    
...

Посилання по темі

Loading

AWS: Grafana Loki, InterZone трафік в AWS, та Kubernetes nodeAffinity
0 (0)

9 Серпня 2023

Трафік в AWS взагалі досить цікава та місцями складна штука, колись писав окремо про це у пості AWS: Cost optimization – обзор расходов на сервисы и стоимость трафика в AWS – прийшов час трохи повернутися до цієї теми.

Отже, в чьому проблема: в AWS Cost Explorer помітив, що кілька днів поспіль маємо зростання витрат на EC2-Other:

А що у нас входить в EC2-Other? Всякі Load Balancers, IP, EBS та трафік, див. Tips and Tricks for Exploring your Data in AWS Cost Explorer.

Щоб перевірити, на шо саме виросли трати – переключаємо Dimension на Usage Type та у Service вибираємо EC2-Other:

Бачимо, що виросли кости на DataTransfer-Regional-Bytes, які є “This is Amazon EC2 traffic that moves between AZs but stays within the same region.” – див. Understand AWS Data transfer details in depth from cost and usage report using Athena query and QuickSight та Understanding data transfer charges.

Можемо переключити на API Operation, і побачити який саме трафік почав використовуватись:

InterZone-In та InterZone-Out.

Як раз на минулому тижні запустив моніторинг з VictoriaMetrics Kubernetes Stack з Grafana Loki і налаштовував збір логів з CloudWatch Logs та додавав алерти з Loki Ruler – мабуть воно і вплинуло на трафік. Давайте розбиратись.

VPC Flow Logs

Що нам треба, це додати Flow Logs для VPC нашого Kubernetes кластеру – тоді побачимо, які саме Kubernetes-поди або Lambda-фунції в AWS почали активно “їсти” трафік. Детальніше є в пості AWS: VPC Flow Logs – знайомство та аналітика з CloudWatch Logs Insights.

Створюємо CloudWatch Log Group з кастомними полями щоб мати pkt_srcaddr та pkt_dstaddr, які містять у собі IP Kubernetes подів, див. Using VPC Flow Logs to capture and query EKS network communications.

В Log Group налаштовуємо наступні поля:

region vpc-id az-id subnet-id instance-id interface-id flow-direction srcaddr dstaddr srcport dstport pkt-srcaddr pkt-dstaddr pkt-src-aws-service pkt-dst-aws-service traffic-path packets bytes action

Далі, налаштовуємо Flow Logs для VPC нашого кластеру:

І йдемо дивитсь на логи.

CloudWatch Logs Insigts

Беремо запит із прикладів:

І переписуємо його під свій формат – взяв з того ж поста у Примеры Logs Insights:

parse @message "* * * * * * * * * * * * * * * * * * *" 
| as region, vpc_id, az_id, subnet_id, instance_id, interface_id, 
| flow_direction, srcaddr, dstaddr, srcport, dstport, 
| pkt_srcaddr, pkt_dstaddr, pkt_src_aws_service, pkt_dst_aws_service, 
| traffic_path, packets, bytes, action 
| stats sum(bytes) as bytesTransferred by pkt_srcaddr, pkt_dstaddr
| sort bytesTransferred desc
| limit 10

Та отримуємо цікаву картину:

Де в топі з великим відривом бачимо дві адреси – 10.0.3.111 та 10.0.2.135, які нагнали аж 28995460061 байт трафіку.

Loki components та трафік

Перевіряємо, що ж це за поди в Kubernetes, і заодно знаходимо відповідні WorkerNodes/EC2.

Спершу 10.0.3.111:

[simterm]

$ kk -n dev-monitoring-ns get pod -o wide | grep 10.0.3.111
loki-backend-0                                                    1/1     Running   0          22h   10.0.3.111   ip-10-0-3-53.ec2.internal    <none>           <none>

[/simterm]

Та 10.0.2.135:

[simterm]

$ kk -n dev-monitoring-ns get pod -o wide | grep 10.0.2.135
loki-read-748fdb976d-grflm                                        1/1     Running   0          22h   10.0.2.135   ip-10-0-2-173.ec2.internal   <none>           <none>

[/simterm]

І вже тут я згадав, що саме 31-го липня включив алерти в Loki, які обробляються як раз в поді backend, де крутиться компонент Ruler (раніше він був у поді read).

Тобто левова частина трфіку відбувається саме між Read та Backend подами.

Окреме питання що саме там в такій кількості передається, але поки треба вирішити проблему с витратами на трафік.

Перевіримо в яких AvailabilityZones знаходяться Kubernetes WorkerNodes.

Інстанс ip-10-0-3-53.ec2.internal, де крутиться под з Backend:

[simterm]

$ kk get node ip-10-0-3-53.ec2.internal -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]'
us-east-1b

[/simterm]

Та ip-10-0-2-173.ec2.internal, де знаходиться под з Read:

[simterm]

$ kk get node ip-10-0-2-173.ec2.internal -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]'
us-east-1a

[/simterm]

Ось і маємо cross-AvailabilityZones трафік.

Kubernetes podAffinity та nodeAffinity

Що можемо спробувати – це додати Affinity для подів, щоб вони запускались в одній AvailabilityZone. Див. Assigning Pods to Nodes та Kubernetes Multi-AZ deployments Using Pod Anti-Affinity.

Для подів у Helm-чарті вже маємо affinity:

...
  affinity: |
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchLabels:
              {{- include "loki.readSelectorLabels" . | nindent 10 }}
          topologyKey: kubernetes.io/hostname
...

Перший варіант – це вказати Kubernetes Scheduler, що ми хочемо поді Read розташовувати на тій самій WorkerNode, де є поди з Backend. Для цього можемо використати podAffinity.

Перевірямо лейбли Backend:

[simterm]

$ kk -n dev-monitoring-ns get pod loki-backend-0 --show-labels
NAME             READY   STATUS    RESTARTS   AGE   LABELS
loki-backend-0   1/1     Running   0          23h   app.kubernetes.io/component=backend,app.kubernetes.io/instance=atlas-victoriametrics,app.kubernetes.io/name=loki,app.kubernetes.io/part-of=memberlist,controller-revision-hash=loki-backend-8554f5f9f4,statefulset.kubernetes.io/pod-name=loki-backend-0

[/simterm]

Тож для Reader можемо задати podAntiAffinity з labelSelector=app.kubernetes.io/component=backend – тоді Reader буде “тягнутись” до тії ж AvailabilityZone, де запущено Backend.

Інший варіант – через nodeAffinity, і в Expressions для обох Read та Backend вказати лейблу з бажаною AvailabilityZone.

Спробуємо з preferredDuringSchedulingIgnoredDuringExecution, тобто “soft limit”:

...
  read:
    replicas: 2
    affinity: |
      nodeAffinity:
        preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 1
          preference:
            matchExpressions:
            - key: topology.kubernetes.io/zone
              operator: In
              values:
              - us-east-1a
  ...
  backend:
    replicas: 1
    affinity: |
      nodeAffinity:
        preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 1
          preference:
            matchExpressions:
            - key: topology.kubernetes.io/zone
              operator: In
              values:
              - us-east-1a 
...

Деплоїмо, перевіряємо Read-поди:

[simterm]

$ kk -n dev-monitoring-ns get pod -l app.kubernetes.io/component=read -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP           NODE                         NOMINATED NODE   READINESS GATES
loki-read-d699d885c-cztj7   1/1     Running   0          50s   10.0.2.181   ip-10-0-2-220.ec2.internal   <none>           <none>
loki-read-d699d885c-h9hpq   0/1     Running   0          20s   10.0.2.212   ip-10-0-2-173.ec2.internal   <none>           <none>

[/simterm]

Та зони інстансів:

[simterm]

$ kk get node ip-10-0-2-220.ec2.internal -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]'
us-east-1a

$ kk get node ip-10-0-2-173.ec2.internal -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]'
us-east-1a

[/simterm]

Окей, тут все є, а що там Backend?

[simterm]

$ kk get nod-n dev-monitoring-ns get pod -l app.kubernetes.io/component=backend -o wide
NAME             READY   STATUS    RESTARTS   AGE   IP           NODE                        NOMINATED NODE   READINESS GATES
loki-backend-0   1/1     Running   0          75s   10.0.3.254   ip-10-0-3-53.ec2.internal   <none>           <none>

[/simterm]

І його нода:

[simterm]

$ kk -n dev-get node ip-10-0-3-53.ec2.internal -o json | jq -r '.metadata.labels["topology.ebs.csi.aws.com/zone"]'
us-east-1b

[/simterm]

А чому в 1b, коли ми вказали 1a?? Глянемо StatefulSet – чи додались наші affinity:

[simterm]

$ kk -n dev-monitoring-ns get sts loki-backend -o yaml
apiVersion: apps/v1
kind: StatefulSet
...
    spec:
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - preference:
              matchExpressions:
              - key: topology.kubernetes.io/zone
                operator: In
                values:
                - us-east-1a
            weight: 1
...

[/simterm]

Все є.

Добре – давайте використаємо “hard limit”, тобто requiredDuringSchedulingIgnoredDuringExecution:

...
  backend:
    replicas: 1
    affinity: |
      nodeAffinity:
        requiredDuringSchedulingIgnoredDuringExecution:
          nodeSelectorTerms:
          - matchExpressions:
            - key: topology.kubernetes.io/zone
              operator: In
              values:
              - us-east-1a
...

Деплоїмо ще раз, і тепер под з Бекендом застряг у статусі Pending:

[simterm]

$ kk -n dev-monitoring-ns get pod -l app.kubernetes.io/component=backend -o wide
NAME             READY   STATUS    RESTARTS   AGE     IP       NODE     NOMINATED NODE   READINESS GATES
loki-backend-0   0/1     Pending   0          3m39s   <none>   <none>   <none>           <none>

[/simterm]

Чому? Дивимось Events:

[simterm]

$ kk -n dev-monitoring-ns describe pod loki-backend-0
...
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  34s   default-scheduler  0/3 nodes are available: 1 node(s) didn't match Pod's node affinity/selector, 2 node(s) had volume node affinity conflict. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling..

[/simterm]

Спешу подумав, що на WokrerkNdoes вже маємо максимум подів – 17 штук на t3.medium.

Перевіримо:

[simterm]

$ kubectl -n dev-monitoring-ns get pods -A -o jsonpath='{range .items[?(@.spec.nodeName)]}{.spec.nodeName}{"\n"}{end}' | sort | uniq -c | sort -rn
     16 ip-10-0-2-220.ec2.internal
     14 ip-10-0-2-173.ec2.internal
     13 ip-10-0-3-53.ec2.internal

[/simterm]

Але ні – місця ще є.

Тоді що – EBS? Часта проблема, коли EBS в одній AvailabilityZone, а Pod запускається в іншій.

Знаходимо Volume Бекенду – там йому підключаються алерт-рули для Ruler:

[simterm]

...
Volumes:
  ...
  data:
    Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
    ClaimName:  data-loki-backend-0
    ReadOnly:   false
...

[/simterm]

Знаходимо відповідний Persistent Volume:

[simterm]

$ kubectl k -n dev-monitoring-ns get pvc data-loki-backend-0
NAME                  STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-loki-backend-0   Bound    pvc-b62bee0b-995e-486b-9f97-f2508f07a591   10Gi       RWO            gp2            15d

[/simterm]

І AvailabilityZone цього EBS:

[simterm]

$ kk -n dev-monitoring-ns get pv pvc-b62bee0b-995e-486b-9f97-f2508f07a591 -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]'
us-east-1b

[/simterm]

Так і є – диск у нас в зоні us-east-1b, а под намагаємось запустити под в зоні us-east-1a.

Що можемо зробити – це або Readers запускати в зоні 1b, або видалити PVC для Backend, і тоді при деплої він створить новий PV та EBS в зоні 1a.

Так як в волюмі ніяк даних нема і для Ruler правила створються з ConfigMap, то простіше просто видалити PVC:

[simterm]

$ kubectl k -n dev-monitoring-ns delete pvc data-loki-backend-0
persistentvolumeclaim "data-loki-backend-0" deleted

[/simterm]

Видаляємо под, щоб він перестворився:

[simterm]

$ kk -n dev-monitoring-ns delete pod loki-backend-0
pod "loki-backend-0" deleted

[/simterm]

Перевіряємо, що PVC створений:

[simterm]

$ kk -n dev-monitoring-ns get pvc data-loki-backend-0
NAME                  STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-loki-backend-0   Bound    pvc-5b690c18-ba63-44fd-9626-8221e1750c98   10Gi       RWO            gp2            14s

[/simterm]

І його локація тепер:

[simterm]

$ kk -n dek -n dev-monitoring-ns get pv pvc-5b690c18-ba63-44fd-9626-8221e1750c98 -o json | jq -r '.metadata.labels["topology.kubernetes.io/zone"]'
us-east-1a

[/simterm]

І сам под теж запустився:

[simterm]

$ kk -n dev-monitoring-ns get pod loki-backend-0
NAME             READY   STATUS    RESTARTS   AGE
loki-backend-0   1/1     Running   0          2m11s

[/simterm]

Результати трафіку

Робив це в п’ятницю, і на понеділок маємо результат:

Все вийшло, як і планувалось – Cross AvailabilityZone трафік тепер майже на нулі.

Loading

Arch Linux: failed to mount on real root
0 (0)

25 Липня 2023

Оновлював вчора Arch Linux, і за 9 років корисування ціюєю системою вперше зіткнувся с помилкою, коли після ребуту система не змогла підключити диск:

ERROR: device UUID not found.
mount: /new_root: can’t find UUID.
ERROR: Failed to mount UUID on real root.
You are now being dropped into an emergency shell.

В принципі проблема ясна – або змінився UUID диска, або “щось пішло не так” з ядром.

Трапилось це через те, що під час апргейду в /tmp закінчилось місце, і mkinitcpio не зміг зібрати нове ядро.

Завантажуємость с USB, і будемо пробувати фіксити.

Першим перевіряємо, чи правильний UUID вказано в fstab.

Перевіряємо розділи:

[simterm]

[root@archiso ~]# lsblk 
NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
loop0         7:0    0 693.5M  1 loop /run/archiso/airootfs
sda           8:0    1  14.3G  0 disk 
├─sda1        8:1    1   798M  0 part 
└─sda2        8:2    1    15M  0 part 
nvme0n1     259:0    0 953.9G  0 disk 
├─nvme0n1p1 259:1    0   512M  0 part 
├─nvme0n1p2 259:2    0   900G  0 part 
└─nvme0n1p3 259:3    0  53.4G  0 part

[/simterm]

/dev/nvme0n1p2 – мій рутовий роздил.

Підключаємо його:

[simterm]

[root@archiso ~]# mount /dev/nvme0n1p2 /mnt/

[/simterm]

Перевіряємо UUID в /etc/fstab старої системи:

[simterm]

[root@archiso ~]# cat /mnt/etc/fstab 
# Static information about the filesystems.
# See fstab(5) for details.

# <file system> <dir> <type> <options> <dump> <pass>
# /dev/nvme0n1p2
UUID=31268b66-5fca-44f6-8e22-acc281026eaf       /               ext4            rw,relatime     0 1

[/simterm]

І перевіряємо реальний UUID розділу:

[simterm]

[root@archiso ~]# blkid  /dev/nvme0n1p2
/dev/nvme0n1p2: UUID="31268b66-5fca-44f6-8e22-acc281026eaf" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="519b201a-02"

[/simterm]

Все вірно.

Окей, запускаємо iwctl, налаштовуємо WiFi.

Знаходимо ім’я інтерфейсу:

[simterm]

[iwd] station list

[/simterm]

Перевіряємо доступні мережі:

[simterm]

[iwd]# station wlan0 get-networks

[/simterm]

Підключаємось – пароль спитає під час підключення:

[simterm]

[iwd]# station wlan0 connect setevoy-linksys-5-0

[/simterm]

Встановлюємо пакети ядра:

[simterm]

[root@archiso ~]# pacstrap /mnt base linux linux-firmware

[/simterm]

Та збираємо initramfs:

[simterm]

[root@archiso ~]# arch-chroot /mnt/
[root@archiso /]# mkinitcpio -p linux

[/simterm]

Ребутаємось – все завелось.

Loading