AWS: VPC Prefix та максимальна кількість подів на Kubernetes WorkerNodes
0 (0)

28 Лютого 2024

Кожна WorkerNode в Kubernetes може мати обмежену кількість подів, і цей ліміт визначається трьома параметрами:

  • CPU: загальна кількість requests.cpu не може бути більше, ніж є CPU на Node
  • Memory: загальна кількість requests.memory не може бути більше, ніж є Memory на Node
  • IP: загальна кількість подів не може бути більшою, ніж є IP-адрес у ноди

І якщо перші два ліміти такі собі “soft” – бо ми можемо просто не задавати requests взагалі – то ліміт по кількості IP-адрес на ноді це вже “hard” ліміт, бо кожному поду, який запускається на ноді, потрібно видати власну адресу з пула Secondary IP його ноди.

А проблема полягає в тому, що ці адреси дуже часто використовуються ще до того, як на ноді закінчаться CPU або Memory – і в такому випадку ми опиняємось в ситуації, коли наша нода underutilized, тобто – ми могли б ще запустити на ній поди, але не можемо, бо для них нема вільних IP.

Наприклад, одна з наших нод t3.medium виглядає так:

В неї є вільні CPU, не вся пам’ять requested, але Pods Allocation вже 100% – бо до ноди t3.medium може бути додано 17 Secondary IP для подів, і вони всі вже зайняті.

Максимум Secondary IP на AWS EC2

Кількість же додаткових (secondary) IP на ЕС2 залежить від кількості ENI (Elastic network Interface) та кількості IP на кожен інтерфейс, і ці параметри залежать від типу EC2.

Наприклад, t3.medium може мати 3 інтерфейси, і на кожному може бути до 6 IP (див. IP addresses per network interface per instance type):

Тобто всього 18 адрес, але мінус по 1 Private IP роботу ENI самого інстансу – і для подів на такій ноді буде доступно 17 адрес.

Amazon VPC Prefix Assignment Mode

Щоб вирішити проблему з кількістью Secondary IP на EC2 можна використати VPC Prefix Assignment Mode – коли на інтерфейс підключається не окремий IP, а цілий блок /28, див. Assign prefixes to Amazon EC2 network interfaces.

Наприклад, ми можемо створити новий ENI і йому присвоїти CIDR (Classless Inter-Domain Routing) префікс:

$ aws --profile work ec2 create-network-interface --subnet-id subnet-01de26778bea10395 --ipv4-prefix-count 1

Перевіряємо цей ENI:

Єдиний момент, який варто мати на увазі це те, що VPC Prefix Assignment Mode доступний тільки для інстансів на AWS Nitro System – останнє покоління гіпервізорів AWS, на якому працюють інстанси T3, M5, C5, R5 і т.д. – див. Instances built on the Nitro System.

What is: CIDR /28

Кожна IPv4-адреса складається з 32 бітів, поділених на 4 октети (групи по 8 біт). Ці біти можуть бути представлені у двійковій системі (0 або 1) або у десятковій формі (значення між 0 та 255 для кожного октету). Ми будемо оперувати саме 0 та 1.

Маска підмережі /28 вказує на те, що перші 28 бітів IP-адреси зарезервовані для ідентифікації мережі – тоді 4 біти (32 всього мінус 28 зарезервованих) залишаються для визначення індивідуальних хостів в мережі:

Знаючи, що у нас є 4 вільні біти, а кожен біт може мати значення 0 або 1, ми можемо порахувати загальну кількість комбінацій: 2 в ступені 4, або 2×2×2×2=16 – тобто в мережі /28 може бути загалом 16 адрес, включаючи як адресу мережі (перший IP), так і broadcast адресу (останній IP), отже саме для хостів буде доступно 14 адрес.

Тож замість того, щоб на ENI підключати один Secondary IP – ми підключаємо відразу 16.

При цьому варто враховувати скільки ваша VPC Subnet зможе мати таких блоків, бо це буде визначати кільікість WorkerNodes, які ви зможете запустити.

Тут вже простіше використати утіліти накшалт ipcalc.

Наприклад, в мене Private Subnets мають префікс /20, і якщо всю цю мережу розбити на блоки по /28, то будемо мати 256 підмереж і 3584 адрес:

$ ipcalc 10.0.16.0/20 /28
...
Subnets:   256
Hosts:     3584

Або можна використати калькулятор онлайн – ipcalc.

VPC Prefix та AWS EKS VPC CNI

Добре – ми побачили, як ми можемо виділити блок адрес на інтерфейс, який підключений до EC2.

Що далі? Як з цього пулу адрес видається окрема адреса поду, який ми запускаємо в Kubernetes?

Цим займається VPC Container Networking Interface (CNI) Plugin, який складається з двох основних компонентів:

  • L-IPAM daemon (IPAMD): відповідає за створення та підключення ENI до EC2-інстансів, призначення блоків адрес до цих інтерфейсів та “прогрів” IP-префіксів для пришвидшення запуску подів (поговоримо далі)
  • CNI plugin: відповідає за налаштування мережевих інтерфейсів на ноді – як ethernet, та і віртуальних, і комунікує з IPAMD через RPC (Remote Procedure Call)

Як саме це реалізовано чудово описано в пості Amazon VPC CNI plugin increases pods per node limits > How it works:

Тож процес виглядає таким чином:

  1. Kubernetes Pod при запуску виконує запит до IPAMD на виділення IP
  2. IPAMD перевіряє доступні адреси, якщо вільні адреси є – виділяє один для поду
  3. якщо вільних адрес в підключених до ЕС2 префіксах нема – то IPAMD робить запит на підключення до ENI нового префіксу
    1. якщо до існуючого ENI вже не можна додавати нові префікси – робиться запит на підключення нового ENI
      1. якщо нода вже має максимальну кількість ENI – то запит поду на новий IP фейлиться
    2. якщо новий префікс додано (на існуючий або новий ENI) – то з нього вибирається IP для поду

WARM_PREFIX_TARGET, WARM_IP_TARGET та MINIMUM_IP_TARGET

Див. WARM_PREFIX_TARGET, WARM_IP_TARGET and MINIMUM_IP_TARGET.

Для конфігурації виділення префіксів нодам та IP подам VPC CNI має три додаткові опції – WARM_PREFIX_TARGET, WARM_IP_TARGET та MINIMUM_IP_TARGET:

  • WARM_PREFIX_TARGET: скільки підключених /28 префіксів тримати “в запасі”, тобто вони будуть підключені до ENI, але адреси з них ще не використовуються
  • WARM_IP_TARGET: скільки мінімально IP адрес підключати при створенні ноди
  • MINIMUM_IP_TARGET: скільки мінімально IP адрес тримати “в запасі”

При використанні VPC Prefix Assignment Mode ви не можете задати всі три параметри в нуль – як мінімум або WARM_PREFIX_TARGET або WARM_IP_TARGET мають бути задані хоча б в 1.

Якщо заданий WARM_IP_TARGET та/або MINIMUM_IP_TARGET – вони будуть мати перевагу над WARM_PREFIX_TARGET, тобто значення з WARM_PREFIX_TARGET буде ігноруватись.

Subnet CIDR reservations

Документація – Subnet CIDR reservations.

При використанні Prefix IP, адреси в префіксу мають бути суміжні, тобто в одному префіксу не можуть бути адреси “10.0.31.162” (блок 10.0.31.160/28) та “10.0.31.178” (блок 10.0.31.176/28).

Якщо сабнет активно використовується, і в ньому немає безперервного блоку адрес для виділння цілого префіксу, то ви отримаєте помилку:

failed to allocate a private IP/Prefix address: InsufficientCidrBlocks: The specified subnet does not have enough free cidr blocks to satisfy the request

Щоб запобігти цьому, можна використати функцію резервації блоків – VPC Subnet CIDR reservations для створення єдиного блоку, з якого потім будуть “нарізатись” блоки по /28. Такий блок не буде використовуватись для виділення Private IP для EC2, натомість VPC CNI буде створювати префікси саме з цієї “резервації”.

При цьому ви можете створити таку резервацію навіть якщо окремі IP в цьому блоку вже використовуються на EC2 – як тільки такі адреси звільняться, вони більше не будуть виділятись окремим інстансам EC2, а будуть зберігатись для формування префіксів /28.

Отже, якщо в мене є VPC Subnet з блоком /20 – я можу розбити її на два CIDR Reservation блоки по /21, і в кожному /21 блоці мати:

$ ipcalc 10.0.24.0/21 /28
...
Subnets:   128
Hosts:     1792

128 блоків /28 по 14 IP для хостів – разом 1792 IP для подів.

Активація VPC CNI Prefix Assignment Mode в AWS EKS

Документація – Increase the amount of available IP addresses for your Amazon EC2 nodes.

Все, що треба зробити – це змінити значення змінної ENABLE_PREFIX_DELEGATION в aws-node DaemonSet:

$ kubectl set env daemonset aws-node -n kube-system ENABLE_PREFIX_DELEGATION=true

При використанні Terraform модулю terraform-aws-modules/eks/aws це можна зробити через configuration_values для vpc-cni AddOn:

...
module "eks" {
  ...
  cluster_addons = {
    coredns = {
      most_recent = true
    }
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent    = true
      before_compute = true
      configuration_values = jsonencode({
        env = {
          ENABLE_PREFIX_DELEGATION = "true"
          WARM_PREFIX_TARGET       = "1"
        }
      })
    }
  }
...

Див. examples.

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

$ kubectl describe daemonset aws-node -n kube-system
...
    Environment:
      ...
      VPC_ID:                                 vpc-0fbaffe234c0d81ea
      WARM_ENI_TARGET:                        1
      WARM_PREFIX_TARGET:                     1
...

При використанні AWS Managed NodeGroups новий ліміт буде заданий автоматично.

В цілому, максимальна кількість подів буде залежати від типу інстансу і кількості vCPU на ньому – 110 подів на кожні 10 ядер (див. Kubernetes scalability thresholds). Але є ще ї ліміти, які задані самим AWS.

Наприклад для t3.nano з 2 vCPU це буде 34 поди – перевіримо скриптом max-pod-calculator.sh:

$ ./max-pods-calculator.sh --instance-type t3.nano --cni-version 1.9.0 --cni-prefix-delegation-enabled
34

На c5.4xlarge з 16 vCPU – 110 подів:

$ ./max-pods-calculator.sh --instance-type c5.4xlarge --cni-version 1.9.0 --cni-prefix-delegation-enabled 
110

А на c5.24xlarge з 96 ядрами – 250 подів, бо це вже обмеження від AWS:

$ ./max-pods-calculator.sh --instance-type c5.24xlarge --cni-version 1.9.0 --cni-prefix-delegation-enabled
250

Налаштування Karpenter

Для того, щоб задати максимальну кількість подів на WorkerNodes, які створює Karpenter – використовуємо опцію maxPods для NodePool:

...
        - key: karpenter.sh/capacity-type
          operator: In 
          values: ["spot", "on-demand"]
      kubelet:
        maxPods: 110
...

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

$ kk get node
NAME                          STATUS   ROLES    AGE   VERSION
ip-10-0-61-176.ec2.internal   Ready    <none>   2d    v1.28.5-eks-5e0fdde

Для перевірки створимо Deployment з 3 подами:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80

Деплоїмо, і Karpenter створює один NodeClaim з t3.small:

$ kk get nodeclaim
NAME            TYPE       ZONE         NODE   READY   AGE
default-w7g9l   t3.small   us-east-1a          False   3s

Пара хвилин – і поди на ньому запустились:

Тепер скейлимо Deployment до, наприклад, 50 подів:

$ kk scale deployment nginx-deployment --replicas=50
deployment.apps/nginx-deployment scaled

І все ще маємо один NodeClaim з тим же t3.small, але тепер на ньому запущено 50 подів:

Звісно, при такому підході треба завжди задавати Pod requests, щоб кожен под мав доступ до CPU та Memory – саме requests для нас тепер будуть лімітами на кількість подів на нодах.

Loading

Terraform: створення модулю для збору логів AWS ALB в Grafana Loki
0 (0)

20 Лютого 2024

Приклад створення модулю Terraform для автоматизації збору логів з AWS Load Balancers у Grafana Loki.

Як працює сама схема див. у Grafana Loki: збираємо логи AWS LoadBalancer з S3 за допомогою Promtail Lambda – ALB пише логи в S3-бакет, звідки їх забирає Lambda-функція з Promtail і пересилає у Grafana Loki.

В чому ідея з модулем Terraform:

  • є EKS оточення – наразі один кластер, але пізніше може бути декілька
  • є аплікейшени – API у бекенда, моніторинг у девопсів тощо
    • у кожного аплікейшена може бути один або декілька власних оточень – Dev, Staging, Prod
  • для аплікейшенів є AWS ALB, з яких треба збирати логи

Код Terraform для збору логів досить великий – aws_s3_bucket, aws_s3_bucket_public_access_block, aws_s3_bucket_policy, aws_s3_bucket_notification, ще й Lambda-функції.

При цьому ми маємо декілька проектів у різних команд, і у кожного проекту можуть бути декілька оточень – у когось тільки Ops або тільки Dev, у когось – Dev, Staging, Prod.

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

Але головна причина – це трохи mess при створені ресурсів для логування через декілька оточень – одне оточення це сам EKS-кластер, а інше оточення – це самі сервіси на кшталт моніторингу або Backend API

Тобто:

  • в рутовому модулі проекту маємо змінну для EKS-кластеру – $environment зі значенням ops/dev/prod (наразі маємо один кластер і, відповідно, один environment == "ops")
  • в модуль логів з рутового модулю будемо передавати іншу змінну – app_environments зі значеннями dev/staging/prod, плюс імена сервісів, команди тощо – це оточення самих сервісів/applications

Отже, в рутовому модулі, тобто в проекті, будемо викликати новий модуль ALB Logs в циклі для кожного значення з environment, а в середині модуля – в циклі створювати ресурси по кожному з app_environments.

Спочатку зробимо все локально в існуючому проекті, а потім винесемо новий модуль в GitHub-репозиторій, і підключимо його в проекті з репозиторію.

Створення модулю

В репозиторії проекту зараз маємо таку структуру файлів – тестувати будемо в проекті, який створює ресурси для моніторингу, але не принципово, просто тут вже налаштований backend і інші параметри для Terraform:

$ tree .
.
|-- Makefile
|-- acm.tf
|-- backend.hcl
|-- backend.tf
|-- envs
|   |-- ops
|   |   `-- ops-1-28.tfvars
|-- iam.tf
|-- lambda.tf
|-- outputs.tf
|-- providers.tf
|-- s3.tf
|-- variables.tf
`-- versions.tf

Створюємо директорію для модулів і каталог для самого модулю:

$ mkdir -p modules/alb-s3-logs

Створення S3 bucket

Почнемо з простого бакету – описуємо його в файлі modules/alb-s3-logs/s3.tf:

resource "aws_s3_bucket" "alb_s3_logs" {
  bucket = "test-module-alb-logs"
}

Далі, включаємо його в основному модулі, в самому проекті в main.tf:

module "alb_logs_test" {
  source = "./modules/alb-s3-logs"
}

Виконуємо terraform init і перевіряємо з terraform plan:

Добре.

Далі в наш новий модуль треба додати декілька inputs (див. Terraform: модулі, Outputs та Variables) – щоб формувати ім’я корзини, і мати значення для app_environments.

Створюємо файл modules/alb-s3-logs/variables.tf:

variable "eks_env" {
  type        = string
  description = "EKS environment passed from a root module (the 'environment' variable)"
}

variable "eks_version" {
  type        = string
  description = "EKS version passed from a root module"
}

variable "component" {
  type        = string
  description = "A component passed from a root module"
}

variable "application" {
  type        = string
  description = "An application passed from a root module"
}

variable "app_environments" {
  type        = set(string)
  description = "An application's environments"
  default  = [
    "dev",
    "prod"
  ]
}

Далі в модулі оновлюємо ресурс aws_s3_bucket – додаємо for_each (див. Terraform: цикли count, for_each та for) по всім значенням з app_environments:

resource "aws_s3_bucket" "alb_s3_logs" {
  # ops-1-28-backend-api-dev-alb-logs
  # <eks_env>-<eks_version>-<component>-<application>-<app_env>-alb-logs
  for_each = var.app_environments

  bucket = "${var.eks_env}-${var.eks_version}-${var.component}-${var.application}-${each.value}-alb-logs"

  # to drop a bucket, set to `true` first
  # apply
  # then remove the block
  force_destroy = false
}

Або можемо зробити краще – винести формування імен корзин в locals:

locals {
  # ops-1-28-backend-api-dev-alb-logs
  # <eks_env>-<eks_version>-<component>-<application>-<app_env>-alb-logs
  bucket_names = { for env in var.app_environments : env => "${var.eks_env}-${var.eks_version}-${var.component}-${var.application}-${env}-alb-logs" }
}

resource "aws_s3_bucket" "alb_s3_logs" {
  for_each = local.bucket_names

  bucket = each.value

  # to drop a bucket, set to `true` first
  # run `terraform apply`
  # then remove the block
  # and run `terraform apply` again
  force_destroy = false
}

Тут беремо кожний елемент зі списку app_environments, формуємо змінну env, і формуємо map[] з іменем bucket_names, де в key у нас буде значення з env, а в value – ім’я корзини.

Оновлюємо виклик модулю в проекті – додаємо передачу параметрів:

module "alb_logs_test" {
  source = "./modules/alb-s3-logs"

  #bucket = "${var.eks_env}-${var.eks_version}-${var.component}-${var.application}-${each.value}-alb-logs"
  # i.e. 'ops-1-28-backend-api-dev-alb-logs'
  eks_env     = var.environment
  eks_version = local.env_version
  component   = "backend"
  application = "api"  
}

Перевіряємо ще раз:

Створення aws_s3_bucket_public_access_block

До файлу modules/alb-s3-logs/s3.tf додамо ресурс aws_s3_bucket_public_access_block – проходимось в циклі по всім бакетам:

...
# block S3 bucket public access
resource "aws_s3_bucket_public_access_block" "alb_s3_logs_backend_acl" {
  for_each = aws_s3_bucket.alb_s3_logs

  bucket = each.value.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Створення Promtail Lambda

Далі додамо створення Lambda-функцій – на кожну корзину буде своя функція зі своїми змінними для лейбл в Loki.

Тобто для корзини “ops-1-28-backend-api-dev-alb-logs” ми створимо інстанс Promtail Lambda, у якої в змінних EXTRA_LABELS будуть значення “component=backend, logtype=alb, environment=dev“.

Для створення функцій нам потрібні нові змінні:

  • vpc_id: для Lambda Security Group
  • vpc_private_subnets_cidrs: для правил в Security Group – куди буде дозволено доступ
  • vpc_private_subnets_ids: для самих функцій – в яких сабнетах їх запускати
  • promtail_image: Docker image URL з AWS ECR, з якого буде створюватись Lambda
  • loki_write_address: для Promtail – куди слати логи

Дані по VPC ми отримуємо в самому проекті з ресурсу data "terraform_remote_state" (див. Terraform: terraform_remote_state – отримання outputs інших state-файлів), який бере їх з іншого проекту, який менеджить наші VPC:

# connect to the atlas-vpc Remote State to get the 'outputs' data
data "terraform_remote_state" "vpc" {
  backend = "s3"
  config = {
    bucket         = "tf-state-backend-atlas-vpc"
    key            = "${var.environment}/atlas-vpc-${var.environment}.tfstate"
    region         = var.aws_region
    dynamodb_table = "tf-state-lock-atlas-vpc"
  }
}

І потім в locals створюється об’єкт vpc_out з даними по VPC, і там жеж формується URL для Loki:

locals {
  ...
  # get VPC info
  vpc_out = data.terraform_remote_state.vpc.outputs

  # will be used in Lambda Promtail 'LOKI_WRITE_ADDRESS' env. variable
  # will create an URL: 'https://logger.1-28.ops.example.co:443/loki/api/v1/push'
  loki_write_address = "https://logger.${replace(var.eks_version, ".", "-")}.${var.environment}.example.co:443/loki/api/v1/push"
}

Додаємо нові змінні в variables.tf нашого модуля:

...
variable "vpc_id" {
  type        = string
  description = "ID of the VPC where to create security group"
}

variable "vpc_private_subnets_cidrs" {
  type        = list(string)
  description = "List of IPv4 CIDR ranges to use in Security Group rules and for Lambda functions"
}

variable "vpc_private_subnets_ids" {
  type        = list(string)
  description = "List of subnet ids when Lambda Function should run in the VPC. Usually private or intra subnets"
}

variable "promtail_image" {
  type        = string
  description = "Loki URL to push logs from Promtail Lambda"
  default = "492***148.dkr.ecr.us-east-1.amazonaws.com/lambda-promtail:latest"
}

variable "loki_write_address" {
  type        = string
  description = "Loki URL to push logs from Promtail Lambda"
}

Створення security_group_lambda

Створюємо файл modules/alb-logs/lambda.tf, і почнемо з модуля security_group_lambda з модулю terraform-aws-modules/security-group/aws, який створить нам Security Group – вона у нас одна на всі функції для логування:

data "aws_prefix_list" "s3" {
  filter {
    name   = "prefix-list-name"
    values = ["com.amazonaws.us-east-1.s3"]
  }
}

module "security_group_lambda" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "~> 5.1.0"

  name        = "${var.eks_env}-${var.eks_version}-loki-logger-lambda-sg"
  description = "Security Group for Lambda Egress"

  vpc_id = var.vpc_id

  egress_cidr_blocks      = var.vpc_private_subnets_cidrs
  egress_ipv6_cidr_blocks = []
  egress_prefix_list_ids  = [data.aws_prefix_list.s3.id]

  ingress_cidr_blocks      = var.vpc_private_subnets_cidrs
  ingress_ipv6_cidr_blocks = []

  egress_rules  = ["https-443-tcp"]
  ingress_rules = ["https-443-tcp"]
}

В файлі main.tf проекту додаємо передачу нових параметрів в модуль:

module "alb_logs_test" {
  source = "./modules/alb-s3-logs"

  #bucket = "${var.eks_env}-${var.eks_version}-${var.component}-${var.application}-${each.value}-alb-logs"
  # i.e. 'ops-1-28-backend-api-dev-alb-logs'
  eks_env     = var.environment
  eks_version = local.env_version
  component   = "backend"
  application = "api"

  vpc_id                    = local.vpc_out.vpc_id
  vpc_private_subnets_cidrs = local.vpc_out.vpc_private_subnets_cidrs
  vpc_private_subnets_ids   = local.vpc_out.vpc_private_subnets_ids
  loki_write_address        = local.loki_write_address
}

Виконуємо terraform init та terraform plan:

Тепер можемо додавати Lambda.

Створення модулю promtail_lambda

Далі – сама функція.

В ній нам потрібно буде вказати allowed_triggers – ім’я корзини, з якої можна виконувати нотифікацію про створення в корзині нових об’єктів, і для кожної корзини ми хочемо створити окрему функцію з власними змінними для labels в Loki.

Для цього описуємо модуль module "promtail_lambda", де знов зробимо цикл по всім корзинам – як робили з aws_s3_bucket_public_access_block.

Але в параметрах функції нам потрібно передати поточне значення з app_environments – “dev” або “prod“.

Для цього ми можемо використати each.key, бо коли ми створюємо resource “aws_s3_bucket” “alb_s3_logs” з for_each = var.app_environments або for_each = local.bucket_names – то отримуємо обє’кт, в якому в key буде кожне значення з var.app_environments, а в value – деталі корзини.

Давайте глянемо як це виглядає.

В нашому модулі додамо output – можна прямо в файлі modules/alb-s3-logs/s3.tf:

output "buckets" {
  value       = aws_s3_bucket.alb_s3_logs
}

В рутовому модулі, в самому проекті в файлі main.tf – теж output, який використовує output модулю:

...
output "alb_logs_buckets" {
  value = module.alb_logs_test.buckets
}

Робимо terraform plan, і маємо такий результат:

Тож повністю наша Lambda-функція буде такою:

...
module "promtail_lambda" {
  source  = "terraform-aws-modules/lambda/aws"
  version = "~> 7.2.1"
  # key: dev
  # value:  ops-1-28-backend-api-dev-alb-logs
  for_each = aws_s3_bucket.alb_s3_logs

  # <eks_env>-<eks_version>-<component>-<application>-<app_env>-alb-logs-logger
  # bucket name: ops-1-28-backend-api-dev-alb-logs
  # lambda name: ops-1-28-backend-api-dev-alb-logs-loki-logger
  function_name = "${each.value.id}-loki-logger"
  description   = "Promtail instance to collect logs from ALB Logs in S3"

  create_package = false
  # https://github.com/terraform-aws-modules/terraform-aws-lambda/issues/36
  publish = true

  image_uri     = var.promtail_image
  package_type  = "Image"
  architectures = ["x86_64"]

  # labels: "component,backend,logtype,alb,environment,dev"
  # will create: component=backend, logtype=alb, environment=dev
  environment_variables = {
    EXTRA_LABELS             = "component,${var.component},logtype,alb,environment,${each.key}"
    KEEP_STREAM              = "true"
    OMIT_EXTRA_LABELS_PREFIX = "true"
    PRINT_LOG_LINE           = "true"
    WRITE_ADDRESS            = var.loki_write_address
  }

  vpc_subnet_ids         = var.vpc_private_subnets_ids
  vpc_security_group_ids = [module.security_group_lambda.security_group_id]
  attach_network_policy  = true

  # bucket name: ops-1-28-backend-api-dev-alb-logs
  allowed_triggers = {
    S3 = {
      principal  = "s3.amazonaws.com"
      source_arn = "arn:aws:s3:::${each.value.id}"
    }
  }
}

Тут в each.value.id ми будемо мати ім’я корзини, а в environment,${each.key}" – значення “dev” або “prod“.

Перевіряємо – terraform init && terraform plan:

Створення aws_s3_bucket_policy

Наступним ресурсом нам потрібна політика для S3, яка буде дозволяти писати ALB та читати нашій Lambda-функції.

Тут у нас будуть дві нові змінні:

  • aws_account_id: передамо з рутового модулю
  • elb_account_id: поки можемо задати дефолтне значення, бо ми тільки в одному регіоні

Додаємо в variables.tf модулю:

...
variable "aws_account_id" {
  type        = string
  description = "AWS account ID"
}

variable "elb_account_id" {
  type        = string
  description = "AWS ELB Account ID to be used in the ALB Logs S3 Bucket Policy"
  default     = 127311923021
}

І в файлі modules/alb-s3-logs/s3.tf описуємо сам aws_s3_bucket_policy:

...
resource "aws_s3_bucket_policy" "s3_logs_alb_lambda_allow" {
  for_each = aws_s3_bucket.alb_s3_logs

  bucket = each.value.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${var.elb_account_id}:root"
        }
        Action = "s3:PutObject"
        Resource = "arn:aws:s3:::${each.value.id}/AWSLogs/${var.aws_account_id}/*"
      },
      {
        Effect = "Allow"
        Principal = {
          AWS = module.promtail_lambda[each.key].lambda_role_arn
        }
        Action = "s3:GetObject"
        Resource = "arn:aws:s3:::${each.value.id}/*"
      }
    ]
  })
}

Тут ми знову використовуємо each.key з наших корзин, де будемо мати значення “dev” або “prod“.

І, відповідно, можемо звернутись до кожного ресурсу module "promtail_lambda" – бо вони теж створюються в циклі – module.alb_logs_test.module.promtail_lambda["dev"].aws_lambda_function.this[0].

Додаємо передачу aws_account_id в корневому модулі:

module "alb_logs_test" {
  source = "./modules/alb-s3-logs"
  ...
  vpc_private_subnets_ids   = local.vpc_out.vpc_private_subnets_ids
  loki_write_address        = local.loki_write_address
  aws_account_id            = data.aws_caller_identity.current.account_id
}

Перевіряємо з terraform plan:

Створення aws_s3_bucket_notification

Останній ресурс – aws_s3_bucket_notification, який створить нотифікацію в Lambda при появі нового об’єкту в корзині.

Тут теж самий принцип – цикл по корзинам, і по env через each.key:

...
resource "aws_s3_bucket_notification" "s3_logs_notification" {
  for_each = aws_s3_bucket.alb_s3_logs

  bucket = each.value.id

  lambda_function {
    lambda_function_arn = module.promtail_lambda[each.key].lambda_function_arn
    events              = ["s3:ObjectCreated:*"]
    filter_prefix       = "AWSLogs/${var.aws_account_id}/"
  }
}

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

І тепер у нас все готово – давайте деплоїти.

Перевірка роботи Promtail Lambda

Деплоїмо з terraform apply, перевіряємо корзини:

$ aws --profile work s3api list-buckets | grep ops-1-28-backend-api-dev-alb-logs
            "Name": "ops-1-28-backend-api-dev-alb-logs",

Створимо Ingress з s3.bucket=ops-1-28-backend-api-dev-alb-logs:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
    spec:
      containers:
        - name: nginx-demo-container
          image: nginx
          ports:
            - containerPort: 80

---
apiVersion: v1
kind: Service
metadata:
  name: nginx-demo-service
spec:
  selector:
    app: nginx-demo
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-ingress
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80}]'
    alb.ingress.kubernetes.io/load-balancer-attributes: access_logs.s3.enabled=true,access_logs.s3.bucket=ops-1-28-backend-api-dev-alb-logs
spec:
  ingressClassName: alb
  rules:
    - host: test-logs.ops.example.co
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-demo-service
                port:
                  number: 80

Перевіряємо його:

$ kk get ingress example-ingress
NAME              CLASS   HOSTS                     ADDRESS                                                                   PORTS   AGE
example-ingress   alb     test-logs.ops.example.co   k8s-opsmonit-examplei-8f89ccef47-1782090491.us-east-1.elb.amazonaws.com   80      39s

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

$ aws s3 ls ops-1-28-backend-api-dev-alb-logs/AWSLogs/492***148/
2024-02-20 16:56:54        107 ELBAccessLogTestFile

Тестовий файл є – значить ALB може писати логи.

Робимо запити до ендпоінту:

$ curl -I http://test-logs.ops.example.co
HTTP/1.1 200 OK

За пару хвилин перевіряємо відповідну Lambda-функцію:

Виклики пошли, добре.

І перевіряємо логи в Loki:

Все працює.

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

Terraform модуль з GitHub репозиторію

Створюмо новий репозиторій, копіюємо в нього всю директорію модулю – alb-s3-logs:

$ cp -r ../atlas-monitoring/terraform/modules/alb-s3-logs/ .
$  tree .
.
|-- README.md
`-- alb-s3-logs
    |-- lambda.tf
    |-- s3.tf
    `-- variables.tf

2 directories, 4 files

Комітимо, пушимо:

$ ga -A
$ gm "feat: module for ALB logs collect"
$ git push

І оновлюємо його виклик в проекті:

module "alb_logs_test" {
  #source = "./modules/alb-s3-logs"
  source = "[email protected]:org-name/atlas-tf-modules//alb-s3-logs"
  ...
  loki_write_address        = local.loki_write_address
  aws_account_id            = data.aws_caller_identity.current.account_id
}

Робимо terraform init:

$ terraform init
...
Downloading git::ssh://[email protected]/org-name/atlas-tf-modules for alb_logs_test...
- alb_logs_test in .terraform/modules/alb_logs_test/alb-s3-logs

...

І перевіряємо ресурси:

$ terraform plan
...
No changes. Your infrastructure matches the configuration.

Все готово.

Loading

Grafana Loki: LogQL та Recoding Rules для метрик з логів AWS Load Balancer
0 (0)

10 Лютого 2024

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

Отже, про що мова: у нас є AWS Load Balancers, логи з яких збираються до Grafana Loki, див. Grafana Loki: збираємо логи AWS LoadBalancer з S3 за допомогою Promtail Lambda.

В Loki маємо Recoding Rules і remote write, за допомогою чого генеруємо деякі метрики, і записуємо їх до VictoriaMetrics, звідки їх використовують VMAlert та Grafana – див. Grafana Loki: оптимізація роботи – Recording Rules, кешування та паралельні запити.

Що я хочу, це зробити графіки по Load Balancers не з метрик CloudWatch – а генерувати їх з логів, по, по-перше – збір метрик з CloudWatch у Prometheus/VictoriaMetrics коштує грошей за запити до CloudWatch, по-друге – з логів ми можемо отримати набагато більше інформації, по-третє – метрики CloudWatch мають обмеження, які ми можемо обійти з логами.

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

Також деякі метрики не дають тієї картини, котру хочеться, як-от метрика ProcessedBytes, яка має значення по “traffic to and from clients“, але немає інформації по recieved та sent bytes.

Тож що будемо робити:

  • розгорнемо тестове оточення в Kubernetes – Pod та Ingress/ALB з логуванням в S3
  • налаштуємо збір логів з S3 через Lambda-функцію з Promtail
  • подивимось на поля в ALB Access Logs – які можуть бути нам цікаві
  • подумаємо, які графіки хотілося б мати для дашборди Графани і, відповідно, які метрики нам для цього можуть знадобитись
  • створимо запити для Loki, які будуть формувати дані з логу AWS LoadBalancer
  • і напишемо кілька Loki Recording Rules, які будуть нам генерувати потрібні метрики

Тестове оточення в Kubernetes

Нам потрібен Pod, який буде генерувати HTTP-відповіді, та Ingress, який створить AWS ALB, з якого ми будемо збирати логи.

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

Створюємо корзину для логів:

$ aws --profile work s3api create-bucket --bucket eks-alb-logs-test --region us-east-1

Створюємо файл з Policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::127311923021:root"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::eks-alb-logs-test/AWSLogs/492***148/*"
        },
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::492***148:role/grafana-atlas-monitoring-ops-1-28-loki-logger-devops-alb"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::eks-alb-logs-test/*"
        }
    ]
}

Підключаємо цю політику до корзини:

$ aws --profile work s3api put-bucket-policy --bucket eks-alb-logs-test --policy file://s3-alb-logs-policy.json

В бакеті налаштовуємо Properties > Event notifications на івенти ObjectCreated:

Задаємо Destination до Lambda з Promtail:

Описуємо ресурси для Kubernetes – Deployment, Service та Ingress.

В Ingress annotations задаємо access_logs.

Крім того, потрібен TLS – бо записи про імена доменів в запитах ми будемо брати саме з нього:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
    spec:
      containers:
        - name: nginx-demo-container
          image: nginx
          ports:
            - containerPort: 80

---
apiVersion: v1
kind: Service
metadata:
  name: nginx-demo-service
spec:
  selector:
    app: nginx-demo
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-ingress
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}, {"HTTP":80}]'
    alb.ingress.kubernetes.io/load-balancer-attributes: access_logs.s3.enabled=true,access_logs.s3.bucket=eks-alb-logs-test
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:492***148:certificate/bab1c48a-d11e-4572-940e-2cd43aafb615
spec:
  ingressClassName: alb
  rules:
    - host: test-alb-logs-1.setevoy.org.ua
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-demo-service
                port:
                  number: 80

Деплоїмо:

$ kk apply -f ingress-svc-deploy.yaml 
deployment.apps/nginx-demo-deployment created
service/nginx-demo-service created
ingress.networking.k8s.io/example-ingress created

Перевіряємо Ingress та Load Balancer:

$ kk get ingress example-ingress
NAME              CLASS   HOSTS                          ADDRESS                                                                   PORTS   AGE
example-ingress   alb     test-alb-logs-1.setevoy.org.ua   k8s-opsmonit-examplei-8f89ccef47-1707845441.us-east-1.elb.amazonaws.com   80      2m40s

Перевіряємо з cURL, що все працює:

$ curl -I https://test-alb-logs.setevoy.org.ua
HTTP/1.1 200 OK

Є сенс додати Security Group, де закрити доступ усім крім вашого IP – щоб під час тестування бути впевненим в результатах.

Application Load Balancer: корисні поля в Access Log

Документація – Access logs for your Application Load Balancer.

Що нам може бути цікавим в логах?

Враховуємо, що в нашому випадку Load Balancer target – це буде Kubernetes Pod:

  • type: у нас деякі ALB використовують WebSockets, то було добре мати змогу виділяти їх
  • time: час створення відповіді клієнту – не те, щоб знадобилось в графіках, але най буде
  • elb: Load Balancer ID – корисно в алертах і можна використати в фільтрах графіків Grafana
  • client:port та target:port: звіки запит прийшов, і куди пішов; у нашому випадку target:port буде Pod IP
  • request_processing_time: час від отримання запиту від клієнта до передачі його до Pod
  • target_processing_time: час від відправки запиту до Pod до початку отримання відповіді від Pod
  • response_processing_time: час від отримання відповіді від Pod до початку передачі відповіді клієнту
  • elb_status_code: HTTP код від ALB до клієнта
  • target_status_code: HTTP код від Pod до ALB
  • received_bytes: отримано байт від клієнта – для HTTP включає розмір headers, для WebSockets – загальний об’єм байт переданий в рамках connection
  • sent_bytes: аналогічно, але для відповіді клієнту
  • request: повний URL запиту від клієнта у формі HTTP method + protocol://host:port/uri + HTTP version
  • user_agent: User Agent
  • target_group_arn: Amazon Resource Name (ARN) для Target Group, на яку віправляються запити
  • trace_id: X-Ray ID – корисно, якщо ним користуєтесь (я робив data links в Grafana на дашборду з X-Ray)
  • domain_name: на HTTPS Listener – ім’я хосту в запиті під час TLS handshake (власне домен, за яким звернувся клієнт)
  • actions_executed: може бути корисним особливо якщо використовується AWS WAF, див. Actions taken
  • error_reason: причина помилки, якщо запит сфейливсся – я не користуюсь, але може бути корисно, див. Error reason codes
  • target:port_list: аналогічно target:port
  • target_status_code_list: аналогічно target_status_code

Grafana Loki: логи Application Load Balancer та LogQL pattern

Знаючи існуючі поля ми можемо створити запит, який буде формувати Fields з записів в логах. А далі ці поля ми зможемо використовувати або для створення labels, або для побудови графіків.

Офіційна документація по темі:

І мій пост Grafana Loki: можливості LogQL для роботи з логами та створення метрик для алертів.

(я все збираєсь спробувати VictoriaLogs, цікаво, чи вміє вона в Recodring Rules та генерацію метрик, як доберусь – напишу пост)

При роботі з Load Balancer логами в Loki враховуйте, що від моменту запиту до самої Loki дані дойдуть через 5 хвилин – і це окрім того, що ми бачимо дані з затримкою, створить ще деякі проблеми, які далі розберемо.

Використуємо парсер pattern, якому задамо імена полів, які хочемо створити: на кожне поле в логу зробимо окремий <field> в Loki:

pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`

В кінці через <_> пропускаємо два поля – classification та classification_reason, бо я поки не бачу, де б воно могло знадобитись.

Тепер повний запит в Loki разом з Stream Selector буде виглядати так:

{logtype="alb", component="devops"} |= "test-alb-logs.setevoy.org.ua" | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`

Або, як ми вже маємо поле domain, то замість Line filter expression додамо Label filter expressiondomain="test-alb-logs.setevoy.org.ua":

{logtype="alb", component="devops"} 
| pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
| domain="test-alb-logs.setevoy.org.ua"

HTTP Load testing tools

Далі потрібно чимось виконувати запити до нашого Load Balancer, щоб генерувати логи, і мати змогу задавати конкретні параметри на кшалт кількості запитів в секунду.

Спробував декілька різних утиліт, наприклад Vegeta:

$ echo "GET https://test-alb-logs.setevoy.org.ua/" | vegeta attack -rate=1 -duration=60s | vegeta report
Requests      [total, rate, throughput]         60, 1.02, 1.01
Duration      [total, attack, wait]             59.126s, 59s, 125.756ms
Latencies     [min, mean, 50, 90, 95, 99, max]  124.946ms, 133.825ms, 126.062ms, 127.709ms, 128.203ms, 542.437ms, 588.428ms
Bytes In      [total, mean]                     36900, 615.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:60  
Error Set:

Або wrk2:

$ wrk2 -c1 -t1 -d60s -R1 https://test-alb-logs.setevoy.org.ua/
Running 1m test @ https://test-alb-logs.setevoy.org.ua/
  1 threads and 1 connections
  Thread calibration: mean lat.: 166.406ms, rate sampling interval: 266ms
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   127.77ms    0.94ms 131.97ms   80.00%
    Req/Sec     0.80      1.33     3.00    100.00%
  60 requests in 1.00m, 49.98KB read
Requests/sec:      1.00
Transfer/sec:     852.93B

wrk2 якось більше зайшов, бо простіше.

Ще, звісно, можно взяти Bash та Apache Benchmark:

#!/bin/bash
for i in {1..60}
do
  ab -n 1 -c 1 https://test-alb-logs.setevoy.org.ua/
  sleep 1
done

Але це трохи зайві костилі.

Або JMeter – проте це вже зовсім overkill для наших потреб

Дашборда Grafana: планування

Для ALB є готові дашборди, наприклад – AWS ELB Application Load Balancer by Target Group та AWS ALB Cloudwatch Metrics, тож з них ми можемо взяти собі ідеї для графіків, які будемо будувати.

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

Детальніше про створення борди можна почитати у, наприклад, Karpenter: моніторинг та Grafana dashboard для Kubernetes WorkerNodes, а тут зосередимось на запитах Loki.

Фільтри:

  • domain – візьмемо з поля domain (забігаючи наперед – запит label_values(aws:alb:requests:sum_by:rate:1m,domain))
  • alb_id – з фільтром по $domain (запит label_values(aws:alb:requests:sum_by:rate:1m{domain=~"$domain"),domain))

Далі – на самій борді зверху будуть всякі Stats, а під ними – графіки.

Зверху – stats:

  • requests/sec: per-second rate запитів на секунду
  • response time: загальний час відповіді клієнту
  • 4xx rate та 5xx rate: per-second rate помилок
  • received та transmitted throughput: скільки байт в секунду отримуємо та відправляємо
  • client connections: загальна кількість connections

І власне графіки кожному обраному в фільтрі Load Balancer:

  • HTTP codes by ALB: загальна картина по кодам відповідей клієнтам, per-second rate
  • HTTP code by targets: загальна картина по кодам відповідей від Pods, per-second rate
  • 4xx/5xx graph: графік по помилкам, per-second rate
  • requests total: графік по загальній кількості запитів від клієнтів
  • ALB response time: загальний час відповіді клієнту
  • target response time: час відповіді від подів
  • received та transmitted throughput: скільки байт в секунду отримуємо та відправляємо

Створення метрик з Loki Recording Rules

Запускаємо тест – 1 запит в секунду:

$ wrk2 -c1 -t1 -R1 -H "User-Agent: wrk2" -d30m https://test-alb-logs-1.setevoy.org.ua/

І поїхали творити запити.

LogQL для requests/sec

Перше, що приходить в голову для отримання per-second rate запитів – це використати функцію rate():

rate(log-range): calculates the number of entries per second

В rate() використовуємо наш stream selector {logtype="alb", component="devops"} (в мене ці лейбли задаються на Lambda Promtail) та парсер pattern, щоб створити поля зі значеннями, які потім будемо використовувати:

Щоб отримати загальний результат по всім знайденим записам – “огорнемо” наш запит в функцію sum():

sum(
    rate(
        {logtype="alb", component="devops"} 
            | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
            | domain="test-alb-logs.setevoy.org.ua"
            [1m]
    )
)

Якщо ж ми хочемо робити метрику, яка буде в лейблах мати ім’я ALB та ім’я домену, щоб потім використовувати ці лейбли в фільтрах дашборди Grafana – то додаємо sum by (elb_id, domain):

І тепер маємо значення по кількості всіх унікальних пар elb_id, domain – 1 запит на секунду, як ми з задавали в wrk2 -c1 -t1 -R1.

Наче виглядає чудово?

Але давайте спробуємо цей запит використати в Loki Recording Rules та створити на його основі метрику.

Loki Recording Rule для метрик та проблема з AWS Load Balancer Logs EmitInterval

Додаємо запис для створення метрики aws:alb:requests:sum_by:rate:1m:

kind: ConfigMap
apiVersion: v1
metadata:
  name: loki-alert-rules
data:
  rules.yaml: |-
    groups:
      - name: AWS-ALB-Metrics-1
        rules:
        - record: aws:alb:requests:sum_by:rate:1m
          expr: |
            sum by (elb_id, domain) (
                rate(
                    {logtype="alb", component="devops"} 
                        | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
                        | domain="test-alb-logs-1.setevoy.org.ua"
                        [1m]
                )
            )

Деплоїмо, і перевіряємо в VictoriaMetrics:

WTF?!?

По-перше – чому значення 0.3, а не 1?

По-друге – чому ми в результаті маємо розриви в лінії?

AWS Load Balancer та logs EmitInterval

Давайте повернемось до Loki, і збільшемо інтервал для rate() до 5 хвилин:

І в результаті бачимо, що значення поступово знижується з 1 до нуля.

Чому?

Тому що логи з LoadBalancer до S3 збираються раз на 5 хвилин – див. Access log files:

Elastic Load Balancing publishes a log file for each load balancer node every 5 minutes

В Classic Load Balancer є можливість налаштувати частоту передачі логів до S3 з параметром EmitInterval – але в Application Load Balancer це значення фіксоване 5 хвилинами.

А як працює функція rate()?

Вона в момент виклику бере всі значення за заданий проміжок часу – останні 5 хвилин в нашому випадку, rate({...}[5m]) – і формує середнє значення за секунду:

calculates per second rate of all values in the specified interval

Тобто картина виходить така:

Тож коли ми викликаємо rate() в 12:28 – вона бере дані за проміжок 12:23-12:28, але на цей момент остані логи в нас є тільки до 12:25, а тому і значення, яке повертає rate() з кожною хвилиною зменшується.

Коли ж нові логи приходять до Loki – то rate() обробляє їх всіх, і лінія вирівнюється до значення 1.

Окей, тут більш-менш ясно-понятно.

А що з графіками в Prometheus/VictoriaMetrics, які ми будуємо з метрики aws:alb:requests:sum_by:rate:1m?

Тут картина ще цікавіша, бо Loki Ruler виконує запити раз на хвилину:

# How frequently to evaluate rules.
# CLI flag: -ruler.evaluation-interval
[evaluation_interval: <duration> | default = 1m]

Тобто, запит був виконаний у 12:46:42:

(час на сервері UTC, -2 години від часу в VictoriaMetrics VMUI, тому тут 10:46:42, а не 12:46:42)

І отримав значення “0.3” – бо нових логів ще було:

І навіть якщо ми в Recording Rule збільшемо час в rate() до 5 хвилин – ми все одно не тримаємо правильної картини, бо частина проміжку часу ще не буде мати логів.

Біда.

LogQL та offset

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

Я тут досить довго намагався знайти рішення, і пробував варіант з count_over_time, думав спробувати рішення з ruler_evaluation_delay_duration, як описано у Loki recording rule range query, і думав пробувати погратись з Ruler evaluation_interval, але придумалось набагато простіше і, мабуть, найбільш правильне – чому б не використати offset?

Не був впевнений, що LogQL його підтримує, але як виявилось – таки є:

З offset 5m ми “зсуваємо” початок нашого запиту: замість того, щоб брати дані за останню хвилину – ми беремо дані за 5 хвилин раніше + наша хвилина для rate(), і тоді в результаті ми вже маємо всі логи.

Це, звісно, трохи портить загальну картину, бо ми не отримаємо самі актуаільні дані, але ми і так маємо їх з затримкою в 5 хвилин, і для графіків та алертів по Load Balancer такі затримки не є критичними, тож, imho, рішення цілком валідне.

Додамо нову метрику aws:alb:requests:sum_by:rate:1m:offset:5m:

...
        - record: aws:alb:requests:sum_by:rate:1m:offset:5m
          expr: |
            sum by (elb_id, domain) (
                rate(
                    {logtype="alb", component="devops"} 
                        | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
                        | domain="test-alb-logs-1.setevoy.org.ua"
                        [1m] offset 5m
                )
            )

І перевіримо результат тепер:

Щоб впевнитись – запустимо тест на 2 запити в секунду:

$ wrk2 -c2 -t2 -R2 -H "User-Agent: wrk2" -d120m https://test-alb-logs-1.setevoy.org.ua/

Чекаємо 5-7 хвилин, і перевіряємо графік ще раз:

Окей, з цим розібрались – поїхали далі.

LogQL для response time

Що потрібно: мати уяву скільки часу займає весь процес відповіді клієнту – від отримання запиту на ALB, до початку відправки йому даних.

Для цього у нас в логах є три поля:

  • request_processing_time: час від отримання запиту від клієнта до передачі його до Pod
  • target_processing_time: час від відправки запиту до Pod до початку отримання відповіді від нього
  • response_processing_time: час від отримання відповіді від Pod до початку передачі відповіді клієнту

Тож ми можемо зробити три метрики, потім отримати загальну суму – і отримаємо загальний час.

Для цього нам потрібно:

  • вибрати логи з Log Selector
  • із запису взяти значення з поля target_processing_time
  • і отрмати середнє значення за, наприклад, 1 хвилину

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

Запит буде виглядати таким:

avg_over_time (
    {logtype="alb"} 
        | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
        | __error__="" 
        | unwrap target_processing_time
        | target_processing_time >= 0
        | domain="test-alb-logs-1.setevoy.org.ua"
    [1m] offset 5m
) by (elb_id)

target_processing_time >= 0 використовуємо, бо деякі записи можуть мати значення -1:

This value is set to -1 if the load balancer can’t dispatch the request to a target

Фактично, avg_over_time() виконує sum_over_time() (сума з unwrap target_processing_time всіх значень за 1 хвилину) / на count_over_time() (загальна кількість записів з Log Selector {logtype="alb"}):

sum(sum_over_time (
    {logtype="alb"} 
        | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
        | __error__="" 
        | unwrap target_processing_time
        | target_processing_time >= 0
        | domain="test-alb-logs-1.setevoy.org.ua"
    [1m] offset 5m
))
/
sum(count_over_time (
    {logtype="alb"} 
        | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
        | __error__="" 
        | target_processing_time >= 0
        | domain="test-alb-logs-1.setevoy.org.ua"
    [1m] offset 5m
))

І порівняємо результат з CloudWatch – 14:21 на нашому графіку це 14:16 в CloudWatch, бо ми використовуємо offset 5m:

0.0008 CloudWatch – і 0.0007 у нас. В принципі, плюс-мінус воно.

Або можемо взяти 99-й перцентиль:

І теж саме в CloudWatch:

Додаємо Recording Rule aws:alb:target_processing_time:percentile:99:

- record: aws:alb:target_processing_time:percentile:99
  expr: |
    quantile_over_time (0.99,
        {logtype="alb"} 
            | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
            | __error__="" 
            | unwrap target_processing_time
            | target_processing_time >= 0
            | domain="test-alb-logs-1.setevoy.org.ua" [1m] offset 5m
    ) by (elb_id)

І перевіряємо в VictoriaMetrics:

Ще раз порівняємо з CloudWatch:

Добре, з цим теж розібрались.

Далі – статистика по 4хх, 5хх, та й взагалі всім кодам.

LogQL для 4xx та 5xx errors rate

Тут все просто – можемо взяти наш самий перший запит, і додати в sum by (elb_code):

sum by (elb_id, domain, elb_code) (
    rate(
        {logtype="alb", component="devops"} 
            | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
            | domain="test-alb-logs-1.setevoy.org.ua"
            [1m] offset 5m
    )
)

У нас все ще запущений wrk2 на 2 запити в секунду – ось ми їх і бачимо.

Давайте додамо ще один виклик на тіж 2 запити в секунду, але на неіснуючу сторінку, шоб отримати код 404:

$ wrk2 -c2 -t2 -R2 -H "User-Agent: wrk2" -d120m https://test-alb-logs-1.setevoy.org.ua/404-page

І поки воно дійде до логів – створимо метрику:

- record: aws:alb:requests:sum_by:elb_http_codes_rate:1m:offset:5m
  expr: |
    sum by (elb_id, domain, elb_code) (
        rate(
            {logtype="alb", component="devops"} 
                | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
                | domain="test-alb-logs-1.setevoy.org.ua"
                [1m] offset 5m
        )
    )

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

Аналогічно робимо, якщо треба мати метрики/графіки по кодам з TargetGroups, тільки замість sum by (elb_id, domain, elb_code) робимо sum by (elb_id, domain, target_code).

LogQL для received та transmitted bytes

Метрика CloudWatch ProcessedBytes має загальне значення для отримано/передано, а я хотів б бачити їх окремо.

Отже, зараз у нас виконується 2 запити в секунду.

Глянемо в лог на значення поля sent_bytes:

На кожен запит з кодом 200 ми відправляємо відповідь у 853 байти, тобто на два запити в секунду – 1706 байт/секунду.

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

sum by (elb_id, domain) (              
    sum_over_time(
        (
            {logtype="alb"} 
            | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
            | __error__="" 
            | domain="test-alb-logs-1.setevoy.org.ua"
            | unwrap sent_bytes
        )[1s]
    )
)

Тут ми з unwrap sent_bytes беремо значення поля sent_bytes, передаємо його до sum_over_time[1s] – і отримуємо наші 1706 байт в секунду:

Додаємо Recording Rule:

- record: aws:alb:sent_bytes:sum
  expr: |
    sum by (elb_id, domain) (              
        sum_over_time(
            (
                {logtype="alb"} 
                | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
                | __error__="" 
                | domain="test-alb-logs-1.setevoy.org.ua"
                | unwrap sent_bytes
            )[1s] offset 5m
        )
    )

І маємо дані в VictoriaMetrcis:

Або можна зробити теж саме, але використовуючи функцію rate():

sum by (elb_id, domain) (
  rate(
    (
      {logtype="alb"} 
      | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
      | __error__="" 
      | domain="test-alb-logs-1.setevoy.org.ua"
      | unwrap sent_bytes
    )[5m]
  )
)

Для Recording Rules – додаємо offset 5m:

- record: aws:alb:sent_bytes:rate:5m:offset:5m
  expr: |
    sum by (elb_id, domain) (
      rate(
        (
          {logtype="alb"} 
          | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
          | __error__="" 
          | domain="test-alb-logs-1.setevoy.org.ua"
          | unwrap sent_bytes
        )[1m] offset 5m
      )
    )

Аналогічно робимо для received_bytes.

LogQL для Client Connections

Як ми можемо порахувати кількість саме connections до ALB? Бо в CloudWatch такої метрики нема.

Кожен TCP Connection формується з пари Client_IP:Client_Port – Destination_IP:Destination_Port – див. A brief overview of TCP/IP communications.

В логах ми маємо поле client_ip зі значеннями типу “178.158.203.100:41426“, де 41426 – це локальний TCP-порт на моєму ноуті, який використовується в рамках цього підключення до destination Load Balancer.

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

Отже, зробимо такий запит:

sum by (elb_id, domain) (
    sum by (elb_id, domain, client_ip) (
        count_over_time(
            (
                {logtype="alb"} 
                | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
                | __error__="" 
                | domain="test-alb-logs-1.setevoy.org.ua"
            )[1s]
        )
    )
)

Тут:

  • за допомогою count_over_time()[1s] ми беремо кількість записів в логах за останню секунду
  • отриманий результат “огортаємо” в sum by (elb_id, domain, client_ip) – тут client_ip використовуємо, щоб отримати унікальні значення для кожного Client_IP:Client_Port, і отримуємо кількість унікальних значень по полям elb_id, domain, client_ip
  • а потім ще раз огортаємо в sum by (elb_id, domain) – щоб отримати загальну кількість таких унікальних записів по кожному Load Balancer та Domain

Для перевірки запускаємо ще один інстанс wrk2, через пару хвилин ще один – і маємо 4 connection:

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

Або теж саме, але з rate() за 1 хвилину – щоб графік був більш рівномірний:

sum by (elb_id, domain) (
    sum by (elb_id, domain, client_ip) (
        rate(
            (
                {logtype="alb"} 
                | pattern `<type> <date> <elb_id> <client_ip> <pod_ip> <request_processing_time> <target_processing_time> <response_processing_time> <elb_code> <target_code> <received_bytes> <sent_bytes> "<request>" "<user_agent>" <ssl_cipher> <ssl_protocol> <target_group_arn> "<trace_id>" "<domain>" "<chosen_cert_arn>" <matched_rule_priority> <request_creation_time> "<actions_executed>" "<redirect_url>" "<error_reason>" "<target>" "<target_status_code>" "<_>" "<_>"`
                | __error__="" 
                | domain="test-alb-logs-1.setevoy.org.ua"
            )[1m] offset 5m
        )
    )
)

Ну і в принципі на цьому все. Маючі такі запити ми можемо побудувати борду.

Loki Recording Rules, Prometheus/VictoriaMetrics та High Cardinality

Окремо додам на тему створення labels: було б дуже прикольно зробити метрику, яка в своїх labels мала б, наприклад, target/Pod IP, або IP клієнтів.

Втім, це призведе до такого явища як “High Cardinality“:

Any additional log lines that match those combinations of label/values would be added to the existing stream. If another unique combination of labels comes in (e.g. status_code=”500”) another new stream is created.

Imagine now if you set a label for ip. Not only does every request from a user become a unique stream. Every request with a different action or status_code from the same user will get its own stream.

Doing some quick math, if there are maybe four common actions (GET, PUT, POST, DELETE) and maybe four common status codes (although there could be more than four!), this would be 16 streams and 16 separate chunks. Now multiply this by every user if we use a label for ip. You can quickly have thousands or tens of thousands of streams.

This is high cardinality. This can kill Loki.

Див. How labels in Loki can make log queries faster and easier.

В нашому випадку ми генеруємо метрику, яку записуємо в TSDB VictoriaMetrics, і кожна додаткова лейбла з унікальним значенням буде створювати додаткові time-series.

Див. Understanding Cardinality in Prometheus Monitoring.

Loading

Arduino: запуск Arduino IDE на Linux, та перший “Hello, World!”
0 (0)

3 Лютого 2024

Продовжуємо бавитись з Ардуінкою 🙂

В попередньому пості трохи поговорили про Starter Kit – тепер хочеться вже ж щось зробити.

Тож встановимо IDE на Arch Linux, і спробуємо виконати перший код.

Запуск Arduino IDE на Linux

Документація для Arch Linux є тут>>>.

Встановлюємо IDE:

$ yay -S arduino-ide-bin

Перший запуск:

$ arduino-ide

Додамо CLI:

$ sudo pacman -S arduino-cli

Приклади в моїй офіційній документації є тут>>>, але там прям біда з форматуванням коду, тож я взяв інший приклад ось тут – Turn an LED on and off every second.

Arduino IDE, Linux та “No ports discovered”

Тепер нам в IDE треба вибрати board, тобто плату Arduino, з якою будемо працювати, але – “No ports discovered

Ну, Linux жеж 🙂

“Нєльзя просто так взять, і…” (с)

У CLI аналогічно:

$ arduino-cli board list
No boards found.

Окей, а якщо..

 

Але ні – після ребуту системи теж нічого не змінилося.

Судячи з документації, має з’явитись новий девайс – /dev/ttyACM0 або /dev/ttyUSBx – проте в мене нічого. Та й lsusb не виводить ніяких нових девайсів.

Почав гуглити, знайшов ось цей тред, і подумав – може і в мене проблема з USB-кабелем? А як перевірити? Бо іншого кабелю під рукою нема.

Проте є ігровий ПК з Віндою – давайте спробуємо там.

Встановлюємо IDE на Вінду, і – о чудо!

Але ж працювати під Віндою не хочеться, тож пішов знову пробувати з Linux, переключив Arduino назад до ноута з Arch, і – о, знову чудо!

Feb 03 16:14:41 setevoy-wrk-laptop kernel: usb 4-2: new full-speed USB device number 5 using xhci_hcd
Feb 03 16:14:41 setevoy-wrk-laptop kernel: usb 4-2: New USB device found, idVendor=10c4, idProduct=ea60, bcdDevice= 1.00
Feb 03 16:14:41 setevoy-wrk-laptop kernel: usb 4-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
Feb 03 16:14:41 setevoy-wrk-laptop kernel: usb 4-2: Product: CP2102 USB to UART Bridge Controller
Feb 03 16:14:41 setevoy-wrk-laptop kernel: usb 4-2: Manufacturer: Silicon Labs
Feb 03 16:14:41 setevoy-wrk-laptop kernel: usb 4-2: SerialNumber: 0001
Feb 03 16:14:41 setevoy-wrk-laptop kernel: cp210x 4-2:1.0: cp210x converter detected
Feb 03 16:14:41 setevoy-wrk-laptop kernel: usb 4-2: cp210x converter now attached to ttyUSB0
Feb 03 16:14:41 setevoy-wrk-laptop mtp-probe[48870]: checking bus 4, device 5: "/sys/devices/pci0000:00/0000:00:08.1/0000:06:00.4/usb4/4-2"
Feb 03 16:14:41 setevoy-wrk-laptop mtp-probe[48870]: bus: 4, device: 5 was not an MTP device
Feb 03 16:14:42 setevoy-wrk-laptop mtp-probe[48931]: checking bus 4, device 5: "/sys/devices/pci0000:00/0000:00:08.1/0000:06:00.4/usb4/4-2"
Feb 03 16:14:42 setevoy-wrk-laptop mtp-probe[48931]: bus: 4, device: 5 was not an MTP device

І тепер є новий девайс в lsusb:

$ lsusb
...
Bus 004 Device 005: ID 10c4:ea60 Silicon Labs CP210x UART Bridge
...

І tty:

$ ll /dev/ttyUSB0 
crw-rw---- 1 root uucp 188, 0 Feb  3 16:14 /dev/ttyUSB0

І тепер є порт в IDE під Linux:

 

Не знаю, чому так вийшло. Можливо, Ардуінка якось ініциалізувалася, коли нормально підключилась на Windows.

Але це ще не все 🙂

“/dev/ttyUSB0”: Permission denied

Потрібно додатково дозволити доступ до /dev/ttyUSB0 – тут трохи забігаю наперед, коли вже пробував заливати код:

Додаємо файл /etc/udev/rules.d/01-ttyusb.rules:

SUBSYSTEMS=="usb-serial", TAG+="uaccess"

Робимо релоад udev:

$ sudo udevadm control --reload

Перепідключаємо ардуінку, і клікаємо Upload:

І тепер все завантажилось:

Arduino “Hello, World!”

Окей, наче все готово?

Спробуємо зробити з прикладу Turn an LED on and off every second.

IDE та код

Тут просто копіюємо код з тієї сторінки:

// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
  delay(1000);                      // wait for a second
  digitalWrite(LED_BUILTIN, LOW);   // turn the LED off by making the voltage LOW
  delay(1000);                      // wait for a second
}

Підключаємо Arduino, клікаємо Upload:

В коді все наче досить просто:

  • виконуємо ініціалізацію LED_BUILTIN (13 pin на платі)
  • запускаємо цикл, в якому
    • подаємо живлення на LED_BUILTIN
    • чекаємо 1000 мілісекунд
    • відключаємо живлення

Тепер зберемо сам “девайс”, який буде нам блимкати лампочкою.

LED та резистор

Нам потрібен LED і резистор на 220 Ом.

Якщо з LED все зрозуміло, то з резистором я трохи покопався.

В наборі є кілька резисторів і таблиця відповідності кольорів – але я ну ніяк не міг розібрати які саме кольори на них (але нормально побачив, коли зробив цю фотку для цього посту 🙂 ):

Тож просто взяв мультиметр, та поміряв ним:

Коли ж вже це все зробив, то стала зрозуміла і ця схема:

Тож резистор на фотці має:

  • червоний
  • червоний
  • чорний
  • чорний
  • коричневий

Тобто відповідно до схеми це буде “2 2 0 х1 1%” – 220 Ом, начебто вірно?

Тепер спробуємо це все підключити.

Breadboard для Arduino

Дістаємо breadboard (“макетна плата”).

Під капотом вона має такі з’єднання:

І шини для підключення живлення та компонентів:

Отже, нам потрібно таке підключення:

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

Сам LED довгою ніжкою підключаємо на сторону живлення (“+”), до резистора, короткою – до “землі”:

Далі – підключаємо на самій платі Arduino: чорний провід до GRD (Ground), червоний – до 13 pin – він у нас відповідає за роботу з LED (але з портами детальніше будемо розбиратись вже далі, див. Digital Pins):

Підключаємо Arduino до ноута (або підключаємо блок живлення), і…

Wow! It works! 🙂

Окей.

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

Loading

Arduino: перше знайомство та мій Arduino Starter Kit
0 (0)

3 Лютого 2024

Бути девопсом – то, звичайно круто – всі ці клауди, Терраформи, сесуріті і прочі дуже цікаві штуки.

Але я давно хотів спробувати і щось більш “реальне”, щось таке, щоб можна було потримати в руках, і цими ж руками зібрати.

В минулому році, коли думав про чергову підготовку до зими (див. Підготовка до зими 2023-2024: електрохарчування), знов згадав свою ідею мати вдома пожежну сигналізацію – бо на балконі стоять акумулятори. І, звісно, можна купити готові рішення від Ajax Systems, але ж можна і зробити самому!

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

Тож врешті-решт я таки вирішив почати знайомитись з Arduino. До того ж досвід роботи з мікроконтролерами може знадобитись, якщо доведеться мати справу з дронами (if you know what I mean 😉 ).

Ну і само собою – хіба ж можна займатись такими штуками, і не написати про на RTFM? 🙂 Тож додаю нову рубрику, і сподіваюсь, що буду її періодично оновлювати.

Arduino: the very beginning

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

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

Тож з того, що можна зробити – і що було б корисно вдома, аби мати більше мотивації:

  • система відеоспостереження
  • сигналізація пожежі/потопу
  • система автополиву квітів
  • робот-пилосос
  • система управління акваріумом

Та безліч всього іншого, бо датчиків для Arduino – просто море.

Як познайомитись з Arduino?

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

Мені з цим допомагають два основні матеріали:

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

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

Ще, звісно, є купа матеріалів у всяких блогах, на Youtube тощо – але до них поки не добрався.

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

Тож  час розпаковувати його – і пробувати щось сконектити.

Arduino Starter Kit

Список компонентів в моєму наборі прям дуже широкий:

  • 5 * Синій світлодіод
  • 5 * Червоний світлодіод
  • 5 * Жовтий світлодіод
  • 1 * RGB світлодіод
  • 8 * Резистор 220 Ом
  • 5 * Резистор 10 кОм
  • 5 * Резистор 1кОм
  • 1 * 10K потенціометр
  • 1 * Зумер (активний)
  • 1 * Зумер (пасивний)
  • 4 * Великий кнопковий перемикач
  • 2 * Датчик нахилу
  • 3 * Фоторезистор
  • 1 * Датчик полум’я
  • 1 * Датчик температури LM35
  • 1 * Регістр зсуву 74HC595N
  • 1 * 7-сегментний світлодіодний 1x модуль
  • 1 * 7-сегментний світлодіодний 4х модуль
  • 1 * 8 * 8 Світлодіодна матриця
  • 1 * 2×16 РК-дисплей
  • 1 * ІЧ-приймач
  • 1 * ІЧ-пульт дистанційного керування
  • 1 * Серводвигун
  • 1 * Кроковий модуль драйвера
  • 1 * Кроковий двигун
  • 1 * Модуль джойстика
  • 1 * Релейний модуль
  • 1 * Датчик руху PIR
  • 1 * Аналоговий датчик газу
  • 1 * Модуль акселерометра ADXL345
  • 1 * Ультразвуковий датчик HC-SR04
  • 1 * Модуль годинника реального часу на DS3231
  • 1 * Датчик температури і вологості DHT11
  • 1 * Датчик вологості грунту
  • 1 * RFID-модуль RC522
  • 1 * RFID-карта
  • 1 * RFID-ключ
  • 40 * Конектор
  • 1 * Макетна плата
  • 10 * Перемички мама-мама
  • 30 * Перемички тато-тато
  • 1 * 6-елементна AA акумуляторна батарея
  • 1 * Кабель USB

Arduino controller

Власне сама “ардуінка”, контролер – головна плата:

Хоча їх теж є багато і від різних виробників:

Але принципіально вони всі майже однакові:

Або в моєму випадку, з документації:

Головна його частина – мікроконтролер ATmega328P:

Для його програмування є Arduino IDE, але її ми встановимо на Linux та подивимось в наступній частині – Arduino: запуск Arduino IDE на Linux та “Hello, World!”.

Забігаючи наперед – для Arduino використовується Wiring – фреймворк зі спрощеним C++, але начебто можна писати і на самій С++ (буде привід згадати її).

Підключення Arduino до комп’ютера

Ну, що – спробуємо його включити?)

Живлення може бути прямо від USB:

Ваааау!)))

“It works!” (c)

Тепер можна починати щось робити.

Тож в наступній частині ми встановимо IDE на Linux, і заставимо нашу ардуінку блимкати LED.

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

Loading

Karpenter: моніторинг та Grafana dashboard для Kubernetes WorkerNodes
0 (0)

1 Лютого 2024

Маємо AWS EKS з Karpenter, який займається автоскелінгом EC2 – див. AWS: знайомство з Karpenter для автоскейлінгу в EKS, та встановлення з Helm-чарту.

В цілому проблем з ним поки не маємо, але в будь-якому разі потрібен його моніторинг, для чого Karpeneter “з коробки” надає метрики, які можемо використати в Grafana та Prometheus/VictoriaMetrics алертах.

Тож що будемо робити сьогодні:

  • додамо збір метрик до VictoriaMetrics
  • подивимось які метрики нам можуть бути корисні
  • додамо Grafana Dashboard для WorkerNodes + Karpenter

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

Окремо треба буде створити алерти – але це вже іншим разом. Маючи уяву про доступні метрики Karpenter та Prometheus-запити для графіків в Grafana проблем з алертами не має бути.

Поїхали.

Збір метрик Karpenter з VictoriaMetrics VMAgent

Наша VictoriaMetrics деплоїться власним чартом, див. VictoriaMetrics: створення Kubernetes monitoring stack з власним Helm-чартом.

Для додавання нового target оновлюємо values цього чарту – додаємо ендпоінт karpenter.karpenter.svc.cluster.local:8000:

...
  vmagent:
    enabled: true
    spec: 
      replicaCount: 1
      inlineScrapeConfig: |
        - job_name: yace-exporter
          metrics_path: /metrics
          static_configs:
            - targets: ["yace-service:5000"]
        - job_name: github-exporter
          metrics_path: /
          static_configs:
            - targets: ["github-exporter-service:8000"]     
        - job_name: karpenter
          metrics_path: /metrics
          static_configs:
            - targets: ["karpenter.karpenter.svc.cluster.local:8000"]  
...

Деплоїмо, і для перевірки таргету відкриваємо порт до VMAgent:

$ kk port-forward svc/vmagent-vm-k8s-stack 8429

Перевіряємо таргети:

Для перевірки метрик відкриваємо порт до VMSingle:

$ kk port-forward svc/vmsingle-vm-k8s-stack 8429

І шукаємо дані по запиту {job="karpenter"}:

Корисні метрики Karpenter

Тепер глянемо, які саме метрики нам можуть бути корисні.

Але перед тим, як розбиратись з метриками – давайте проговоримо основні поняття в Karpenter (окрім очевидних типу Node, NodeClaim або Pod):

  • controller: компонент Karpenter, який віподвідає за певний аспект його роботи, наприклад, Pricing controller відповідає за перевірку вартості інстансів, а Disruption Controller відповідає за керування процесом зміни стану WorkerNodes
  • reconciliation (“узгодження”): процес, коли Karpenter виконує узгодження бажаного стану (desired state) и реального (current state), наприклад – при появі Pod, для якого нема вільних ресурсів на існуючих WorkerNodes, Karpenter створить нову Node, на якій зможе запустись Pod, і його статус стане Running – тоді reconciliation процес статусу цього поду завершиться
  • consistency (“когерентність” або “узгодженість”): процес внутрішнього контролю і забезпечення відповідності необхідним параметрам (наприклад, перевірка того, що створена WorkerNode має диск розміром саме 30 GB)
  • disruption: процес зміни WorkerNodes в кластері, наприклад перестворення WorkerNode (для заміни на інстанс з більшою кількістю CPU або Memory), або видалення існуючої ноди, на якій нема запущених Pods
  • interruption: випадки, коли EC2 буде зупинено у зв’язку з помилками на hardware, виключення інстансу (коли робиться Stop або Terminate instance), або у випадку зі Spot – коли AWS “відкликає” інстанс; ці евенти йдуть на віподвідну SQS, звідки їх отримує Karpenter, щоб запустити новий інстанс на заміну
  • provisioner: компонент, який аналізує поточні потреби кластера, такі як запити на створення нових Pod, визначає, які ресурси потрібно створити (WorkerNodes), і ініціює створення нових (взагалі, Provisioner був замінений на NodePool, але окремі метрики по ньому залишились)

Тут я зібрав тільки ті метрики, які мені вважаються найбільш корисними в даний момент, але варто самому передивитись документацію Inspect Karpenter Metrics і є трохи більше деталей у документації Datadog:

Controller:

  • controller_runtime_reconcile_errors_total: кількість помилок при оновленні WorkerNodes (тобто в роботі Disruption Controller при виконанні операцій по Expiration, Drift, Interruption та Consolidation) – корисно мати графік або алерт 
  • controller_runtime_reconcile_total: загальна кількість таких операції – корисно мати уяву про активність Karpenter і, можливо, мати алерт, якщо це відбувається надто часто

Сonsistency:

  • karpenter_consistency_errors: виглядає як корисна метрика, але в мене вона пуста (принаймні поки що)

Disruption:

  • karpenter_disruption_actions_performed_total: загальна кількість дій по disruption (видалення/перестворення WorkerNodes), в лейблах метрик вказується disruption method – корисно мати уяву про активність Karpenter і, можливо, мати алерт, якщо це відбувається надто часто
  • karpenter_disruption_eligible_nodes: загальна кількість WorkerNodes для виконання disruption (видалення/перестворення WorkerNodes), в лейблах метрик вказується disruption method
  • karpenter_disruption_replacement_nodeclaim_failures_total: загальна кількість помилок при створенні нових WorkerNodes на заміну старим, в лейблах метрик вказується disruption method

Interruption:

  • karpenter_interruption_actions_performed: кількість дій за повідомленнями про EC2 Interruption (з SQS) – можливо має сенс, але в мене за тиждень збору метрик такого не траплялось

Nodeclaims:

  • karpenter_nodeclaims_created: загальна кількість створенних NodeClaims з лейблами по причині створення та відповідним NodePool
  • karpenter_nodeclaims_terminated: аналогічно, але по видаленним NodeClaims

Provisioner:

  • karpenter_provisioner_scheduling_duration_seconds: можливо, має сенс моніторити, бо якщо цей показник буде рости але буде надто великим – то це може бути ознакою проблем; проте, в мене за тиждень хістограма karpenter_provisioner_scheduling_duration_seconds_bucket незмінна

Nodepool:

  • karpenter_nodepool_limit: ліміт CPU/Memory NodePool, заданий в його Provisioner (spec.limits)
  • karpenter_nodepool_usage: використання ресурсів NodePool – CPU, Memory, Volumes, Pods

Nodes:

  • karpenter_nodes_allocatable: інформація по існуючим WorkerNodes – тип, кількість CPU/Memory, Spot/On-Demand, Availability Zone, etc
    • можна мати графік по кількості Spot/On-Demand інстансів
    • можна використовувати для отримання даних по доступних ресурсах ЦПУ/пам’яті – sum(karpenter_nodes_allocatable) by (resource_type)
  • karpenter_nodes_created: загальна кількість створенних нод
  • karpenter_nodes_terminated: загальна кількість видалених нод
  • karpenter_nodes_total_pod_limits: загальна кількість всіх Pod Limits (окрім DaemonSet) на кожній WorkerNode
  • karpenter_nodes_total_pod_requests: загальна кількість всіх Pod Requests (окрім DaemonSet) на кожній WorkerNode

Pods:

  • karpenter_pods_startup_time_seconds: час від сторення поду до його переходу в статус Running (сума по всім подам)
  • karpenter_pods_state: досить корисна метрика, бо в лейблах має статус поду, на якій він ноді запущений, неймспейс тощо

Cloudprovider:

  • karpenter_cloudprovider_errors_total: кількість помилок від AWS
  • karpenter_cloudprovider_instance_type_price_estimate: вартість інстансів по типам – можна на дашборді виводити вартість compute-потужностей кластеру

Створення Grafana dashboard

Для Grafana є готовий дашборд – Display all useful Karpenter metrics, але він якось зовсім не інформативний. Втім, з нього можна взяти деякі графіки та/або запити.

Зараз в мене є власна борда для перевірки статусу та ресурів по кожній окремій WorkerNode:

В графіках цієї борди є Data Links на дашборду з деталями по на дашборду з інформацією по конкретному Pod:

Графіки ALB внизу будуються з логів в Loki.

Тож що зробимо: нову борду, на якій будуть всі WorkerNodes, а в Data Links графіків цієї борди зробимо лінки на перший дашборд.

Тоді буде непогана навігація:

  1. загальна борда по всім WorkerNodes з можливістю перейти на дашборду з більш детальною інформацією по конкретній ноді
  2. на борді по конкретній ноді вже буде інформація по подах на цій ноді, і data links на борду по конкретному поду

Планування дашборди

Давайте поміркуємо, що саме ми б хотіли бачити на новій борді.

Фільтри/змінні:

  • мати змогу бачити всі WorkerNodes разом, або обрати одну чи кілька окремо
  • мати змогу бачити ресурси по конкретним Namespaces або Applications (в моєму випадку кожен сервіс має власний неймспейс, тому використаємо їх)

Далі, інформація по нодам:

  • загальна інформаця по нодам:
    • кількість нод
    • кількість подів
    • кількість ЦПУ
    • кількість Мем
    • spot vs on-deman ratio
    • вартість всіх нод за добу
  • відсотки від allocatable використано:
    • cpu – від pods requested
    • mem – від pods requested
    • pods allocation
  • реальне використання ресурсів – графіки по нодам:
    • CPU та Memory подами
    • кількість подів – процент від максимума на ноді
    • створено-видалено нод (by Karpenter)
    • вартість нод
    • процент EBS used
    • network – in/our byes/sec

По Karpenter:

  • controller_runtime_reconcile_errors_total – загальна кількість помилок
  • karpenter_provisioner_scheduling_duration_seconds – час створення подів
  • karpenter_cloudprovider_errors_total – загальна кількість помилок

Looks like a plan?

Поїхали творити.

Створення дашборди

Робимо нову борду, задаємо основні параметри:

Grafana variables

Нам потрібні дві змінні – по нодам і неймпспейсам.

Ноди можемо вибрати з karpenter_nodes_allocatable, неймспейси отримати з karpenter_pods_state.

Створюємо першу змінну – node_name, включаємо можливість вибору All або Multi-value:

Створюємо другу змінну – $namespace.

Щоб вибирати неймспейси тільки з обраних в фільтрі нод – додаємо можливість фільтру по $node_name яку створили вище і використовуємо регулярку “=~” – якщо нод буде обрано кілька:

Переходимо до графіків.

Кількість нод в кластері

Запит – використовуємо фільтр под обраним нодам:

count(sum(karpenter_nodes_allocatable{node_name=~"$node_name"}) by (node_name))

Кількість подів в кластері

Запит – тут фільтр і по нодам, і по неймспейсу:

sum(karpenter_pods_state{node=~"$node_name", namespace=~"$namespace"})

Кількість ядер CPU на всіх нодах

Частина ресурсів зайнята системою то ДемонСетами – вони у karpenter_nodes_allocatable не враховються. Можна перевірити запитом sum(karpenter_nodes_system_overhead{resource_type="cpu"}).

Тому можемо вивести або загальну кількість – karpenter_nodes_allocatable{resource_type="cpu"} + karpenter_nodes_system_overhead{resource_type="cpu"}, або тільки дійсно доступну для наших workloads – karpenter_nodes_allocatable{resource_type="cpu"}.

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

sum(karpenter_nodes_allocatable{node_name=~"$node_name", resource_type="cpu"}) + sum(karpenter_nodes_system_overhead{node_name=~"$node_name", resource_type="cpu"})

Загальний доступний об’єм пам’яті

JFYI:

  • SI standard: 1000 bytes in a kilobyte.
  • IEC standart: 1024 bytes in a kibibyte

Але давайте просто самі зробимо / 1024, і використаємо Кілобайти:

sum(sum(karpenter_nodes_allocatable{node_name=~"$node_name", resource_type="memory"}) + sum(karpenter_nodes_system_overhead{node_name=~"$node_name", resource_type="memory"})) / 1024

Spot instances – % від загальної кількості Nodes

Окрім нод створенних самим Karpenter у нас є окрема “дефолтна” нода, яка створються при створенні кластеру – для всяких controllers. Вона теж Spot (поки що), тож рахуймо і її.

Формула буде такою:

загальна сума нод = всі спот від karpenter + 1 дефолтна / на загальну кількість нод

Сам запит:

sum(count(sum(karpenter_nodes_allocatable{node_name=~"$node_name", nodepool!="", capacity_type="spot"}) by (node_name)) + 1) / count(sum(karpenter_nodes_allocatable{node_name=~"$node_name"}) by (node_name)) * 100

CPU requested – % від загального allocatable

Запит беремо з дефолтної борди Karpenter, трохи підпилюємо під свої фільтри:

sum(karpenter_nodes_total_pod_requests{node_name=~"$node_name", resource_type="cpu"}) / sum(karpenter_nodes_allocatable{node_name=~"$node_name", resource_type="cpu"})

Memory requested – % від загального allocatable

Аналогічно:

sum(karpenter_nodes_total_pod_requests{node_name=~"$node_name", resource_type="memory"}) / sum(karpenter_nodes_allocatable{node_name=~"$node_name", resource_type="memory"})

Pods allocation – % від загального allocatable

Скільки подів маємо від загальної ємності:

sum(karpenter_pods_state{node=~"$node_name", namespace=~"$namespace"} / sum(karpenter_nodes_allocatable{node_name=~"$node_name", resource_type="pods"})) * 100

Controller errors

Метрика controller_runtime_reconcile_errors_total включає в себе і контролери від VictoriaMetrcis, тож виключаємо їх через {container!~".*victoria.*"}:

sum(controller_runtime_reconcile_errors_total{container!~".*victoria.*"})

Cloudprovider errors

Рахуємо як рейт в секунду (з контролерами мабуть теж краще рейт, подивимось, як будуть помилки):

sum(rate(karpenter_cloudprovider_errors_total[15m]))

Nodes cost 24h – вартість всіх Nodes за добу

А от тут прям дуже цікаво вийшло.

По-перше – у AWS є дефолтні метрики білінгу від CloudWatch, але наш проект користується кредитами від AWS і ці метрики пусті.

Тому скористаємось метриками від Karpenter – karpenter_cloudprovider_instance_type_price_estimate.

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

Що ми маємо:

  • дефолта нода: з типом Spot, але створюється не Karpenter – можемо її ігнорувати
  • ноди, створені Karpenter: можуть бути або spot або on-demand, і можуть бути різних типів (t3.medium. c5.large, etc)

Спочатку нам треба отримати кількість нод по кожному типу:

count(sum(karpenter_nodes_allocatable) by (node_name, instance_type,capacity_type)) by (instance_type, capacity_type)

Отримуємо 4 spot, і один інстанс без лейбли capacity_type – бо це з дефолтної нод-групи:

Можемо його виключити з {capacity_type!=""} – він у нас один, без скейлінгу, можемо не враховувати, бо це тільки для CriticalAddons.

Для кращої картини візьмемо більший проміжок часу, бо там був ще t3.small:

Далі, у нас є метрика karpenter_cloudprovider_instance_type_price_estimate, використовуючи яку нам треба порахувати вартість всіх інстансів по кожному instance_type і capacity_type.

Запит буде виглядати так (дякую ChatGPT):

sum by (instance_type, capacity_type) (
    count(sum(karpenter_nodes_allocatable) by (node_name, instance_type, capacity_type)) by (instance_type, capacity_type)
    * on(instance_type, capacity_type) group_left 
    avg(karpenter_cloudprovider_instance_type_price_estimate) by (instance_type, capacity_type)
)

Тут:

  1. “внутрішній” запит sum(karpenter_nodes_allocatable) by (node_name, instance_type, capacity_type): рахується сума всіх CPU, memory тощо для кожної комбінації node_name, instance_type, capacity_type
  2. “зовнішній” count(...) by (instance_type, capacity_type): результат попереднього запиту рахуємо з count, щоб отримати кількість кожної комбінації – отримуємо кількіть WorkerNodes кожного instance_type та capacity_type
  3. другий запит – avg(karpenter_cloudprovider_instance_type_price_estimate) by (instance_type, capacity_type): повертає нам середню ціну по кожному instance_type та capacity_type
  4. використовуючи * on(instance_type, capacity_type): множимо кількість нод с запита номер 2 (count(...)) на результат с запита номер 3 (avg(...)) по співпадаючим комбінаціям метрик instance_type та capacity_type
  5. і самий перший “зовнішній” запит sum by (instance_type, capacity_type) (...): повертає нам суму по кожій комбінації

В результаті маємо такий графік:

Отже, що ми тут маємо:

  • 4 інстанси t3.medium та 2 t3.small
  • загальна вартість всіх t3.medium на годину виходить 0.074, всіх t3.small – 0.017

Для перевірки порахуємо вручну.

Спочатку по t3.small:

{instance_type="t3.small", capacity_type="spot"}

Виходить 0.008:

І по t3.medium:

{instance_type="t3.medium", capacity_type="spot"}

Виходить 0.018:

Тож:

  • 4 інстанси t3.medium по 0.018 == 0.072 usd/година
  • 2 інстанси t3.small по 0.008 == 0.016 usd/година

Все сходиться.

Залишилось все це зібрати разом, і вивести загальную вартість всіх серверів за 24 години – використаємо avg() і результат помножимо на 24 години:

avg(
  sum by (instance_type, capacity_type) (
    count(sum(karpenter_nodes_allocatable{capacity_type!=""}) by (node_name, instance_type, capacity_type)) by (instance_type, capacity_type)
    * on(instance_type, capacity_type) group_left 
    avg(karpenter_cloudprovider_instance_type_price_estimate) by (instance_type, capacity_type)
  )
) * 24

І в результаті все у нас зараз виглядає так:

Йдемо далі – до графіків.

CPU % use by Node

Тут вже використаємо дефолтні метрики від Node Exporter – node_cpu_seconds_total, але вони мають лейбли instance у вигляді instance="10.0.32.185:9100", а не node_name або node як у  метриках від Karpenter (karpenter_pods_state{node="ip-10-0-46-221.ec2.internal"}).

Тож щоб їх застосувати node_cpu_seconds_total з нашою змінною $node_name – додамо нову змінну node_ip, яку будемо формувати з метрики kube_pod_info з фільтром по лейблі node, де використовуємо нашу стару змінну node_name – щоб вибирати поди тільки з обраних в фільтрах нод.

Додаємо нову змінну, поки для перевірки не вимикаємо “Show on dashboard“:

І тепер можемо створити графік с запитом:

100 * avg(1 - rate(node_cpu_seconds_total{instance=~"$node_ip:9100", mode="idle"}[5m])) by (instance)

Але в такому випадку instance нам поверне результати як “10.0.38.127:9100” – а ми всюди використовуємо “ip-10-0-38-127.ec2.internal“. До того ж ми не зможемо додати data links, бо друга панель використовує формат ip-10-0-38-127.ec2.internal.

Тож ми можемо використати label_replace(), і переписати запит так:

100 * avg by (instance) (
    label_replace(
        rate(node_cpu_seconds_total{instance=~"$node_ip:9100", mode="idle"}[5m]), 
        "instance", 
        "ip-${1}-${2}-${3}-${4}.ec2.internal", 
        "instance", 
        "(.*)\\.(.*)\\.(.*)\\.(.*):9100"
    )
)

Тут label_replace отримує 4 агрументи:

  1. перший – метрика, над якою будемо виконувати трансформацію (результат rate(node_cpu_seconds_total))
  2. другий – лейбла, над якою ми будемо виконувати трансформацію – instance
  3. третій – новий формат value для лейбли – “ip-${1}-${2}-${3}-${4}.ec2.internal
  4. четвертий – ім’я лейбли, з якої ми будемо тримувати дані за допомогою regex

І останнім описуємо сам regex “(.*)\\.(.*)\\.(.*)\\.(.*):9100“, за яким треба отрмати кожен октет з IP 10.0.38.127, а потім кожен результат відповідно записати у ${1}-${2}-${3}-${4}.

Тепер маємо графік в такому вигляді:

Memory used by Node

Тут все аналогічно:

sum by (instance) (
  label_replace(
    (
      1 - (
        node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes
      )
    ) * 100,
    "instance", "ip-${1}-${2}-${3}-${4}.ec2.internal", "instance", "(.*)\\.(.*)\\.(.*)\\.(.*):9100"
  )
)

Pods use % by Node

Тут нам треба виконати запит між двома метриками – karpenter_pods_state та karpenter_nodes_allocatable:

(
  sum by (node) (kube_pod_info{node=~"$node_name", created_by_kind!="Job"})
  / 
  sum by (node) (kube_node_status_allocatable{node=~"$node_name", resource="pods"})
) * 100

Або ми можемо виключити нашу дефолтну ноду “ip-10-0-41-2.ec2.internal” і відобразити тільки ноди самого Карпентеру додавши вибірку по karpenter_nodes_allocatable{capacity_type!=""} – бо нам тут більше цікаво наскільки зайняті ноди, які створено самим Karpenter під наші аплікейшени.

Але для цього нам знабиться метрика karpenter_nodes_allocatable, в якій ми можемо перевірити наявність лейбли capacity_typecapacity_type!="".

Проте karpenter_nodes_allocatable має лейблу node_name а не node як в попередніх двух, тому ми знову можемо додати label_replace, і зробити такий запит:

(
  sum by (node) (kube_pod_info{node=~"$node_name", created_by_kind!="Job"})
  / 
  on(node) group_left
  sum by (node) (kube_node_status_allocatable{node=~"$node_name", resource="pods"})
) * 100
and on(node)
label_replace(karpenter_nodes_allocatable{capacity_type!=""}, "node", "$1", "node_name", "(.*)")

Тут в and on(node) ми використовуємо лейблу node в результатах запиту зліва (sum by()) і в результаті справа, щоб зі списку нод в karpenter_nodes_allocatable{capacity_type!=""} (тобто всі ноди, окрім нашої “дефолтної”) вибрати тільки ті, які є в результатх першого запиту:

EBS use % by Node

Тут вже простіше:

sum(kubelet_volume_stats_used_bytes{instance=~"$node_name", namespace=~"$namespace"}) by (instance) 
/ 
sum(kubelet_volume_stats_capacity_bytes{instance=~"$node_name", namespace=~"$namespace"}) by (instance) 
* 100

Nodes created/terminated by Karpenter

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

increase(karpenter_nodes_created[1h])

Та:

- increase(karpenter_nodes_terminated[1h])

Тут в функції increase() перевіряємо наскільки змінилося значення за годину:

А щоб позбутися цих “сходинок” – можемо додатково загорнути результат у функцію avg_over_time():

Grafana dashboard: фінальний результат

І все разом у нас тепер виглядає так:

Додавання Data links

Останім кроком буде додавання Data Links на графіки: потрібно додати лінку на іншу дашборду, по конкретній ноді.

Ця борда має такий URL: https://monitoring.ops.example.co/d/kube-node-overview/kubernetes-node-overview?var-node_name=ip-10-0-41-2.ec2.internal

Де в var-node_name=ip-10-0-41-2.ec2.internal задається ім’я ноди, по якій треба вивести дані:

Тож відкриваємо графік, знаходимо Data links:

Задаємо ім’я та URL – список всіх полів можна отримати по Ctrl+Space:

__field візьме дані з labels.node з результату запиту в панелі:

І сформує посилання у вигляді “https://monitoring.ops.example.co/d/kube-node-overview/kubernetes-node-overview?var-node_name=ip-10-0-38-110.ec2.internal“.

Ну і на цьому начебто все.

Loading

AWS: EKS Pod Identities – заміна IRSA? Спрощуємо менеджмент IAM доступів
5 (1)

15 Грудня 2023

Ще з дуже цікавих новинок останнього re:Invent – це EKS Pod Identities: нова можливість керувати доступами подів до ресурсів AWS.

The current state: IAM Roles for Service Accounts

До цього ми використовували модель IAM Roles for Service Accounts, IRSA, де для того, щоб якомусь поду дати доступ до, наприклад, S3, ми створювали IAM Role з відповідною IAM Policy, налаштовували її Trust Policy – щоб дозволити виконувати AssumeRole тільки з відповідвідного кластеру, потім створювали Kubernetes ServiceAccount, в annotations якого вказували ARN цієї ролі.

За такою схемою ми мали декілька “error prone” моментів:

  • найбільш розповсюджена проблема, з якою і я стикався прям ну дуже багато раз – помилки в Trust Policy, де треба було вказувати OIDC кластеру
  • помилки в самому ServiceAccount, де можна було помилитись в ARN ролі

Див. AWS: EKS, OpenID Connect та ServiceAccounts.

The f(ea)uture state: EKS Pod Identities

Проте тепер EKS Pod Identities дозволяє нам один раз створити IAM Role, ніяк її не обмежувати конкретним кластером, і підключати цю роль до подів (знову-таки – через ServiceAccount) прямо з AWS CLI, AWS Console чи через AWS API (Terraform, CDK, etc).

Як це виглядає:

  • в EKS додаємо новий контроллер – Amazon EKS Pod Identity Agent add-on
  • створюємо IAM Role, в Trust Policy якої тепер використовуємо Principal: pods.eks.amazonaws.com
  • і з AWS CLI, AWS Console чи через AWS API підключаємо цю роль напряму до потрібного ServiceAccount

Го тестити!

Створення IAM Role

Переходимо в IAM, створюємо роль.

В Trusted entity type вибираємо EKS і новий тип – EKS – Pod Identity:

В Permissions візьмемо вже існуючу політику на S3ReadOnly:

Задаємо ім’я ролі, і як раз тут і бачимо нову Trust Policy:

І давайте порівняємо її з Trust Policy для IRSA ролі:

Набагато простіше, а значить – менше варіантів для помилок, і взагалі простіше менеджити. До того ж, ми більше не зав’язані на Cluster OIDC Provider.

До речі, з EKS Pod Identities ми можемо використовувати і role session tags.

Окей, йдемо далі.

Amazon EKS Pod Identity Agent add-on

Переходимо до нашого кластеру, встановлюємо новий компонент – Amazon EKS Pod Identity Agent add-on:

Чекаємо хвилину – готово:

І поди цього контролера:

$ kk -n kube-system get pod | grep pod
eks-pod-identity-agent-d7448                    1/1     Running   0               91s
eks-pod-identity-agent-m46px                    1/1     Running   0               91s
eks-pod-identity-agent-nd2xn                    1/1     Running   0               91s

Підключення IAM Role до ServiceAccount

Переходимо в Access, і клікаємо Create Pod Identity Association:

Вибираємо роль, яку створили вище.

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

Аналогічно з ім’ям ServiceAccount – можна задати вже створенний SA, можно задати нове ім’я:

Створення Pod та ServiceAccount

Описуємо маніфест:

apiVersion: v1
kind: Namespace
metadata:
  name: ops-iam-test-ns
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ops-iam-test-sa
  namespace: ops-iam-test-ns
---
apiVersion: v1
kind: Pod
metadata:
  name: ops-iam-test-pod
  namespace: ops-iam-test-ns
spec:
  containers:
    - name: aws-cli
      image: amazon/aws-cli:latest
      command: ['sleep', '36000']
  restartPolicy: Never
  serviceAccountName: ops-iam-test-sa

Деплоїмо:

$ kubectl apply -f iam-sa.yaml    
namespace/ops-iam-test-ns created
serviceaccount/ops-iam-test-sa created
pod/ops-iam-test-pod created

І пробуємо доступ:

$ kk -n ops-iam-test-ns exec -ti ops-iam-test-pod -- bash
bash-4.2# aws s3 ls
2023-02-01 11:29:34 amplify-staging-112927-deployment
2023-02-02 15:40:56 amplify-dev-174045-deployment
...

Але якщо ми спробуємо іншу операцію, на яку ми не підключали політику, наприклад – EKS, то отримаємо 403:

bash-4.2# aws eks list-clusters

An error occurred (AccessDeniedException) when calling the ListClusters operation: User: arn:aws:sts::492***148:assumed-role/EKS-Pod-Identities-test-TO-DEL/eks-atlas-eks--ops-iam-te-cc662c4d-6c87-44b0-99ab-58c1dd6aa60f is not authorized to perform: eks:ListClusters on resource: arn:aws:eks:us-east-1:492***148:cluster/*

Проблеми?

Наразі я бачу одну потенційну не проблему, але питання, яке варто мати на увазі: якщо раніьше ми налаштовували доступ на рівні сервісу, то з EKS Pod Identities це робиться на рівні управління кластером.

Тобо: в мене є сервіс, Backend API. В нього є власний репозиторій, в якому є каталог terrafrom, в якому створюються необхідні IAM-ролі.

Далі, є каталог helm, в якому маємо маніфест з ServiceAccount, в якому в анотаціях через змінні передається ARN цієї IAM ролі.

І на цьому все – мені (точніше – CI/CD пайплайну, який виконує деплой) потрібен доступ тільки до IAM, потрібен доступ в EKS на створення Ingress, Deployment та ServiceAccount.

Але тепер треба буде думати як давати доступ ще й до EKS на рівні AWS, бо треба буде виконувати додаткову операцію в AWS API на Create Pod Identity Assosiaction.

До речі, в Terraform вже новий ресурс для цього – aws_eks_pod_identity_association.

Проте виглядає дійсно класно, і може дуже спростити життя по менеджменту EKS та IAM.

EKS Pod Identity restrictions

Варто звернути увагу на документацію, бо в EKS Pod Identity restrictions говориться, що EKS Pod Identities доступна тількина Amazon Linux:

EKS Pod Identities are available on the following:

Loading

AWS: CloudWatch – Multi source query: збираємо метрики із зовнішнього Prometheus
0 (0)

13 Грудня 2023

Ще один цікавий анонс з останнього re:Invent – це те, що в CloudWatch додали можливість збирати метрики із зовнішніх ресурсів (див. дуже цікавий доклад AWS re:Invent 2023 – Cloud operations for today, tomorrow, and beyond (COP227)).

Тобто тепер ми можемо створювати графіки та/або алерти не тільки з дефолтних метрик самого CloudWatch – але й за допомогою конекторів для CloudWatch підключити збір метрик з Amazon Managed Service for Prometheus, звичайного Prometheus, Amazon OpenSearch Service, Amazon RDS для MySQL та PostgreSQL, CSV файлів з S3 бакетів і навіть з Microsoft Azure Monitor.

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

Підключення метрик з vanilla Prometheus

Є Prometheus на голому EC2 інстансі, на якому відкрито порт 9090 – спочатку спробуємо тут, потім глянемо на VictoriaMetrcis в EKS.

Переходимо в CloudWatch Metrics, тепер маємо нову вкладку Multi source query:

Вибираємо Prometheus:

Задаємо параметри.

Поля логін-пароль обов’язкові, тож навіть якщо Prometheues не потребує аутентифікації – задаємо тут якісь значення.

Нижче можна налаштувати параметри мережі. Наприклад, якщо Prometheus доступний тільки всередені VPC – то тут можемо вибрати VPC і сабнети:

Клілкаємо Create data source – в CloudWatch запустить створення CloudFormation стеку, в якому створить Lambda-функції, які власне і будуть збирати дані з нашого дата-сорсу:

Повертаємось до CloudWatch, де тепер маємо новий дата-сорс:

І можемо з нього отримати метрики:

VictoriaMetrics, EKS та VPC

Зараз маємо VictoriaMetrics в Kubernetes, див. VictoriaMetrics: створення Kubernetes monitoring stack з власним Helm-чартом.

Щоб CloudWatch міг збирати метрики – нам треба відкрити доступ до VMSingle.

Тут маємо два варіанти – або звичайний Ingress/ALB, див. values.yaml, або через VMAuth з аутентифікацією, див. VictoriaMetrics: VMAuth – проксі, аутентифиікація та авторизація.

І при додаванні data source в CloudWatch єдина відмінність від звичайного Prometheus – це URI:

  • у Prometheues ми ходимо на URI hostname:9090/api/v1/labels
  • у VicrotiaMetrcis з VMSingle – через hostname:8429/prometheus/api/v1/labels

Тож в data source додаємо як https://vmsingle.ops.example.co/prometheus/.

Редагування і видалення data source

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

Схоже що поки що видаляється дата-сорс тільки через CloudFormation – видалення стеку, а змінити якісь параметри можна тільки в самій Lambda-функції.

Наприклад, щоб відредагувати Prometheus URL – він задається в змінних оточення функції:

Але anyway – виглядає це все дуже прикольно.

Loading

AWS: Amazon Q – знайомство, можливості та перші враження
0 (0)

8 Грудня 2023

В цікаві часи живемо.

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

Сам Amazon його називає “AI-powered assistant”, по факту для нас, як інженерів, це просто chatbot, з яким ми можемо поговорити, щоб отримати допомогу у розв’язанні якихось проблем або для отримання рекомендації для налаштування сервісу. Для бізнесу ж він може мабуть багато іншого, але нам він цікавий, як асистент по роботі з AWS.

Під капотом Amazon Q використовує Amazon Bedrock, тож щоб краще розуміти що таке Q – давайте глянемо і на Bedrock.

Amazon Bedrock

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

Отже, AWS Bedrock – це managed сервіс від AWS, який дозволяє будувати ваші АІ-powered сервіси, використовуючи Foundation Models (FM) від Amazon, Мета, Amazon, Stability AI, та інших:

Див. What is a Foundation Model?

Для роботи з FL Bedrock надає єдиний API, і вам не потрібно будувати ніякої інфраструктури для запуску моделей.

Крім того, ви можете розширяти базу знань Bedrock за рахунок власних баз знань (“Knowledge base“). При цьому ваші дані не будуть об’єднані з самим FM, тобто повністю зберігається всяка privacy (включаючи підтримку стандартів GDPR, HIPAA).

По Bedrock варто глянути доповідь AWS re:Invent 2023 – Build your first generative AI application with Amazon Bedrock (AIM218) – там і про сам сервіс, і його архітектуру, і демо, і взагалі трохи розглядаються базові поняття – Machine Learning, Deep Learning, Generative AI.

Amazon Q

Тож Amazon Q – це система поверх Bedrock (я поки не знайшов інформації, яка саме модель використовується, але, мабуть, Titan – бо це система від самого Amazon).

По факту це чат-бот, з яким ми можемо поговорити, і який вже нам доступний в AWS Console справа, де ми звикли бачити розділ допомоги:

Хоча місцями Q не може відповісти навіть на прості питання 🙂

При цьому і сам Q, і Bedrock заточені під приватність даних, тому начебто можна спокійно (нєт) підключати їх до корпоративних даних – наприклад, до Git-репозиторію, або Atlassian Confluence, і тоді Q при формуванні відповіді буде використовувати дані з цих джерел для формування відповідей.

Чому “нєт?” Бо поки система в Preview, і про неї вже пишуть, що:

Knowledge baseQ is “experiencing severe hallucinations and leaking confidential data,”

Див. Amazon’s Q has ‘severe hallucinations’ and leaks confidential data in public preview, employees warn.

Проте я думаю, що це “дитячі хвороби”, і з часом Amazon все пофіксить.

Тож якщо стисло, то Q нам може допомогти у:

  • налаштуванні нової системи, або виборі архітектури
  • вирішенні проблем з сервісами
  • працювати з нашим кодом, написанні тестів

Amazon Q доступний у:

  • AWS Console
  • в його документації
  • в різних IDE (далі подивимось на VSCode)
  • Slack (через AWS Chatbot або Slack gateway)
  • в сервісах Amazon – наприклад, CodeWhisperer (аналог GitHub Copilot?)

Amazon Q vs ChatGPT

Мабуть, чи найперше питання, яке приходить в голову.

Знову-таки, чисто моє IMHO:

  • Amazon Q – це про бізнес: якщо ChatGPT це так би мовити “AI-чатбот загального призначення”, то Q може тісно інтегруватися з вашим бізнесом – вашими даними, користувачами тощо
  • Amazon Q – це про безпеку даних: нам обіцяють дуже потужні інструменти по обмеженню доступів: наприклад, якщо до Q буде підключена Confluence, то при формуванні відповіді на запит юзера будуть перевірятись його права в Confluence, і не будуть використані дані, до яких він там не має доступу (цікаво, як це реалізовано і як воно буде працювати, але поки просто маємо на увазі)
  • Amazon Q – це про інтеграцію:
    • модель, яка використовується для відповідей навчалася на даних самого AWS, яких за 17 років існування набралось багато – в тому числі мабуть і якісь дані, котрих зазвичай нема у відкритому доступі, тому по ідеї – Q в деяких моментах може давати більш точні відповіді (знову-таки – може не відразу, поки воно ще в Preview)
    • Q інтегрований з самими сервісами AWS, і ви можете використовувати його прямо в QuickSighe, або, як вже казалось, в самій AWS Console

(оці три пункта звучать так, наче Amazon мені заплатили за цей пост :-D)

Amazon Q pricing

Як завжди у AWS – “всьо сложно” 🙂

По-перше, поки система в Preview – більша частина можливостей безкоштовні.

По-друге – доступні будуть два різних плана: Business і Builder.

Якщо коротко, то Business – це більше про якісь маркетингові штуки на кшталт чат-бота для допомоги співробітникам у вирішення якихось питань, або такий собі “internal Google” – але з доступом до внутрішніх баз даних.

А от Builder, судячи з документації – це як раз те, що буде цікаво нам, як інженерам, бо саме в цьому плані буде доступ до “over 17 years’ worth of AWS knowledge and experience building in the cloud, including best practices, well-architected patterns“.

Див. Amazon Q pricing.

Amazon Q in AWS Console

Спробуємо потраблшутити з Amazon Q – створимо EC2, зламаємо нетворкінг, і спитаємо Q.

Створюємо інстанс, дозволяємо SSH:

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

$ ssh 16.170.217.131
The authenticity of host '16.170.217.131 (16.170.217.131)' can't be established.
...

Далі, редагуємо SecurityGroup – видаляємо доступ SSH:

І питаємо Amazon Q:

На що він відповідає, що може спробувати проаналізувати проблему з AWS VPC Reachability Analyzer. Спробуємо – переходимо за посиланням, і:

Ну, я все ж очікував, що Q прям зможе проаналузвати конфіг нетворкінгу EC2, і побачить, що там проблема в самій SecurityGroup. Але це я багато, мабуть, хочу)

Втім вже те, що він так інтегрується з системами типу VPC Reachability Analyzer – непогано, і далі, сподіваюсь, буде ще краще.

Amazon Q та інтеграція з VSCode

Документація – Set up Amazon Q in your IDE.

Встановлюємо AWS Toolkit:

Проходимо аутентифікацію – клікаємо Use free with AWS Builder ID:

Відкриється вікно в браузері, там підтверджуємо код, і вказуємо свою пошту – можна будь-яку, не обов’язкового ту, яка використовується в AWS акаунті:

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

Дозволяємо доступ:

І тепер маємо підключений Amazon Q в нашому VSCode:

Задаємо питання – і він просканує відкритий у редакторі код, і на основі нього видасть відповідь:

Але питання розуміє не завжди коректно: в цілому він має підтримувати контекст розмови і враховувати відкритий код, але ось маємо відкритий код Terraform і пов’язане питання перед цим, а він дає відповідь про AWS Console:

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

На доповіді на самому re:Invent показували демо роботи з IDE, і там був приклад, як Q допомагає створювати код для AWS CDK та Python, і там виглядає прям дуже круто, бо я влітку цього року намучився з ChatGPT, який постійно видавав приклади для старої версії CDK або старих версій бібліотек (див. AWS: CDK – створення EKS з Python та загальні враження від CDK).

Amazon Q Application

Ми також можемо створити Application в AWS Console, і там використати власні дата-сорси, на основі яких Q буде формувати відповіді.

Плюс, там жеж можемо створити і веб-інтерфейс для юзерів:

Створюємо:

Далі налаштовується Retriever – яким чином Q буде отримувати дані з дата-сорса:

І останнім налаштовується вже сам дата-сорс через вибір конекторів – а їх тут дуже багато:

Візьмемо Web crawler:

Тут вже налаштовується і аутентифікація – і саме тут вочевидь будуть перевірятись user permissions – те, про що я вписав на початку, що Amazon Q не дасть інформації юзеру, якщо він не має до неї доступу в самому дата-сорсі:

І решта налаштувань – там і VPC, і IAM, параметри синхронізації тощо:

Робимо синхронізацію з нашим дата-сорсом:

І далі вже можемо відкрити веб-інтерфейс, і початитись з ботом:

А якщо клікнути Deploy web experience – то треба буде налаштовувати SAML:

І воно наче і має працювати без деплою, в Web preview режимі, але в мене воно просто зависало на запиті:

Можливо тому, що не закінчена синхронізація з дата-сорсом, але процес синхронізації з RTFM затягнувся на кілька годин, хоча тут всього-то близько 1500 постів, плюс всякі медіафайли типу скріншотів. Мабуть, вже не дочекаюсь її завершення, але якщо після закінчення синхронізації запрацює – то цю частину тут оновлю.

Висновки та враження від Amazon Q

Я не сказав би, що сервіс прям “producation ready” – але він дійсно ще в Preview, а тому багато чого буде допилюватись/фікситись/апгрейдитись, і тут все ще є над чим попрацювати, бо наразі вона виглядає трохи сирою – Amazon явно поспішали випустити її хоча б в Preview, бо вся ця “гонка Generative AI” і вот ето вот всьо.

Але в цілому система виглядає доволі перспективною, особливо, як вона навчиться толком допомагати траблшутити якісь проблеми в AWS.

Ну і головне – її дійсно можна буде використовувати різними бізнесами, інтегруючи зі своїми системами та базами даних, і не переживати за всякий privacy complience – а з ChartGPT це зараз дуже велика проблема.

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

Loading

Kubernetes: забезпечення High Availability для Pods
0 (0)

20 Листопада 2023

Що маємо: є у нас Kubernetes cluster, на якому скейлінгом WorkerNodes займається Karpenter, який для NodePool має  параметр disruption.consolidationPolicy=WhenUnderutilized, тобто він буде намагитись “ущільніти” розміщеня подів на нодах так, щоб максимально ефективно використати ресурси CPU та Memory.

В цілому все працює, але це призводить до того, що досить часто перестворюються WorkerNodes, а це викликає “переселення” наших Pods на інші ноди.

Тож задача зараз зробити так, щоб скейлінг і процес consolidation не викликав перебоїв в роботі наших сервісів.

Загалом це тема не стільки про сам Karpenter, скільки про забезпечення стабільності роботи подів в Kubernetes загалом, але зараз я детально зайнявся цим питанням саме через Karpenter, тому будемо трохи говорити і про нього.

Karpenter Disruption Flow

Щоб краще розуміти, що відбувається з нашими подами, давайте коротко глянемо як Karpenter виводить з пулу WorkerNode. Див. Termination Controller.

Після того, як Karpenter виявив, що є ноди, які треба термінейтити, він:

  1. ставить на ноду Kubernetes finalizer
  2. ставить на такую ноду taint karpenter.sh/disruption:NoSchedule щоб Kubernetes не створював нових подів на цій ноді
  3. при необхідності створює нову ноду, на яку буде переносити поди з ноди, яка буде виведена з роботи (або викорстає ноду яка вже є, якщо вона може прийняти додаткові поди відповідно до їх requests)
  4. виконує Pod Eviction подів з ноди (див. Safely Drain a Node та API-initiated Eviction)
  5. після того, як з ноди всі поди окрім DaemonSets видалені, Karpenter видаляє відповідний NodeClaim
  6. видаляє finalizer ноди, що дозволяє Kubernetes виконати видалення цієї ноди

Kubernetes Pod Eviction Flow

І коротко процес того, як сам Kubernetes виконує “виселення” поду:

  1. API Server отримує Eviction request і виконує перевірку – чи можна цей под виселити (наприклад – чи не порушить його видалення обмежень якогось PodDisruptionBudget)
  2. відмічає ресурс цього поду на видалення
  3. kubelet починає процес gracefully shut down – тобто відправляє сигнал SIGTERM
  4. Kubernetes видаляє IP цього поду зі списку ендпоінтів
  5. якщо под не закінчив роботи на протязі заданого – то kubelet відправляє сигнал SIGKILL, щоб вбити процес негайно
  6. kubelet відправляє сигнал API Server, що под можна видаляти зі списку об’єктів
  7. API Server видаляє под з бази

Див. How API-initiated eviction works та Pod Lifecycle – Termination of Pods.

Kubernetes Pod High Availability Options

Тож що ми можемо зробити з подами, щоб наш сервіс працював незалежно від роботи Karpenter і взагалі стабільно і “бєз єдіного разрива” (с) ?

  • мати мінімум по 2 поди на критичних сервісах
  • мати Pod Topology Spread Constraints, щоб Pods розміщались на різних WorkerNodes – тоді якщо вбивається одна нода з одним подом – інший под на іншій ноді залишиться живим
  • мати PodDisruptionBudget, щоб мінімум 1 под був завжди живий – це не дасть Karpenter виконати evict всіх подів відразу, бо він слідкує за додтриманням PDB
  • і щоб гарантовано не дати виконати Pod Eviction – можемо задати поду анотацію karpenter.sh/do-not-disrupt – тоді Karpenter буде ігнорувати таки поди (і, відповідно, ноди, на яких буде запущено такий под)

Kubernetes Deployment replicas

Саме просте і очевидне рішення – це мати як мінімум 2 одночасно працюючих поди.

Хоча це не гарантує, що Kubernetes не виконає їхній eviction одночасно, але це мінімальна умова для подальших дій.

Тож або виконуємо руками kubectl scale deployment --replicas=2, або оновлюємо поле replicas в Deployment/StatefulSets/ReplicaSet (див. Workload Resources):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
    spec:
      containers:
        - name: nginx-demo-container
          image: nginx:latest
          ports:
            - containerPort: 80

Pod Topology Spread Constraints

Більш детально описував у Pod Topology Spread Constraints, але якщо коротко, то ми можемо задати правила розміщення Kubernetes Pod так, щоб вони знаходились на різних WorkerNodes. Таким чином коли Karpenter захоче вивести одну ноду з роботи – то у нас залишиться под на іншій ноді.

Проте ніхто не завадить Karpenter-у виконати drain обох нод відразу, тож і це не є 100% гарантією, але це друга умова для забезпечення стабільності роботи нашого сервісу.

Крім того, з Pod Topology Spread Constraints, ми можемо задати розміщення подів у різних Availabilty Zones, що є фактичного must have опцією при побудові High-Availabiltiy архітектури.

Тож додаємо до нашого деплойменту topologySpreadConstraints:

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

І тепер обидва поди мають бути розміщені на різних WorkerNodes:

$ kk get pod -l app=nginx-demo -o json | jq '.items[].spec.nodeName'
"ip-10-1-54-144.ec2.internal"
"ip-10-1-45-7.ec2.internal"

Див. також Scaling Kubernetes with Karpenter: Advanced Scheduling with Pod Affinity and Volume Topology Awareness.

Kubernetes PodDisruptionBudget

За допомогою PodDisruptionBudget ми можемо задати правило на мінімальну кількість доступних або максимальну кількість недоступних подів. Значення може бути як у виді числа, так і у виді відсотка від загальної кількості подів в replicas для Deployment/StatefulSets/ReplicaSet.

У випадку з Deployment в якому маємо два поди і який має topologySpreadConstraints по різним WorkerNodes це дасть гарантію того, що Karpenter не виконає Node Drain двох WorkerNdoes одночасно. Натомість він “переселить” спочатку один под, вб’є його ноду, а потім повторить процес для іншої ноди.

Див. Specifying a Disruption Budget for your Application.

Створимо PDB для нашого деплойменту:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
    spec:
      containers:
        - name: nginx-demo-container
          image: nginx:latest
          ports:
            - containerPort: 80
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: nginx-demo
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: nginx-demo-pdb
spec:
  minAvailable: 50%
  selector:
    matchLabels:
      app: nginx-demo

Деплоїмо і перевіряємо:

$ kk get pdb
NAME             MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
nginx-demo-pdb   50%             N/A               1                     21s

Анотація karpenter.sh/do-not-disrupt

Окрім налаштувань на стороні Kubernetes, ми можемо явно задати заборону на видаленя поду самому Karpenter через додавання анотації karpenter.sh/do-not-disrupt (раніше, до Beta, це були анотації karpenter.sh/do-not-evict та karpenter.sh/do-not-consolidate).

Це може знадобитись наприклад для подів, які мають бути запущені в одному екземплярі (як VictoriaMetircs VMSingle instance), і які небажано зупиняти.

Для цього в template цього поду додаємо annotation:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
      annotations:
        karpenter.sh/do-not-disrupt: "true"        
    spec:
      containers:
        - name: nginx-demo-container
          image: nginx:latest
          ports:
            - containerPort: 80
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: nginx-demo

Див. Pod-Level Controls.

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

Loading