AWS: CDK – знайомство та приклади на Python
0 (0)

12 Травня 2023

AWS Cloud Development Kit (AWS CDK) дозволяє описувати інфрастуктуру використовуючи мови програмування TypeScript, JavaScript, Python, Java, C# або Go.

“Під капотом” створює CloudFormation стек, в якому створються ресурси, описані в вашому коді.

Відповідь на питання “Нашо CDK, коли є Terraform?” можна знайти ось тут – 4 ultimate reasons to prefer AWS CDK over Terraform.

Але так як я поки CDK не користувався, то про переваги та недоліки казати нічого не буду.

Єдине, на що відразу можна звернути увагу, це те, ще по-перше – нема state-файлів, як у Terraform, які, звістно, корисні, але додають трохи болю при менеджменті, по-друге – сам CloudFortaion, який має свої недоліки та “це не баг, а фіча”, зато маємо можливість у веб-інтерфейсі AWS Console побачити всі ресурси.

Взагалі, до AWS CDK прийшов тому, що на новому проекті він вже використовується, тож перш ніж тягнути в проект Terraform, треба розібратись з тим, що тут вже є.

UPD: Ну, нє, таки скажу. Виглядає наче й непогано і цікаво, бо “нарешті Python!”, але:

  • всеж HCL-код виглядає куди більш лаконічно та зрозуміло
  • прикладів та й банально результатів пошуку у Гуглі по Terraform набагто більше, а це значить і швидкість виконання задачі більша
  • ну й… дойшов до того, що треба було створити SES домен, і… І нічого. Дуже неочікувано, але в стандартних Construtcs нічого толком не зайшов, а єдиний більш-менш конструкт з Construct Hub мав приклади тільки для TypeScript, навіть у PyDoc. Таке собі задоволення, чесно кажучи

Окрім CDK для самого AWS, є також cdk8s для Kubernetes та CDKTF для Terraform.

Окей, давайте знайомитись з AWS CDK.

Key concepts

Почати можна з документації AWS Getting started with the AWS CDK або переглянути непоганий відео-туторіал на 20 хвилин Getting Started with AWS CDK and Python.

Отже, основні поняття, якими будемо оперувати при роботі з AWS CDK:

  • App: App являє собою такий собі “контейнер”, в якому ми описуємо наш застосунок, і може мати в собі один або більше Stacks (які потім будут сформовані у CloudFormation Stacks). Див. Apps.
  • Stack: зі Stack буде формуватись CloudFormation Stack або change set. У самому Stack на рівні коду ми описуємо те, які саме ресурси в цьому стеку будуть створені, а ресурси описуємо, використовуючи Constructs. Див. Stacks.
  • Construct: основні “будівельні блоки” у CDK, в яких описуються компоненти, которі необхідно створити в AWS. Див. Construct.

На Construct зупинимось трохи детальніше, бо вони розподіляють на три основних групи:

  • AWS CloudFormation-only або L1 (“layer 1”): тут маємо ресурси, які описані і підтримуються самим CloudFormation, і всі такі ресурси мають имена з префіксом Cfn, наприклад, для AWS S3 бакетів це CfnBucket. Всі ці ресурси знаходяться у модулі aws-cdk-lib.
  • Curated або L2: ці конструкти розроблені командою AWS CDK для спрощення управління інфрастуктурою. Як правило, вони включать в себе ресурси з L1 з деякими дефолтними значеннями та політиками безпеки. Ресурси у aws-cdk-lib готові до використання у production, а якщо ресурс являє собою окремий модуль – то він ще або у стані розробки, або є experimental.
  • Patterns або L3: Patterns включають в себе декілька ресурсів, які дозволяють побудувати всю архітектуру під конкретний use case. Так само як і з L2 ресурсами, готові до продакшену модулі включені в модуль aws-cdk-lib, а ті, що знаходяться у стані розробки – являть собою окремі модулі.

Окрім AWS Construct Library є ще й Construct Hub, в якому можна знайти модулі від партнерів AWS.

Встановлення AWS CDK

Для роботи з AWS CDK, навіть якщо ви будете писати на Python, потрібна Node.js, так як всі мови програмування на CDK будуть працювати через бекенд на Node.js.

Для роботи з CDK маємо CLI, через яку можемо створювати нові App, генерувати CloudFormation темплейти, виконувати diff між нашим кодом і існуючими CloudFormation стеками та багато іншого.

Встановлюємо сам CDK – бекенд та CLI:

[simterm]

$ npm install -g aws-cdk
added 1 package, and audited 2 packages in 1s

[/simterm]

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

[simterm]

$ cdk --help
Usage: cdk -a <cdk-app> COMMAND

Commands:
  cdk list [STACKS..]             Lists all stacks in the app      [aliases: ls]
  cdk synthesize [STACKS..]       Synthesizes and prints the CloudFormation
                                  template for this stack       [aliases: synth]
  cdk bootstrap [ENVIRONMENTS..]  Deploys the CDK toolkit stack into an AWS
                                  environment
  cdk deploy [STACKS..]           Deploys the stack(s) named STACKS into your
                                  AWS account
...

[/simterm]

Цікавості заради – куди веде сам файл cdk:

[simterm]

$ which cdk
/home/setevoy/.nvm/versions/node/v16.18.0/bin/cdk

[/simterm]

Ага, Node.js.

Authentication with AWS

Документація – Authentication and access:

  • AWS SSO: використовууючи SSO-сессію через AWS CLI конфіг (~/aws/config), див. IAM Identity Center authentication
  • AWS EC2 Instance IAM Role: якщо код CDK запускається всередені AWS, наприклад з EC2, то до інстансу можна підключити IAM roles for Amazon EC2
  • AWS IAM User Access/Secret keys: ну і звичні ключі доступу для AWS CLI з профілем у файлах ~/aws/config та ~/.aws/credentials

Створення проекту

cdk init

За допомогою cdk init можемо згенерувати шаблон файлів та директорій.

Щоб отримати допомогу по конкретній команді, наприклад для init – додайте --help після неї, тобто cdk init --help:

Тут з цікавих опцій можуть бути наступні:

  • --verbose: більш детальний аутпут
  • --debug: ще більш детальний
  • --role-arn: використовувати IAM Role (див. AWS: IAM AssumeRole – описание, примеры)
  • --language: мова програмування, яка буде використовуватись при створенні проекту
  • --list: отримати список доступних шаблонів

Спробуємо list:

[simterm]

$ cdk init --list
Available templates:
* app: Template for a CDK Application
   └─ cdk init app --language=[csharp|fsharp|go|java|javascript|python|typescript]
* lib: Template for a CDK Construct Library
   └─ cdk init lib --language=typescript
* sample-app: Example CDK Application with some constructs
   └─ cdk init sample-app --language=[csharp|fsharp|go|java|javascript|python|typescript]

[/simterm]

Тут можемо створити шаблон для App, бібліотеки для Construct Library, або створити sample-app, тобто приклад App, в якому вже будуть додані якісь Constructcs.

Створюємо директорію нашого проекту:

[simterm]

$ mkdir cdk-example && cd cdk-example

[/simterm]

Запускаємо init sample-app:

[simterm]

$ cdk init sample-app --language=python
Applying project template sample-app for python
...
Initializing a new git repository...
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint: 
hint:   git config --global init.defaultBranch <name>
hint: 
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint: 
hint:   git branch -m <name>
Please run 'python3 -m venv .venv'!
Executing Creating virtualenv...
✅ All done!

[/simterm]

Глянемо структуру файлів та директорій проекту:

[simterm]

$ tree .
.
|-- README.md
|-- app.py
|-- cdk.json
|-- cdk_example
|   |-- __init__.py
|   `-- cdk_example_stack.py
|-- requirements-dev.txt
|-- requirements.txt
|-- source.bat
`-- tests
    |-- __init__.py
    `-- unit
        |-- __init__.py
        `-- test_cdk_example_stack.py

4 directories, 11 files

[/simterm]

Python virtualenv та встановлення модулів AWS CDK

source.bat – скрипт для Windows для створення Python virtualenv, який має викликати файл .venv\Scripts\activate.bat:

[simterm]

$ tail -1 source.bat 
.venv\Scripts\activate.bat

[/simterm]

Але так як я це роблю на Linux, то каталогу .venv\Scripts намє взагалі, натомість маємо набор скриптів у .venv/bin/ (ну теж якось виглядає… AWS CDK начебто серйозний проект, але таку дрібницю, як скрипти, зроблено через якось… недбало?):

[simterm]

$ ls -1a .venv/bin/
.
..
Activate.ps1
activate
activate.csh
activate.fish
pip
pip3
pip3.11
python
python3
python3.11

[/simterm]

Для Linux використовуємо .venv/bin/activate, який являею собой shell-команди для створення та налаштування змінних оточення:

[simterm]

$ . .venv/bin/activate
(.venv)

[/simterm]

Щоб перевірити, що ми справді у virtualenv можна перевірити значення змінної $VIRTUAL_ENV, яка має шлях до каталогу поточного віртуального оточення, в якому будуть необхідні бібліотеки:

[simterm]

$ echo $VIRTUAL_ENV
/home/setevoy/Scripts/AWS_CDK/.venv

[/simterm]

Далі, встановлюємо залежності – модулі aws-cdk-lib та constructs:

[simterm]

$ pip install -r requirements.txt
...
Collecting aws-cdk-lib==2.78.0 
 Using cached aws_cdk_lib-2.78.0-py3-none-any.whl (41.0 MB) 
Collecting constructs<11.0.0,>=10.0.0 
 Using cached constructs-10.2.18-py3-none-any.whl (58 kB)
...

[/simterm]

Файл app.py

Перевіримо зміст app.py, який являє собою основу нашого проекту, бо саме з нього CDK почне свою роботу:

#!/usr/bin/env python3

import aws_cdk as cdk

from cdk_example.cdk_example_stack import CdkExampleStack


app = cdk.App()
CdkExampleStack(app, "cdk-example")

app.synth()

Першим виконуємо import aws_cdk as cdk – імпортуємо модуль aws_cdk dir, який знаходиться у каталозі .venv:

[simterm]

$ find . -name aws_cdk
./.venv/lib/python3.11/site-packages/aws_cdk

[/simterm]

Далі, from cdk_example.cdk_example_stack import CdkExampleStack – імпортуємо клас CdkExampleStack(Stack) з модулю cdk_example_stack.py:

from constructs import Construct
from aws_cdk import (
    Duration,
    Stack,
    aws_iam as iam,
    aws_sqs as sqs,
    aws_sns as sns,
    aws_sns_subscriptions as subs,
)


class CdkExampleStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        queue = sqs.Queue(
            self, "CdkExampleQueue",
            visibility_timeout=Duration.seconds(300),
        )

        topic = sns.Topic(
            self, "CdkExampleTopic"
        )

        topic.add_subscription(subs.SqsSubscription(queue))

А в ньому вже бачимо самі ресурси, які будуть створюватись – SQS та SNS з Subscription.

Можна переглянути документацію по ресурсах:

[simterm]

>>> import aws_cdk as cdk
>>> help (cdk.App())

[/simterm]

Де буде описаний клас App:

[simterm]

Help on App in module aws_cdk object:

class App(Stage)
 |  App(*args: Any, **kwargs) -> Any
 |  
 |  A construct which represents an entire CDK app. This construct is normally the root of the construct tree.
 |  
 |  You would normally define an ``App`` instance in your program's entrypoint,
 |  then define constructs where the app is used as the parent scope.
...

[/simterm]

Добре, йдемо далі.

cdk list (або ls) поверне нам список ресурсів в поточному каталозі/проекті:

[simterm]

$ cdk ls
cdk-example

[/simterm]

Далі, можемо спробувати cdk synth, який сгенерує нам шаблон CloudFormation, которий буде використано при деплої ресурсів:

[simterm]

$ cdk synth
Resources:
  CdkExampleQueue7618E31B:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 300
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: cdk-example/CdkExampleQueue/Resource
  CdkExampleQueuePolicy839151B5:
    Type: AWS::SQS::QueuePolicy
...

[/simterm]

Робота з AWS CDK

AWS Account CDK Bootstrap

Перед тим, як вже деплоїти проект, нам потрібно налаштувати наш AWS аккаунт (або регіон) для роботи AWS CDK. Для цього використовуємо cdk bootstrap, який створить CloudFormation стек з необхідними для роботи CDK ресурсами – S3-корзиною, ролі та полісі в IAM, ECR репозиторій, та записи у AWS Systems Manager Parameter Store. Див. bootstrapping.

Запускаємо:

[simterm]

$ cdk -v bootstrap
...
 ⏳  Bootstrapping environment aws://264***286/eu-central-1...
...
CDKToolkit |  0/12 | 1:43:06 PM | REVIEW_IN_PROGRESS   | AWS::CloudFormation::Stack | CDKToolkit User Initiated
CDKToolkit |  0/12 | 1:43:11 PM | CREATE_IN_PROGRESS   | AWS::CloudFormation::Stack | CDKToolkit User Initiated
CDKToolkit |  0/12 | 1:43:16 PM | CREATE_IN_PROGRESS   | AWS::IAM::Role          | FilePublishingRole 
CDKToolkit |  0/12 | 1:43:16 PM | CREATE_IN_PROGRESS   | AWS::IAM::Role          | LookupRole 
CDKToolkit |  0/12 | 1:43:16 PM | CREATE_IN_PROGRESS   | AWS::SSM::Parameter     | CdkBootstrapVersion 
CDKToolkit |  0/12 | 1:43:16 PM | CREATE_IN_PROGRESS   | AWS::IAM::Role          | CloudFormationExecutionRole 
CDKToolkit |  0/12 | 1:43:16 PM | CREATE_IN_PROGRESS   | AWS::ECR::Repository    | ContainerAssetsRepository 
CDKToolkit |  0/12 | 1:43:16 PM | CREATE_IN_PROGRESS   | AWS::IAM::Role          | ImagePublishingRole 
CDKToolkit |  0/12 | 1:43:16 PM | CREATE_IN_PROGRESS   | AWS::S3::Bucket         | StagingBucket
...
CDKToolkit | 11/12 | 1:44:02 PM | CREATE_COMPLETE      | AWS::IAM::Role          | DeploymentActionRole 
CDKToolkit | 12/12 | 1:44:04 PM | CREATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit 
[13:44:09] Stack CDKToolkit has completed updating
 ✅  Environment aws://264***286/eu-central-1 bootstrapped.

[/simterm]

Перевіряємо S3 бакет:

[simterm]

$ aws s3 ls
2023-05-10 13:43:42 cdk-hnb659fds-assets-264***286-eu-central-1

[/simterm]

cdk deploy

І тепер можемо виконати cdk deploy, який створить CloudFormation Stack з нашими SQS/SNS/Subscription:

[simterm]

$ cdk deploy      
...
cdk-example: assets built

This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬────────────────────────┬────────┬─────────────────┬───────────────────────────┬────────────────────────────────────────────────────────┐
│   │ Resource               │ Effect │ Action          │ Principal                 │ Condition                                              │
├───┼────────────────────────┼────────┼─────────────────┼───────────────────────────┼────────────────────────────────────────────────────────┤
│ + │ ${CdkExampleQueue.Arn} │ Allow  │ sqs:SendMessage │ Service:sns.amazonaws.com │ "ArnEquals": {                                         │
│   │                        │        │                 │                           │   "aws:SourceArn": "${CdkExampleTopic}"                │
│   │                        │        │                 │                           │ }                                                      │
└───┴────────────────────────┴────────┴─────────────────┴───────────────────────────┴────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
...

[/simterm]

Віповідаємо Y:

[simterm]

...
Do you wish to deploy these changes (y/n)? y
cdk-example: deploying... [1/1]
[0%] start: Publishing 20e979ce16c7aba5e874330247d9054b841ea313261b523b47a50fc4cd1d6662:current_account-current_region
[100%] success: Published 20e979ce16c7aba5e874330247d9054b841ea313261b523b47a50fc4cd1d6662:current_account-current_region
cdk-example: creating CloudFormation changeset...
[███████████████████▎······································] (2/6)

1:48:50 PM | CREATE_IN_PROGRESS   | AWS::CloudFormation::Stack | cdk-example
1:48:54 PM | CREATE_IN_PROGRESS   | AWS::SQS::Queue        | CdkExampleQueue
...

[/simterm]

CDK локально сгенерує файл темплейту cdk.out/cdk-example.template.json для CloduForamtion та загрузить його до бакету CDK, який був створений під час виконання cdk bootstrap:

[simterm]

$ aws s3 ls cdk-hnb659fds-assets-264***286-eu-central-1
2023-05-10 13:48:45       5750 20e979ce16c7aba5e874330247d9054b841ea313261b523b47a50fc4cd1d6662.json

[/simterm]

Перевіряємо CloduForamtion стек:

[simterm]

$ aws cloudformation list-stacks
{
    "StackSummaries": [
        {
            "StackId": "arn:aws:cloudformation:eu-central-1:264***286:stack/cdk-example/3c4e4db0-ef20-11ed-9672-0a9a3483d50e",
            "StackName": "cdk-example",
            "CreationTime": "2023-05-10T10:48:45.099000+00:00",
...

[/simterm]

Або у AWS Console:

Тим часом деплой завершено:

[simterm]

...
 ✅  cdk-example

✨  Deployment time: 91.52s

Stack ARN:
arn:aws:cloudformation:eu-central-1:264***286:stack/cdk-example/3c4e4db0-ef20-11ed-9672-0a9a3483d50e

✨  Total time: 97.99s

[/simterm]

cdk diff

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

До імпортів додаємо aws_s3 as s3 та прибираємо sns/sqs, iam та Duration.

Видаляємо ресурси SQS та SNS з класу CdkExampleStack, та описуємо створення корзини – беремо приклад з документації PyPI:

from constructs import Construct
from aws_cdk import (
    Stack,
    aws_s3 as s3
)


class CdkExampleStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        bucket = s3.Bucket(self, "MyEncryptedBucket",
            encryption=s3.BucketEncryption.KMS
        )

І поглянемо, що нам поверне cdk diff:

Червоним та - це те, що буде видалятись, зеленим та + – створюватись нове (прям дуже terraform plan нагадало).

Окей – і запускаємо деплой:

[simterm]

$ cdk deploy

✨  Synthesis time: 6.38s

cdk-example: building assets...
...
Do you wish to deploy these changes (y/n)? y
cdk-example: deploying... [1/1]
[0%] start: Publishing 5bb8c7fc8643769d69d5eb9712af36c955f9b509cc05d26740d035e9d7225a16:current_account-current_region
[100%] success: Published 5bb8c7fc8643769d69d5eb9712af36c955f9b509cc05d26740d035e9d7225a16:current_account-current_region
cdk-example: creating CloudFormation changeset...
[████████████▉·············································] (2/9)

11:29:47 AM | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack | cdk-example
11:31:54 AM | CREATE_IN_PROGRESS   | AWS::S3::Bucket    | MyEncryptedBucket
...

[/simterm]

Глянемо, як воно виглядає в UI:

Готово.

Ну і приберемо за собою – видалимо стек.

cdk destroy

Перевіримо їм’я стеку з list:

[simterm]

$ cdk ls            
cdk-example

[/simterm]

І виконуємо cdk destroy з ім’ям стеку, щоб повністю його видалити:

[simterm]

$ cdk destroy cdk-example
Are you sure you want to delete: cdk-example (y/n)? y
cdk-example: destroying... [1/1]

 ✅  cdk-example: destroyed

[/simterm]

Глянемо адмінку:

Але бакет та ключ не видалило. А чому?

AWS CDK RemovalPolicy

Тому що у aws_cdk.core є окрема RemovalPolicy, яка по дефолту має значення Retain.

Ця політика контролює дії, які будуть виконані з ресурсами, которі були видалені з-під контролю CloudFormation:

  • якщо ресурс видалений з шаблону (взагалі я чомусь думав, що CloudFormation видаляє такі ресурси, але то з досвіду, коли шаблони писались делоїлись вручну через AWS CLI, у випадку з CDK, як бачимо, по дефолту вони таки зберігаються)
  • ресурс потребує заміни шляхом створення нового, тож CloudFormation створює новий, а старий видаляє зі свого контролю, але залишає сам ресурс
  • CloudFormation стек видалено

Останній пункт у нас і спрацював.

Окей, давайте повторимо експеримент – створимо стек заново, але до корзини додамо параметр removal_policy для її видалення при видаленні стеку, і auto_delete_objects=True, щоб видалити всі об’єкти в ній, бо інакше корзину видалити не можна.

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

from constructs import Construct
from aws_cdk import (
    Stack,
    RemovalPolicy,
    aws_s3 as s3,
    aws_kms as kms
)


class CdkExampleStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        my_key = kms.Key(self, "MyKey",
            enable_key_rotation=True,
            removal_policy=RemovalPolicy.DESTROY
        )

        bucket = s3.Bucket(self, "MyEncryptedBucket",
            encryption=s3.BucketEncryption.KMS,
            encryption_key=my_key,
            removal_policy=RemovalPolicy.DESTROY,
            auto_delete_objects=True
        )

Виконуємо synth:

[simterm]

$ cdk synth
Resources:
  MyKey6AB29FA6:
    Type: AWS::KMS::Key
    ...
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
...
  MyEncryptedBucket9A8D2FE1:
    Type: AWS::S3::Bucket
    ...
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
...

[/simterm]

Ось тепер має спрацювати – у обох ресурсів бачимо DeletionPolicy: Delete.

деплоймо:

[simterm]

$ cdk deploy
...
cdk-example: creating CloudFormation changeset...

 ✅  cdk-example

✨  Deployment time: 180.7s

Stack ARN:
arn:aws:cloudformation:eu-central-1:264***286:stack/cdk-example/1f5353d0-efe4-11ed-a627-02deb26b4a5c

✨  Total time: 187.53s

[/simterm]

І видаляємо:

[simterm]

$ cdk destroy
Are you sure you want to delete: cdk-example (y/n)? y
cdk-example: destroying... [1/1]

 ✅  cdk-example: destroyed

[/simterm]

Не встиг зробити скрін, але CDK запускав AWS Lambda, яка видаляла об’єкти в корзині, а може й саму корзину.

Ну й поки на цьому все.

У Workshop є ще приклади роботи, більше “продвинуті”, тож рекомендую.

Loading

AWS: Fargate – можливості, порівняння з Lambda/EC2 та використання з AWS EKS
0 (0)

3 Травня 2023

AWS Fargate – ще одне serverless-рішення від Amazon, яке бере на себе управління інфраструктурою, позбавляючи користувача необхідності витрачати час на налаштування ЕС2-інстансів, операційної системи, систем управління контейнерами тощо.

Взагалі, коли знайомився з Fargate, натрапив на чудове відео з AWS re:Invent 2022, де дуже добре розказано (і показано) про Shared Responsibility model у AWS – за які частини системи відповідає Амазон, а за які – користувач, тож дуже рекомендую до перегляду – AWS re:Invent 2022 – A close look at AWS Fargate and AWS App Runner.

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

Тобто, AWS бере на себе все, пов’язане з серверами та операційною системою і її компонентами, тоді як нам лишається тільки створити та запустити контейнер.

При цьому, AWS Fargate можна використовувати разом із AWS Elastic Container Service або AWS Elastic Kubernetes Service, і саме його роботу з AWS EKS ми сьогодні й розглянемо.

AWS Fargate vs AWS Lambda

Перше питання, яке з’явилось у мене, коли я почав читати про Fargate – а навіщо, якщо вже є AWS Lambda? В чому різниця? До речі, у відео ще розказується і про AWS App Runner – ще один serverless-сервіс від Амазону, але зараз не про нього (а ще маємо Knative, хоча це вже не про AWS).

Functionality

Отже, концептуально – AWS Fargate являє собою CaaS, тобто Container as a Service, тоді як AWS Lambda – це FaaS, тобто Function as a Service: для роботи з Fargate вам потрібно мати зібраний docker-образ (чи будь-який інший відповідний до Open Container Initiative специфікації), тоді як для роботи з AWS Lambda вам потрібен тільки код – Lambda сама “запакує” його в контейнер, та запустить.

Крім того, для Fargate ви маєте налаштовувати автоскейлінг контейнерів, тоді як у Lambda це відбувається автоматично. Крім того, контейнери у Fargate не будуть скейлитись в нуль, коли немає роботи – для цього ви маєте виключати Fargate tasks самі (або просто скейлити в нуль поди в Kubernetes), а у Lambda фунцкції будуть запинені, як тільки до них перестануть надходити евенти, які тригерять запуск цих функцій.

Вартість та оплата за сервіс

При цьому обидва сервіси мають однакову модель оплати – pay-as-you-go model, тобто ви платите тільки за час, коли ваш контейнер або функція виконуються, хоча й мають відмінності: у Fargate оплата стягується саме за споживані CPU/RAM в секунду, тоді як у Labmda ви платите за кожен виклик функції та за час її виконання. Див. AWS Fargate Pricing.

Use Cases

Fargate добре підходить для роботи довготривалих задач, для яких ви маєте більше можливостей налаштувати робоче оточення, і маєте менше обмежень на CPU/RAM, дискову систему для збереження даних та не маєте таких суттєвих лімітів на розмір даних, які можете відправляти/отримувати.

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

AWS Fargate vs AWS EKS EC2 Node Groups

Вже по ходу діла запуску перших подів у EKS з використанням Fargate з’явилося інше питання – а що там з EC2?

В цьому порівняні, у Fargate такі переваги:

  • більш швидкий скейлінг
  • може бути більше cost effective рішенням, ніж EC2
  • не потребує security патчів (хоча Managed Node Groups начебто теж самі встановлюють патчі)

Недоліки Fargate:

  • менше контролю над інфрастуктурою
  • іноді рішення з EC2 може бути більш вигідним по вартості
  • обмеження по CPU/Memory та типам інстансів (наприклад, немає змоги звикористовувати GPU)

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

Взагалі, ви просто можете в одному EKS-кластері мати і Node Groups, і Fargate-інстанси для різних подів.

Amazon EKS та AWS Fargate

Отже, у EKS наші поди мають чомусь запускатись. Зазвичай для цього використовуються NodeGrops (Managed та Self-managed), які являють собою звичайні AWS EC2 інстанси, але замість віртуальних машин ми можемо використати AWS Fargate, див. Amazon EKS nodes.

Які саме поди будуть запускатись налаштовується у Fargate profiles, які являються частиною вашого EKS-кластеру. Сам EKS інтегруються з Fargate через контролери, які вбудовані в сервіс EKS і працюють на його control plane. Крім того, для запуску подів у Fargate є окремий scheduler – fargate-scheduler (на відміну від default-scheduler, котрий відповідає за запуск подів на ЕС2-інстансах).

Див. AWS Fargate.

Особливості Fargate у EKS

При планувані використання AWS Fargate разом з вашим EKS, майте на увазі, що:

  • Network Load Balancers та Application Load Balancers при використанні Fargate мають бути з IP targets
  • DaemonSets не підтримуються
  • Privileged containers не підтримуються
  • Pods у Fargate не можуть мати HostPort або HostNetwork
  • Pods у Fargate можуть бути запущені тільки у приватних мережах (private subnets)
  • ви можете використовувати Vertical Pod Autoscaler щоб задати потрібні resources.requests для подів, і потім скейлити їх за допомогою Horizontal Pod Autoscaler
  • Amazon EC2 instance metadata service (IMDS) не підтримується у Fargate
  • на Fargate нодах використовується Amazon VPC CNI plugin for Amazon EKS, альтернативні CNI plugins не підтримуються
  • ви можете використовувати Amazon EBS CSI controller з Fargate, але не маєте змоги підключати додаткові EBS
  • при використанні Kubernetes job з Fargate важливо видаляти ці джоби після завершення їхньої роботи навіть якщо Failed, інакше вони продовжуватимуть використовувати Fargate ноди, а ви продовжите платити
  • максимум 16 vCPU та 120 GB RAM (див. AWS Fargate increases compute and memory resource configurations by 4x)
  • у AWS ECS наче вже є можливість використовувати Fargate Spot instances, але у EKS такого поки нема (але є GitHub issue з таким реквестом)

Див. всі у AWS Fargate considerations.

Створення EKS кластеру

Тут все будемо “клікопсити”, якось іншим разом розгорнемо EKS за допомогою Terraform або AWS CDK.

EKS cluster IAM role

Спершу, нам потрібна AIM роль, через яку майбутній кластер буде спілкуватись з сервісами Амазону, див. Amazon EKS cluster IAM role.

Переходимо в АІМ, клікаємо Create role, у Trusted entity type залишаємо AWS service, у Use case зі списку вибираємо EKS – Cluster:

На сторінці Add permissions лишаємо за-замовченням, та натискаємо Next:

Далі, задаємо ім’я ролі, і тиснемо Create Role:

Створення VPC та Subnets

Боже, сто років не створював їх… Тим більш вручну.

Окей, що нам треба:

  • VPC
  • її розбити на кілька сабнетів в різних AvailabilityZones
    • частина – публічні, з Internet Gateway – для всяких AWS LoadBalancer
    • частина – приватні, тут будуть жити поди Куберу, для них створимо NAT Gateway
  • і ще там щось було з SecurityGroups

Поїхали.

І – оп… Приїхали) Інтерфейс створення VPC переробили прям круто…

Переходимо до VPC, створюємо нову мережу, вибираємо VPC and more – мені просто цікаво, як воно, і виглядає воно прям реально зручно – все створюємо відразу, а не півгодини клікаємо по VPC dashboard, а потім думаємо де ми накосячили в route tables.

Тож створюємо VPC з сабнетами в кожній Availability Zone, створюємо публічні сабнети з Internet Gateway, та приватні с NAT Gateway у кожній Availability Zone (дороже, але надійніше, якщо таке робити для Продакшену). Заодно додамо VPC S3 endpoints – зараз він не потрібен, але взагалі дуже корисна штука в плані security та cost-effective:

Внизу перевіряємо, що DNS hostnames та DNS resolution включені – це вимога для роботи Fargate (описано у тому ж AWS Fargate considerations, якщо ви його пропустили):

Клікаємо Create.

Чорт – реально круто зробили!

Чекаємо створення ресурсів:

Створення EKS cluster

Переходимо до Elastic Kubernetes Service, клікаємо Add cluster > Create:

Задаємо ім’я кластеру, вибираємо створену роль, та клікаємо Next (Secrets encryption – щось новеньке, треба якось потестити):

Далі, вибираємо нашу VPS – сабнети підтянуться самі, нижче вибирамо SecurityGroup, наразі можна дефолтну з нашой VPC:

Далі, налаштовуємо доступ до нашого кластеру.


AWS Fargate Pod зависає у статусі Pending

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

[simterm]

$ kubectl describe pod nginx
Name:                 nginx
Namespace:            default
...
Labels:               eks.amazonaws.com/fargate-profile=fargate-test-eu-central-1-profile
...
Status:               Pending
...
  Warning  LoggingDisabled   11m    fargate-scheduler  Disabled logging because aws-logging configmap was not found. configmap "aws-logging" not found
  Warning  FailedScheduling  8m48s  fargate-scheduler  Pod provisioning timed out (will retry) for pod: default/nginx

[/simterm]

Я вже все перебрав і перегуглив – Fargate profiles, SecurityGroup EKS, NAT Gateways у subnets – всюди все вірно. WTF?

Виявилось, що коли в цьому пості трохи нижче написав:

Вазагалі, звісно бажано відключати публічний доступ до АПІ, і ходити через приватний ендпоінт, наприклад через ВПН. Але зараз залимо обидва варвіанти – і публічний, і приватний, тіль задамо ліміт на ІП.

То побіг далі, і не переключив доступ до кластеру з дефолтного значення Public на Public and private, що й призвело до проблеми.

І це сказано у перших строках документації, і я навіть цитував це, коли описував процес:

Without the private endpoint enabled, the CIDR blocks that you specify for public access must include the outbound sources from your VPC.

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

Окей, йдемо далі – на цей раз вже правильно.


Отже, взагалі, звісно бажано відключати публічний доступ до Kubernetes API, і ходити через приватний ендпоінт, наприклад через VPN. Але зараз включимо обидва варіанти – і публічний, і приватний, тільки задамо ліміт на мій домашній IP.

Знаходимо його:

[simterm]

$ curl ifconfig.me
217.***.***.253

[/simterm]

Та додаємо його у дозволені:

Далі, налаштовуємо логування. Тут теж бажано включати якщо не всі, то хоча б логи API-серверу, Audit або Authentificator, та Scheduler:

На наступному кроці, вибираємо Addons (теж наче не було такого раніше), тут ще й GuardDuty з’явився:

На наступній сторінці не бачу, що можна було б міняти – лишаємо, клікаємо Next:

І нарешті останнє – ревью, та створюємо кластер:

Чекаємо. Раніше це займало хвилин 15-20, але були новини, що Амазон пришвидшив процесс.

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

Для перевірки того, що з кластером все гаразд і для подальшого тестування подів додамо його у наш ~/.kube/config.

Додаємо у конфіг:

[simterm]

$ aws --profile setevoy eks update-kubeconfig --name fargate-test-eu-central-1-cluster --alias setevoy-fargate-test-eu-central-1-cluster
Added new context setevoy-fargate-test-eu-central-1-cluster to /home/setevoy/.kube/config

[/simterm]

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

[simterm]

$ kk get pod --all-namespaces
NAMESPACE     NAME                      READY   STATUS    RESTARTS   AGE
kube-system   coredns-cbbbbb9cb-2hhx2   0/1     Pending   0          18h
kube-system   coredns-cbbbbb9cb-4xf2w   0/1     Pending   0          18h

[/simterm]

Поки що маємо тільки два поди с CoreDNS, зараз у Pending, бо намає ані Woker Nodes, ані Fargate profile.

Переходимо до Fargate.

Підключення AWS Fargate

Тепер, як маємо EKS кластер, настав час підключати Fargate.

Див. документацію у Getting started with AWS Fargate using Amazon EKS, і перше, на шо звертаємо увагу – це Security Groups на Worker Nodes:

If you restrict access to the public endpoint of your cluster using CIDR blocks, we recommend that you also enable private endpoint access. This way, Fargate pods can communicate with the cluster. Without the private endpoint enabled, the CIDR blocks that you specify for public access must include the outbound sources from your VPC.

В нашому випадку ніяких Node Groups нема, тож продовжуємо.

EKS pod execution IAM Role

Спочатку нам треба додати АІМ роль, яка дозволить подам у Fargate комунікувати з Амазоном, див. Amazon EKS pod execution IAM role (обожнюю документацію Амазону).

Переходимо в AIM > Roles > Create role.

На цей раз вибираємо EKS – Fragate pod:

Далі – Next, на сторінці Add permissions лишаємо, як є (можна подивитись самі пермішени), переходимо далі, задаємо ім’я і натискаємо Create role:

Після створення знаходимо роль, переходимо до вкладки Trust relationships, редагуємо роль:

Прописуємо нову політику доступу до цієї ролі:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Condition": {
         "ArnLike": {
            "aws:SourceArn": "arn:aws:eks:eu-central-1:26***286:fargateprofile/fargate-test-eu-central-1-cluster/*"
         }
      },
      "Principal": {
        "Service": "eks-fargate-pods.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Натискаємо Update policy, та переходимо до створення Fargate profile.

Створення Fargate profile

Fargate профіль описує, які саме поди з Kubernetes-кластру будут запускатись у Fargate, див. AWS Fargate profile.

Для створення, переходимо до EKS, потім до нашого кластеру, на вкладці Compute знизу знаходимо Add Fargate profile:

Задаємо ім’я, АІМ роль підставилась автоматично, як і приватні сабнету нашої VPC:

У Pod selectors описується які поди з яких неймспейсів будуть запускатись з цим Fargate профілем.

Зараз для тесту нехай будуть всі, але в цілому можна створювати різні профайли для різних типів подів з виборкою по неймспейсам та/або лейблам, які додані подам:

Далі, перевіряємо, що все вірно, та створюємо профайл:

Створення зайняло хвилин 5.

Запуск Kubernetes-подів у Fargate

Отже, поки що маємо тільки два поди з CoreDNS, які знаходяться у статусі Pending.

Щоб CoreDNS поди запустились у Fargate – редагуємо їхній Deployment, та прибираємо аннотацію eks.amazonaws.com/compute-type: ec2:

 

За хвилину-дві перевіряємо:

[simterm]

$ kubectl -n kube-system get pod
NAME                       READY   STATUS    RESTARTS   AGE
coredns-75694977b5-m7smc   0/1     Pending   0          5m48s
coredns-75694977b5-tgwdl   1/1     Running   0          50s

[/simterm]

Перший пішов.

І після запуску перших подів на вкладці Compute нашого кластеру маємо побачити Fargate-ноди у EKS-кластері:

Поки стартує другий под з CoreDNS – додамо звичайний тестовий под, щоб ще раз побачити як воно працює:

[simterm]

$ kubectl run nginx --image=nginx
pod/nginx created

[/simterm]

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

[simterm]

$ kubectl get pod --all-namespaces
NAMESPACE     NAME                       READY   STATUS    RESTARTS   AGE
default       nginx                      1/1     Running   0          2m27s
kube-system   coredns-75694977b5-r8d6b   1/1     Running   0          46s
kube-system   coredns-75694977b5-tgwdl   1/1     Running   0          5m52s

[/simterm]

Та Fargate-ноди:

Щодо івенту “Disabled logging because aws-logging configmap was not found” – можно окремо налаштувати логгінг, див. Fargate logging, на запуск подів не впливає.

Ще з цікавого, що добре знати, це те, що IAM-роль для наших подів додається до aws-auth ConfigMap кластеру:

[simterm]

$ kk -n kube-system get cm aws-auth -o yaml
apiVersion: v1
data:
  mapRoles: |
    - groups:
      - system:bootstrappers
      - system:nodes
      - system:node-proxier
      rolearn: arn:aws:iam::264***286:role/AmazonEKSFargatePodExecutionRole-fargate-test-eu-central-1
      username: system:node:{{SessionName}}

[/simterm]

Так начебто все.

Можна пробувати користуватись замість звичайних EC2.

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

Loading

Kubernetes: вертикальний скейлінг подів з Vertical Pod Autoscaler
0 (0)

29 Квітня 2023

Окрім Horizontal Pod Autoscaler (HPA), який створює додаткові поди якщо наявні починають використовувати більше CPU/Memory, ніж налаштовано у лімітах HPA, існує і Vertical Pod Autoscaler (VPA), який працює за іншою схемою: замість горизонтального масштабування, тобто збільшення кількості подів, він змінює resources.requests поду, що призводить до того, что Kubernetes Scheduler “переселяє” цей под на іншу WorkerNode, якщо на поточній не вистачає ресурсів.

Тобто, VPA постійно моніторить споживання ресурсів контейнерами у подах, і змінює значення відповідно до актуального споживання ресурсів, і може як збільшувати значення реквестів, так і зменшувати його, таким чином автоматично налаштовуючи потреби пода, щоб уникнути нераціонального використання ресурсів інстансів Kubernetes-кластеру та забезпечити сам под достатнім CPU time і пам’ятю.

Компоненти Vertical Pod Autoscaler

Після деплою VPA, він створює три поди для своєї роботи:

  • recommender: займається моніторингом використання ресурсів подами, і видає свої рекомендації по значенню cpu/mem requests, які треба встановити подам
  • updater: моніторить поди та їхні поточні значення cpu/mem requests, і якщо ці значення не збігаються зі значеннями від recommender – то “вбиває” їх (EvictedByVPA Kubernetes event), щоб контролери Kubernetes перестворили їх з потрібними значеннями
  • admission-plugin: займається власне тим, що встановлює значення реквестів для нових подів, або тих, що були перестворенні після того, як updater кільнув їх

Обмеження Vertical Pod Autoscaler

При використанні VPA, майте на увазі, що:

  • VPA не відслідковує процесс перестворення подів, тобто після того, як под був evicted – то його створення вже цілком залежить від Kubernetes. Якщо у кластері на момент перестворення подів не буде вільних WorkerNodes, то под може залишитиcь у Pending статусі, тому бажано мати Cluster Autoscaler або Karpenter, який запустить новую ноду
  • VPA не може використовуватись одночасно з HPA, якщо скейлінг налаштовано на CPU/Memory, але їх можна використовувати, якщо HPA налаштований на custom metrics
  • також майте на увазі сам факт того, що при роботі VPA перестворює поди, тобто якщо у вас немає якогось fault-tolerant у вигляді додаткових подів, які зможуть взяти на себе навантаження на час перестворення поду – то сервіс буде недоступний, допоки відповідний контроллер (ReplicaSet, StatefulSet, etc) не запустить новий інстанс поду

Див більше у Known limitations.

Запуск Vertical Pod Autoscaler

При роботі, VPA покладається на Kubernetes Metrics Server для отримання значень CPU/Mem подів, але також може використовувати Prometheus, див. How can I use Prometheus as a history provider for the VPA recommender.

Встановлення Metrics Server

Оскільки тестити VPA будемо у Minikube, то встановимо плагін Metrics Server:

[simterm]

$ minikube addons enable metrics-server

[/simterm]

Або у разі звичайного Kubernetes-кластеру – з Helm-чарту:

[simterm]

$ helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
$ helm -n kube-system upgrade --install metrics-server metrics-server/metrics-server

[/simterm]

І дивимось, чи працює kubectl top pod, який бере дані саме з Metrics Server:

[simterm]

$ kubectl top pod --all-namespaces
NAMESPACE     NAME                               CPU(cores)   MEMORY(bytes)   
kube-system   etcd-minikube                      83m          33Mi            
kube-system   kube-apiserver-minikube            249m         254Mi           
kube-system   kube-controller-manager-minikube   54m          45Mi            
kube-system   kube-scheduler-minikube            26m          22Mi

[/simterm]

Встановлення Vertical Pod Autoscaler

Також використовуємо Helm-чарт – cowboysysop/vertical-pod-autoscaler:

[simterm]

$ helm repo add cowboysysop https://cowboysysop.github.io/charts/
$ helm -n kube-system upgrade -install vertical-pod-autoscaler cowboysysop/vertical-pod-autoscaler

[/simterm]

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

[simterm]

$ kk -n kube-system get pod -l app.kubernetes.io/name=vertical-pod-autoscaler
NAME                                                            READY   STATUS    RESTARTS   AGE
vertical-pod-autoscaler-admission-controller-655f9b57d7-q85kc   1/1     Running   0          58s
vertical-pod-autoscaler-recommender-7d964f7894-k87hb            1/1     Running   0          58s
vertical-pod-autoscaler-updater-7ff97c4d85-vfjkj                1/1     Running   0          58s

[/simterm]

Та його CustomResourceDefinitions:

[simterm]

$ kk get crd
NAME                                                  CREATED AT
verticalpodautoscalercheckpoints.autoscaling.k8s.io   2023-04-27T08:38:16Z
verticalpodautoscalers.autoscaling.k8s.io             2023-04-27T08:38:16Z

[/simterm]

Тепер все готове, щоб починати ним користуватись.

Приклади роботи з Vertical Pod Autoscaler

В репозиторії VPA є директорія examples, яка містить приклади маніфестів, наприклад у файлі hamster.yaml є приклад налаштованого VPA та тестового Deployment.

Але давайте створимо свої маніфести, та задеплоїмо ресурси окремо.

Спочатку Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hamster
spec:
  selector:
    matchLabels:
      app: hamster
  replicas: 2
  template:
    metadata:
      labels:
        app: hamster
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534 # nobody
      containers:
        - name: hamster
          image: registry.k8s.io/ubuntu-slim:0.1
          resources:
            requests:
              cpu: 100m
              memory: 50Mi
          command: ["/bin/sh"]
          args:
            - "-c"
            - "while true; do timeout 0.5s yes >/dev/null; sleep 0.5s; done"

Тут маємо створити два поди, яким задаємо requests у 100 Milicpu та 50 Megabyte memory.

Деплоймо:

[simterm]

$ kubectl apply -f hamster-deployment.yaml 
deployment.apps/hamster created

[/simterm]

За хвилину-дві перевіряємо ресурси, які реально споживаються подами:

[simterm]

$ kk top pod
NAME                       CPU(cores)   MEMORY(bytes)   
hamster-65cd4dd797-fq9lq   498m         0Mi             
hamster-65cd4dd797-lnpks   499m         0Mi

[/simterm]

Тепер додамо VPA:

apiVersion: "autoscaling.k8s.io/v1"
kind: VerticalPodAutoscaler
metadata:
  name: hamster-vpa
spec:
  # recommenders field can be unset when using the default recommender.
  # When using an alternative recommender, the alternative recommender's name
  # can be specified as the following in a list.
  # recommenders: 
  #   - name: 'alternative'
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: hamster
  resourcePolicy:
    containerPolicies:
      - containerName: '*'
        minAllowed:
          cpu: 100m
          memory: 50Mi
        maxAllowed:
          cpu: 1
          memory: 500Mi
        controlledResources: ["cpu", "memory"]

Деплоїмо:

[simterm]

$ kubectl apply -f hamster-vpa.yaml 
verticalpodautoscaler.autoscaling.k8s.io/hamster-vpa created

[/simterm]

Перевіряємо сам VPA:

[simterm]

$ kk get vpa
NAME          MODE   CPU   MEM   PROVIDED   AGE
hamster-vpa   Auto                          14s

[/simterm]

І за хвилину-дві, коли спрацює recommender:

[simterm]

$ kk get vpa
NAME          MODE   CPU    MEM       PROVIDED   AGE
hamster-vpa   Auto   587m   262144k   True       43s

[/simterm]

І ще за хвилину – перевіряємо поди, коли спрацює Updater:

[simterm]

$ kk get pod
NAME                       READY   STATUS        RESTARTS   AGE
hamster-65cd4dd797-fq9lq   1/1     Terminating   0          3m43s
hamster-65cd4dd797-hc9cn   1/1     Running       0          13s
hamster-65cd4dd797-lnpks   1/1     Running       0          3m43s

[/simterm]

Та перевіряємо значення requests нового поду:

[simterm]

$ kubectl get pod hamster-65cd4dd797-hc9cn -o yaml | yq '.spec.containers[].resources'
{
  "requests": {
    "cpu": "587m",
    "memory": "262144k"
  }
}

[/simterm]

Тепер, як ми побачили VPA в дії, давайте трохи розберемось з його API та доступними параметрами.

Vertical Pod Autoscaler API reference та параметри

Повний опис див. у API reference, а зараз просто зробимо describe нашого існуючого VPA, щоб зрозуміти, що там взагалі є:

[simterm]

$ kubectl describe vpa/hamster-vpa
Name:         hamster-vpa
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  autoscaling.k8s.io/v1
Kind:         VerticalPodAutoscaler
Metadata:
  Creation Timestamp:  2023-04-27T09:05:41Z
  Generation:          61
  Resource Version:    7016
  UID:                 227c0ce6-7f86-4bff-b9b5-d88914f90bec
Spec:
  Resource Policy:
    Container Policies:
      Container Name:  *
      Controlled Resources:
        cpu
        memory
      Max Allowed:
        Cpu:     1
        Memory:  500Mi
      Min Allowed:
        Cpu:     100m
        Memory:  50Mi
  Target Ref:
    API Version:  apps/v1
    Kind:         Deployment
    Name:         hamster
  Update Policy:
    Update Mode:  Auto
Status:
  Conditions:
    Last Transition Time:  2023-04-27T09:06:11Z
    Status:                True
    Type:                  RecommendationProvided
  Recommendation:
    Container Recommendations:
      Container Name:  hamster
      Lower Bound:
        Cpu:     569m
        Memory:  262144k
      Target:
        Cpu:     587m
        Memory:  262144k
      Uncapped Target:
        Cpu:     587m
        Memory:  262144k
      Upper Bound:
        Cpu:     1
        Memory:  262144k
Events:          <none>

[/simterm]

Або у “чистому” yaml:

[simterm]

$ kubectl get vpa/hamster-vpa -o yaml           
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
...
spec:
  resourcePolicy:
    containerPolicies:
    - containerName: '*'
      controlledResources:
      - cpu
      - memory
      maxAllowed:
        cpu: 1
        memory: 500Mi
      minAllowed:
        cpu: 100m
        memory: 50Mi
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: hamster
  updatePolicy:
    updateMode: Auto
status:
  ...
  recommendation:
    containerRecommendations:
    - containerName: hamster
      lowerBound:
        cpu: 570m
        memory: 262144k
      target:
        cpu: 587m
        memory: 262144k
      uncappedTarget:
        cpu: 587m
        memory: 262144k
      upperBound:
        cpu: "1"
        memory: 262144k

[/simterm]

І тепер розлянемо параметри з нашого VPA та інші, которі можуть бути нам корисні в майбутньому:

  • spec (VerticalPodAutoscalerSpec):
    • targetRef: тип контролеру, який відповідає за поди, которі будуть скейлитись цим VPA
    • updatePolicy (PodUpdatePolicy): задає, чи будуть рекомендації застосовані при створенні поду, і чи будуть застосовуватись протягом його роботи
      • updateMode: може мати значення “Off“, “Initial“, “Recreate” та “Auto” (дефолтне значення):
        • Off: не буде застосовувати нові значення, а тільки внесе їх у поле status (див. нижче)
        • Initial: застосує значення тільки при створенні поду
        • Recreate: застосує значення при створенні поду і під час його роботи
        • Auto: на цей час виконує теж саме, що Recreate (хоча ще чотири роки тому наче казали, що планується при “Auto” міняти реквести без рестарту)
      • minReplicas: мінімальна кількість подів, які мають бути в статусі Running, щоб VPA Updater виконав Pod Eviction для застосування нових значень у requests
    • resourcePolicy (PodResourcePolicy): задає параметри того, як CPU та Memory requests будуть налаштовуватись для конкретних контейнерів, якщо не задано – то VPA застосує нові значення для всіх контейнерів в поді
      • containerPolicies (ContainerResourcePolicy): налаштування для конкретних контейнерів, або для всіх, які не мають власних параметрів, за допомогою containerName = '*'
        • containerName: ім’я контейнеру, для якого описуються параметри
        • mode: задає, чи будуть рекомендації застосовані при створенні контейнеру, і чи будуть застосовуватись протягом його роботи, може мати значення “Off” або “Auto” (дефолтне значення)
        • minAllowed та maxAllowed: задає мінімальні та максимальні значення для CPU/Memory requests
        • ControlledResources: для яких саме ресурсів застосовувати рекомендації – ResourceCPU, ResourceMemory, або обидва (дефолтне, якщо не вказано жодного)
  • status (VerticalPodAutoscalerStatus): останні рекуомендації від Recommender
    • recommendation (RecommendedPodResources): останні рекомендовані значення CPU/Memory
      • containerRecommendations (RecommendedContainerResources): рекомендації для кожного контейнеру
        • containerName: ім’я контейнеру
        • target: рекомендовані значення для контейнеру
        • lowerBound: мінімально рекомендовані значення для контейнеру
        • upperBound: максимально рекомендовані значення для контейнеру
        • uncappedTarget: останні рекомендовані значення CPU/Memory на основі реального споживання ресурсів без врахування ContainerResourcePolicy (тобто без minAllowed та maxAllowed), не враховується Recommender, відображається тільки для інформації

На цьому поки все.

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

Loading

Prometheus: запуск Pushgateway у Kubernetes з Helm та Terraform
0 (0)

28 Квітня 2023

Маємо на проекті багато AWS Lambda функцій, з яких девелопери хочуть мати можливість відправляти метрики до нашого Prometheus, щоб додати власних алертів та графіків у Grafana.

Для цього у функціях використовується бібліотека Prometheus, яка дозволяє ці метрики створювати (див. Prometheus: створення Custom Prometheus Exporter на Python), але ж ми не маємо змоги нормально отримати ці дані з Prometheus, бо функції можуть жити декілька секунд, а Prometheus використовує PULL-модель, тобто він сам ходить до експортерів, та витягує звідти дані.

Що можемо зробити у випадку AWS Lambda – це додати Prometheus Pushgateway, який буде мати зовнішній ендпоінт, на який функції будуть слати свої метрики, а Prometheus буде забирати ці дані з Pushgateway, який виконує таку собі “проксі роль”.

Загальна схема роботи Prometheus та роль Pushgateway добре відображена на цій схемі:

Тож сьогодні запустимо Prometheus Pushgateway у Kubernetes використовуючи Helm-чарт, а чарт будемо встановлювати за допомогою Terraform.

Pushgateway Helm chart

Спочатку, давайте подивимося, що у нас є у values чарту Pushgateway.

По факту, наразі нам тут можуть бути цікавими тільки два параметри – це Ingress та ServiceMonitor.

Напишемо свій values, для тесту задеплоїмо на Dev оточення, а потім додамо до Terraform, через який Pushgateway вже буде деплоїтися по всіх наших моніторинг-оточеннях:

ingress:
  enabled: true
  className: alb
  path: /
  pathType: Prefix
  annotations:
    alb.ingress.kubernetes.io/scheme: internal
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-west-2:638***021:certificate/e97f17e9-33f9-46e7-9b2b-d50de8a72219
    alb.ingress.kubernetes.io/target-type: ip
  hosts:
    - test-pushgateway.monitoring.dev.example.com

serviceMonitor:
  enabled: true
  namespace: monitoring
  additionalLabels:
    release: prometheus

Тут описуємо створення Kubernetes Ingress, якому на порт 443 відповідного AWS Application Load Balancer підключимо SSL сертифікат з Amazon Certificate Manager. В hosts задаємо ім’я хоста, яке за допомогою ExternalDNS буде створено в AWS Route53.

І додаємо ServiceMonitor з лейблою release: prometheus, щоб Prometheus із Kube Prometheus Stack його побачив.

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

[simterm]

$ helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
$ helm repo update

[/simterm]

І встановлюємо Pushgateway до неймспейсу monitoring:

[simterm]

$ helm -n monitoring upgrade --install prometheus-pushgateway-test prometheus-community/prometheus-pushgateway -f pushgateway-values-dev.yaml 
...
NOTES:
1. Get the application URL by running these commands:
  http://test-pushgateway.monitoring.example.com/

[/simterm]

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

[simterm]

$ kubectl get pod,ingress --namespace monitoring -l app.kubernetes.io/instance=prometheus-pushgateway-test
NAME                                               READY   STATUS    RESTARTS   AGE
pod/prometheus-pushgateway-test-6b5dfbdd7f-chzkf   1/1     Running   0          53s

NAME                                                    CLASS   HOSTS                                         ADDRESS                                                              PORTS   AGE
ingress.networking.k8s.io/prometheus-pushgateway-test   alb     test-pushgateway.monitoring.dev.example.com   internal-k8s-monitori-promethe-***-***.us-west-2.elb.amazonaws.com   80      54s

[/simterm]

Pushgateway має власний веб-інтерфейс – відкриваємо доступ до його поду на порт 9091:

[simterm]

$ kubectl -n monitoring port-forward pod/prometheus-pushgateway-test-6b5dfbdd7f-chzkf 9091
Forwarding from 127.0.0.1:9091 -> 9091
Forwarding from [::1]:9091 -> 9091

[/simterm]

Та в браузері переходимо на http://localhost:9091:

Далі, перевіримо чи бачить сам Prometheus цей Pushgateway – відкриваємо порт 9090 до Prometheus Pod:

[simterm]

$ kk -n monitoring port-forward prometheus-prometheus-kube-prometheus-prometheus-0 9090
Forwarding from 127.0.0.1:9090 -> 9090
Forwarding from [::1]:9090 -> 9090

[/simterm]

Та переходимо до Status > Targets, і шукаємо Pushgateway target:

І перевіримо, чи потраплять метрики з Pushgateway до Prometheus.

Оскільки наш Ingress/ALB має тип internal, тобто недоступний ззовні, то запустимо Kubernetes Pod з Ubuntu в Kubernetes-кластері:

[simterm]

$ kubectl run pod --rm -i --tty --image ubuntu -- bash
If you don't see a command prompt, try pressing enter.
root@pod:/# 

[/simterm]

Встановлюємо в ньому curl:

[simterm]

root@pod:/# apt update && apt -y install curl

[/simterm]

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

[simterm]

root@pod:/# echo "some_metric 3.14" | curl --data-binary @- https://test-pushgateway.monitoring.dev.exmaple.com/metrics/job/some_job

[/simterm]

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

Та у Prometheus:

Є, чудово.

Аутентифікація Pushgateway

Оскільки у нас Pushgateway працює на internal-ALB, то я не став поки робити аутентифікацію, бо він це все ще в процесі тестування, і поки невідомо як воно там зайде девелоперам.

Але у values є блок, з прикладом запуску контейнера з openshift/oauth-proxy, а у Using OpenShift OAuth Proxy to secure your Applications on OpenShift наче непогано описано приклад його використання – якось можна буде спробувати.

Terraform та Helm для Pushgateway

Так в проекті повелося” (с), що більша частина ресурсів у Kubernetes деплоїться за допомогою Terraform та його helm_release resource.

У нас багато аккаунтів AWS, і в кожному буде свій ARN сертифікату, тож отримаємо його за допомогою Data Source (або можна створити власний за допомогою aws_acm_certificate):

data "aws_acm_certificate" "wildcard" {
  domain = "*.${var.root_domain}"
}

Змінна root_domain у нас передається з основного модулю, та має вигляд monitoring.dev.example.com.

Для нашого values створюємо темплейт pushgateway-values.tpl.yaml:

ingress:
  enabled: true
  className: alb 
  path: /
  pathType: Prefix 
  annotations:
    alb.ingress.kubernetes.io/scheme: internal
    alb.ingress.kubernetes.io/certificate-arn: ${acmWildcardArn}
    alb.ingress.kubernetes.io/target-type: ip
  hosts: 
    - ${pushgatewayUrl}
  
serviceMonitor:
  enabled: true
  namespace: monitoring
  additionalLabels:
    release: prometheus

І додаємо сам helm_release, в якому у values[] задаємо файл темплейту та пару змінних – ARN сертифікату, який отримали за допомогою data.aws_acm_certificate, та домен для Ingress hosts:

resource "helm_release" "pushgateway" {
  name = "prometheus-pushgateway"

  namespace = kubernetes_namespace.monitoring.id

  repository  = "https://prometheus-community.github.io/helm-charts"
  chart       = "prometheus-pushgateway"
  version     = "2.1.3"
  max_history = 10

  values = [
    templatefile("${path.module}/configs/prometheus/pushgateway-values.tpl.yaml", {
      acmWildcardArn : data.aws_acm_certificate.wildcard.arn,
      pushgatewayUrl : "pushgateway.${var.root_domain}"
    })
  ]
}

Деплоїмо:

[simterm]

$ terraform apply
...
module.monitoring.helm_release.pushgateway: Creating...
module.monitoring.helm_release.pushgateway: Still creating... [10s elapsed]
module.monitoring.helm_release.pushgateway: Still creating... [20s elapsed]
module.monitoring.helm_release.pushgateway: Still creating... [30s elapsed]
module.monitoring.helm_release.pushgateway: Creation complete after 32s [id=prometheus-pushgateway]
...
Apply complete! Resources: 1 added, 1 changed, 0 destroyed.

[/simterm]

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

[simterm]

$ kk -n monitoring get ingress,pod -l app.kubernetes.io/instance=prometheus-pushgateway
NAME                                               CLASS   HOSTS                                    ADDRESS                                                              PORTS   AGE
ingress.networking.k8s.io/prometheus-pushgateway   alb     pushgateway.monitoring.dev.example.com   internal-k8s-monitori-promethe-***-***.us-west-2.elb.amazonaws.com   80      10m

NAME                                          READY   STATUS    RESTARTS   AGE
pod/prometheus-pushgateway-55b9f5ffd6-sm9ck   1/1     Running   0          10m

[/simterm]

Все є.

Готово.

Loading

AWS: Instance Metadata Service v1 vs IMDS v2 та робота з Kubernetes Pod і Docker контейнерів
0 (0)

11 Квітня 2023

Instance metadata (IMDS – Instance Metadata Service) – дані про EC2 інстанс, такі як інформація про AMI, IP, ім’я хосту, і т.д.

Також до Instance Metadata можна додати User Data для зберігання якихось параметрів, які потім можна буде отримати всередині інстансу.

Див. Instance metadata and user data та Instance metadata categories.

Від початку, в AWS була реалізована request/response модель доступу до IMDS, тобто для отримання доступу достатньо було зробити HTTP-запит з хосту. Пізніше, була реалізована система session-oriented, коли для доступу вже треба було отримати токен, і ця система отримала індекс v2. Див. Add defense in depth against open firewalls, reverse proxies, and SSRF vulnerabilities with enhancements to the EC2 Instance Metadata Service та Use IMDSv2.

Приклад роботи з IMDSv1

Наприклад, запустимо EC2 та в User Data додамо якесь значення:

Та пробуємо curl на адресу 169.254.169.254:

[simterm]

$ curl http://169.254.169.254/latest/user-data
somedata: somevalue

[/simterm]

Або отримаємо саме метадату інстансу:

[simterm]

$ curl http://169.254.169.254/latest/meta-data
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
events/
hostname
identity-credentials/
instance-action
instance-id
instance-life-cycle
instance-type
local-hostname
local-ipv4
mac
metrics/
network/
placement/
profile
public-hostname
public-ipv4
public-keys/
reservation-id
security-groups
services/

[/simterm]

Або теж саме з Docker-контейнеру:

[simterm]

root@ip-172-31-30-97:/home/ubuntu# docker run -ti alpine/curl curl http://169.254.169.254/latest/meta-data/
Unable to find image 'alpine/curl:latest' locally
latest: Pulling from alpine/curl
59bf1c3509f3: Pull complete 
da353f38084f: Pull complete 
05df90dbd213: Pull complete 
Digest: sha256:81372de8c566f2d731bde924bed45230018e6d7c21d051c15e283eb8e06dfa2d
Status: Downloaded newer image for alpine/curl:latest
ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
events/
hostname
...

[/simterm]

Доступ до IMDS із Kubernetes Pod

Тобто, і у випадку Kubernetes будь-який под має змогу отримати ці дані.

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

apiVersion: v1
kind: Pod
metadata:
  name: test-imds
  namespace: default
spec:
  containers:
  - name: test-imds
    image: alpine/curl:latest
    command:
      - sleep
      - "3600"
    imagePullPolicy: IfNotPresent
  restartPolicy: Always

Запускаємо його:

[simterm]

$ kk apply -f pod-imds.yaml

[/simterm]

І з нього виконуємо той самий запит з curl:

[simterm]

$ kk exec -ti test-imds -- curl http://169.254.169.254/latest/meta-data/
ami-id
ami-launch-index
ami-manifest-path
autoscaling/
block-device-mapping/
events/
hostname
iam/
...

[/simterm]

IMDS Security Credentials

Крім інших даних, IMDS може повернути Access/Secret ключі та токен, які використовуються для отримання доступу до Instance IAM Role.

Отримаємо security credentials:

[simterm]

$ curl http://169.254.169.254/latest/meta-data/identity-credentials/ec2/security-credentials/ec2-instance
{
  "Code" : "Success",
  "LastUpdated" : "2023-03-28T12:48:33Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASI***BND",
  "SecretAccessKey" : "4Yz***8Bx",
  "Token" : "IQo***GaQ=",
  "Expiration" : "2023-03-28T18:51:13Z"
}

[/simterm]

А використовуючи ці Access/Secret ключі, ми можемо робити все, що дозволено інстансу, і, наприклад, якщо до інстансу підключено ІAМ роль з AdminAccess – ми зможемо отримати ці права.

Перевіримо – додамо до інстансу роль з доступом до S3-бакетів:

Перевіряємо роль в метаданих:

[simterm]

root@ip-172-31-30-97:/home/ubuntu# curl http://169.254.169.254/latest/meta-data/iam/info
{
  "Code" : "Success",
  "LastUpdated" : "2023-03-28T13:34:51Z",
  "InstanceProfileArn" : "arn:aws:iam::514***799:instance-profile/IMDSTestS3ReadOnlyAccess",
  "InstanceProfileId" : "AIPAXPNJUS3H7XEB7UT24"
}

[/simterm]

Та отримаємо ключі та токен:

[simterm]

root@ip-172-31-30-97:/home/ubuntu# curl http://169.254.169.254/latest/meta-data/iam/security-credentials/IMDSTestS3ReadOnlyAccess
{
  "Code" : "Success",
  "LastUpdated" : "2023-03-28T13:35:16Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASI***3PJ",
  "SecretAccessKey" : "IfC***t2n",
  "Token" : "IQo***o4a",
  "Expiration" : "2023-03-28T20:09:51Z"
}

[/simterm]

Додаємо їх собі на робочу машину в ~/.aws/credentials:

[testiam]
aws_access_key_id = ASI***3PJ
aws_secret_access_key = IfC***t2n
aws_session_token = IQo***o4a

Та створимо профіль у ~/.aws/config:

[profile testiam]
region = eu-central-1
output = json

І отримаємо доступ:

[simterm]

$ aws --profile testiam s3 ls
2022-12-19 16:43:06 cronjob-test
2023-02-14 15:02:28 gitlab-s3-cache-test
...

[/simterm]

Жах! :scream:

Права доступу до IMDS

Щоб запобігти такому, можна просто відключити IMDS взагалі, або використовувати IMDS v2.

Відключення IMDS

Для ЕС2 робиться через AWS CLI:

[simterm]

$ aws --profile internal ec2 modify-instance-metadata-options --instance-id i-0b0c0e351255ba78c --http-endpoint disabled
{
    "InstanceId": "i-0b0c0e351255ba78c",
    "InstanceMetadataOptions": {
        "State": "pending",
        "HttpTokens": "optional",
        "HttpPutResponseHopLimit": 1,
        "HttpEndpoint": "disabled",
        "HttpProtocolIpv6": "disabled",
        "InstanceMetadataTags": "disabled"
    }
}

[/simterm]

Або через AWS Console – EC2 > Instance Settings > Modify instance metadata options:

І тепер при запиті до meta-data маємо помилку 403 – Forbidden:

[simterm]

root@ip-172-31-30-97:/home/ubuntu# curl http://169.254.169.254/latest/meta-data/iam/info
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
  <title>403 - Forbidden</title>
...

[/simterm]

Переключення на IMDS v2

Якщо доступ все ж треба, то можемо відключити IMDS v1 та використовувати тільки IMDS v2 додавши обов’язкове використання токена за допомогою параметру --http-tokens:

[simterm]

$ aws --profile internal ec2 modify-instance-metadata-options --instance-id i-0b0c0e351255ba78c --http-endpoint enabled --http-tokens required
{
    "InstanceId": "i-0b0c0e351255ba78c",
    "InstanceMetadataOptions": {
        "State": "pending",
        "HttpTokens": "required",
        "HttpPutResponseHopLimit": 1,
        "HttpEndpoint": "enabled",
        "HttpProtocolIpv6": "disabled",
        "InstanceMetadataTags": "disabled"
    }
}

[/simterm]

Тепер при запиті маємо 401 – Unauthorized:

[simterm]

root@ip-172-31-30-97:/home/ubuntu# curl http://169.254.169.254/latest/meta-data/iam/info
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
  <title>401 - Unauthorized</title>
...

[/simterm]

Але якщо додамо токен – то все працюватиме.

Отримаємо токен:

[simterm]

root@ip-172-31-30-97:/home/ubuntu# TOKEN=`curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`

[/simterm]

Глянемо його:

[simterm]

root@ip-172-31-30-97:/home/ubuntu# echo $TOKEN
AQA***Vyg==

[/simterm]

І тепер знову curl з хедером X-aws-ec2-metadata-token:

[simterm]

root@ip-172-31-30-97:/home/ubuntu# curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/info
{
  "Code" : "Success",
  "LastUpdated" : "2023-03-28T13:34:51Z",
  "InstanceProfileArn" : "arn:aws:iam::514***799:instance-profile/IMDSTestS3ReadOnlyAccess",
  "InstanceProfileId" : "AIPAXPNJUS3H7XEB7UT24"
}

[/simterm]

При використані Terraform модулів для створення Node Groups, звертайте увагу на опції. Наприклад, у cloudposse/terraform-aws-eks-node-group по дефолту включена IMDSv2, див. Behavior changes.

IMDS v2 та Docker

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

[simterm]

root@64cbbd918977:/# curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" 
root@64cbbd918977:/# echo $?
56

[/simterm]

Щоб запобігти цьому, додаємо параметр http-put-response-hop-limit зі значенням більше 1, так як виклик з контейнеру додає ще один хоп при проходженні запросу від клієнта до IMDS: перший, це запит з самого хосту, а другий – із контейнера на цьому хості:

[simterm]

$ aws --profile internal ec2 modify-instance-metadata-options --http-endpoint enabled --http-tokens required --http-put-response-hop-limit 2 --instance-id i-0b0c0e351255ba78c
{
    "InstanceId": "i-0b0c0e351255ba78c",
    "InstanceMetadataOptions": {
        "State": "pending",
        "HttpTokens": "required",
        "HttpPutResponseHopLimit": 2,
        "HttpEndpoint": "enabled",
...

[/simterm]

І пробуємо зараз:

[simterm]

root@64cbbd918977:/# TOKEN=`curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
root@64cbbd918977:/# curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/info
{
  "Code" : "Success",
  "LastUpdated" : "2023-04-10T10:46:45Z",
  "InstanceProfileArn" : "arn:aws:iam::514***799:instance-profile/IMDSTestS3ReadOnlyAccess",
  "InstanceProfileId" : "AIP***T24"
}

[/simterm]

Готово.

Loading

Knative: Serverless для Kubernetes – огляд можливостей та запуск у Minikube
0 (0)

6 Квітня 2023

Knative – система, яка дозволяє використовувати Serverless модель розробки у Kubernetes. По суті, Knative можна уявляти собі як ще один рівень абстракції, який дозволяє девелоперам не поринати в деталі деплойменту, скелінгу та нетворкінгу у “vanilla” Kubernetes.

Розробка самого Knative була розпочта у Google за співучастю таких компаній, як IBM, Pivotal, Red Hat, та загалом має близько 50 компаній-контрібьюторів.

What is: Serverless computing

Але спочатку, давайте розглянемо що таке Serverless взагалі.

Отже, Server та Less, це модель розробки, коли вам не потрібно перейматись менеджментом серверів – все це бере на себе cloud-провайдер, який надає вам послугу Serverless computing.

Тобто, зазвичай маємо в клауді:

  • bare-metal сервери десь в дата-центрі
  • на яких запускаються віртуальні машини
  • на яких ми запускаємо контейнери

Serverless computing поверх цих шарів додає ще один, де ви можете запускати вашу функцію, тобто мінімальний deployable-юніт, не займаючись ані металом, ані віртуалками, ані контейнерами. Ви просто маєте код, який в пару кліків можна запустити в клауді, а всі задачі по менеджменту інфрастуктури бере на себе клауд-провайдер. Ви не маєте турбуватись ані про хай авайлабіліті, ні про failt-tolerance, ні про бекапи, ні про секьюріті-патчі, ні про бекапи, ні про моніторинг і логування того, що відбувається на рівні інфрастуктури. Ба більше – на рівні мережі вам не треба думати про лоад-балансінг та те, як розподіляти навантаження по вашому сервісу – ви просто приймаєте запити на API Gateway або налаштовуєте триггер на event, який триггерить вашу Функцію. Тобто, cloud provider предоставляє вам послугу Function-as-a-Service, FaaS.

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

“Запусти, та радуйся!”

Крім того, в FaaS інша модель оплати за сервіс – замість того, щоб платити за запущені сервери не зважаючи на те, чи виконують вони якусь роботу, чи просто знаходяться у idle стейті, при використанні FaaS ви платите тільки за той час, коли ваша функція виконується – Pay-for-Use Services.

Отже, ви:

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

Серед провайдерів FaaS – Amazon Web Services з його AWS Lambda, Microsoft Azure та Azure Functions, Google Cloud та Cloud Functions, і IBM Cloud з його IBM Cloud Functions.

Детальніше про Serverless – у CNCF Serverless Whitepaper.

Serverless use cases

Serverless модель буде ідеальною для рішень, які можуть працювати асінхронно та не потребують збереження стану, тобто являються Stateless системами.

Наприклад, це може бути функція в AWS, котора при створенні нового ЕС2 в аккаунті буде автоматично додавати теги до неї, накшталт AWS: Lambda – копирование тегов EC2 на EBS, часть 2 – создание Lambda-функции, коли ми використовуємо CloudWatch, який при створенні ЕС2 створює event, який триггерить функцю, передаючи їй аргументом ID EC2-інстансу, для дисків якого треба додати теги: функція запускається по цьому триггеру, додає теги, та зупинятється до наступного виклику.

Self-hosted serverless

Для використання Serverless моделі, необов’язково прив’язуватись до FaaS-провайдеру, натомість можна запустити self-hosted сервіс наприклад в своєму Kubernetes-кластері, і таким чином уникнути vendor lock.

Серед таких рішень:

  • KubelessIron FunctionsSpace CloudOpenLambdaFunktion, OpenWhisk та Fn Project: наразі, більше мертві, ніж живі, хоча деякі досі мають оновлення
  • Fission: має релізи, тобто розвивається, але займає тільки 2%
  • OpenFaaS:  використовується у 10% випадків
  • Knative: наразі являється найбільш популярним (27%) та найбільше активно розвиваючимся проектом

Knative vs AWS Lambda

Але навіщо взагалі морочитись із запуском власного Serverless?

  • маєте можливість обійти ліміти, задані провайдером – див. AWS Lambda quotas
  • уникаєте vendor lock, тобто маєте можливість швидко переїхати до іншого провайдера або використовувати multi-cloud архітектуру для більшої надійності
  • маєте більше можливостей для моніторингу, трейсінгу та роботи з логами, бо в Kubernetes ми можемо все, на відміну від AWS Lambda та її прив’язки до CloudWatch

Доречі, Knative має змогу запускати функції, розроблені для AWS Labmda завдяки Knative Lambda Runtimes.

Компоненти та архітектура Knative

Knative має два основні компоненти – Knative Serving та Knative Eventing.

Для контролю мережи, роутів та ревізій Knative використовує Istio (хоча може бути і інший, див. Configuring the ingress gateway).

Knative Serving

Knative Serving відповідає за деплоймент контейнеру, оновлення, мережу та автоскейлінг.

Роботу Knative Serving можна відобразити наступним чином:

Трафик від клієнта приходить на Ingress, та в залежності від запиту відправляється на конкретний Knative Service, який являє собою Kubernetes Service та Kubernetes Deployment.

В залежності від конфігурації конкретного Knative Service, трафік може бути розподіленний між різними ревізіями, тобто версіями самого application.

KPA – Knative Pod Autoscaler, перевіряє кількість запитів та при необхідності додає нові поди в Deployment. Якщо трафіка нема – то KPA скейлить поди в нуль, а коли з’являються нові запити від клієнтів – запускає поди та скейлить їх в залежності від кількості запитів, які треба обробити.

Основні типи ресурсів:

  • Service: service.serving.knative.dev відповідає за весь цикл життя вашого workload (тобто, деплойменту та зв’язанних з ним ресурсів) – контролює створення залежних ресурсів, таких як роути, конфігурації, ревізії
  • Routes: route.serving.knative.dev відповідає за зв’язок між ендпоінтом та ревізіями
  • Configurations: configuration.serving.knative.dev відповідає за необхідний стан (desired state) деплойменту та створює revisions, на які буде потрапляти трафік
  • Revisions: revision.serving.knative.dev являє собою point-in-time snapshot коду та конфігурацї для кожної зміни у workload

Див. Resource Types.

Knative Eventing

Knative Eventing відповідає за асинхронний зв’язок розподіленних частин вашої системи. Замість того, щоб робити їх залежними друг від друга, вони можуть створювати events (event producers), які будуть отримані іншим компонентом системи (event consumers або subcsribers, або в термінології Knative – sinks), тобто реалізувати event-driven architecture.

Knative Eventing використовує стандартні запроси HTTP POST для відправлення та отримання таких івентів, які мають відповідати специфікації CloudEvents.

Knative Eventing дозволяє створити:

  • Source to Sink: Source (event producers) відправляє івент (чи таки евент?) до Sink, який його обробляє. В ролі Source може бути PingSource, APIServerSource (Kubernetes API івенти), Apache Kafka, GitHub, тощо
  • Channel and Subscription: створює event pipe, коли івент потрапивши до Channel відразу відправляється до subscriber
  • Broker and Trigger: являє собою event mesh – івент, потрапляє до Broker, який має одного чи більше Trigger, котрі має фільтри, в залежності від яких отримється від Брокера ці events

Install Knative in Minikube

Компоненти Knative можна запустити за допомогою YAML-маніфестів (див. Install Knative Serving by using YAML та Install Knative Eventing by using YAML), використовуючи Knative Operator, або ж за допомогою Knative Quickstart plugin до Knative CLI.

Для знайомства нам підійде і quickstart плагін, тож використаємо його, а запускати будемо у Minikube.

Install Knative CLI

Див. Installing kn.

Для Arch-based дистрибутивів – за допомогою pacman із репозиторію:

[simterm]

$ sudo pacman -S knative-client

[/simterm]

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

[simterm]

$ kn version
Version:      
Build Date:   
Git Revision: 
Supported APIs:
* Serving
  - serving.knative.dev/v1 (knative-serving v0.34.0)
* Eventing
  - sources.knative.dev/v1 (knative-eventing v0.34.1)
  - eventing.knative.dev/v1 (knative-eventing v0.34.1)

[/simterm]

Запуск Knative з Quickstart plugin

Див. kn-plugin-quickstart.

Знаходимо останню версію на сторінці релізів, вибираємо потрібну версію, в моєму випадку це буде kn-quickstart-linux-amd64, та завантажуємо у каталог, який є в $PATH:

[simterm]

$ wget -O /usr/local/bin/kn-quickstart https://github.com/knative-sandbox/kn-plugin-quickstart/releases/download/knative-v1.8.2/kn-quickstart-linux-amd64
$ chmod +x /usr/local/bin/kn-quickstart

[/simterm]

Перевіряємо (у CLI навіть є автокомпліт):

[simterm]

$ kn plugin list
- kn-quickstart : /usr/local/bin/kn-quickstart

[/simterm]

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

[simterm]

$ kn quickstart --help
Get up and running with a local Knative environment

Usage:
  kn-quickstart [command]
  ...

[/simterm]

Quickstart плагін замість Istio встановить Kourier, а також створить Minikube кластер та встановить Knative Serving з sslip.io в ролі DNS і Knative Eventing.

Запускаємо:

[simterm]

$ kn quickstart minikube
Running Knative Quickstart using Minikube
Minikube version is: v1.29.0
...
🏄  Done! kubectl is now configured to use "knative" cluster and "default" namespace by default


To finish setting up networking for minikube, run the following command in a separate terminal window:
    minikube tunnel --profile knative
The tunnel command must be running in a terminal window any time when using the knative quickstart environment.

Press the Enter key to continue
...

[/simterm]

В іншому терміналі створюємо minikube-тунель для доступу з хост-машини:

[simterm]

$ minikube tunnel --profile knative &

[/simterm]

Повертаємось до попереднього вікна:

[simterm]

...
🍿 Installing Knative Serving v1.8.5 ...
...
🕸 Installing Kourier networking layer v1.8.3 ...
...
🕸 Configuring Kourier for Minikube...
...
🔥 Installing Knative Eventing v1.8.8 ...
...
🚀 Knative install took: 3m23s 
🎉 Now have some fun with Serverless and Event Driven Apps!

[/simterm]

Перевіряємо кластера Minikube:

[simterm]

$ minikube profile list
|---------|-----------|---------|--------------|------|---------|---------|-------|--------|
| Profile | VM Driver | Runtime |      IP      | Port | Version | Status  | Nodes | Active |
|---------|-----------|---------|--------------|------|---------|---------|-------|--------|
| knative | docker    | docker  | 192.168.49.2 | 8443 | v1.24.3 | Running |     1 |        |
|---------|-----------|---------|--------------|------|---------|---------|-------|--------|

[/simterm]

Неймспейси в Kubernetes:

[simterm]

$ kubectl get ns
NAME               STATUS   AGE
default            Active   7m30s
knative-eventing   Active   5m54s
knative-serving    Active   6m43s
kourier-system     Active   6m23s
...

[/simterm]

Поди в kourier-system:

[simterm]

$ kk -n kourier-system get pod
NAME                                      READY   STATUS    RESTARTS   AGE
3scale-kourier-gateway-7b89ff5c79-hwmpr   1/1     Running   0          7m26s

[/simterm]

Та в knative-eventing:

[simterm]

$ kk -n knative-eventing get pod
NAME                                    READY   STATUS    RESTARTS   AGE
eventing-controller-5c94cfc645-8pxgk    1/1     Running   0          7m10s
eventing-webhook-5554dc76bc-656p4       1/1     Running   0          7m10s
imc-controller-86dbd688db-px5z4         1/1     Running   0          6m39s
imc-dispatcher-5bfbdfcd85-wvpc7         1/1     Running   0          6m39s
mt-broker-controller-78dcfd9768-ttn7t   1/1     Running   0          6m28s
mt-broker-filter-687c575bd4-rpzk7       1/1     Running   0          6m28s
mt-broker-ingress-59d566b54-8t4x7       1/1     Running   0          6m28s

[/simterm]

І в knative-serving:

[simterm]

$ kk -n knative-serving get pod
NAME                                      READY   STATUS      RESTARTS   AGE
activator-66b65c899d-5kqsm                1/1     Running     0          8m17s
autoscaler-54cb7cd8c6-xltb9               1/1     Running     0          8m17s
controller-8686fd49f-976dk                1/1     Running     0          8m17s
default-domain-mk8tf                      0/1     Completed   0          7m34s
domain-mapping-559c8cdcbb-fs7q4           1/1     Running     0          8m17s
domainmapping-webhook-cbfc99f99-kqptw     1/1     Running     0          8m17s
net-kourier-controller-54f9c959c6-xsgp8   1/1     Running     0          7m57s
webhook-5c5c86fb8b-l24sh                  1/1     Running     0          8m17s

[/simterm]

Тепер давайте подвимось, що Knative вміє.

Knative Functions

Functions дозволяє швидко розробляти функції без необхідності писати Dockerfile та взагалі знати Knative та Kubernetes.

Див. About Knative Functions.

Додаємо ще один плагін – kn-func:

[simterm]

$ wget -O /usr/local/bin/kn-func https://github.com/knative/func/releases/download/knative-v1.9.3/func_linux_amd64
$ chmod +x /usr/local/bin/kn-func

[/simterm]

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

[simterm]

$ kn plugin list
- kn-func : /usr/local/bin/kn-func
- kn-quickstart : /usr/local/bin/kn-quickstart

[/simterm]

Логінимось до Docker Registry (хоча якщо не зробити зараз – то Knative запросить логін під час деплою):

[simterm]

$ docker login

[/simterm]

func create

За допомогою func create створимо функцію на Python, див. Creating a function:

[simterm]

$ kn func create -l python hello-world
Created python function in /home/setevoy/Scripts/Knative/hello-world

[/simterm]

На робочій машині буде створено директорію з її кодом:

[simterm]

$ ll hello-world/
total 28
-rw-r--r-- 1 setevoy setevoy   28 Apr  5 12:36 Procfile
-rw-r--r-- 1 setevoy setevoy  862 Apr  5 12:36 README.md
-rwxr-xr-x 1 setevoy setevoy   55 Apr  5 12:36 app.sh
-rw-r--r-- 1 setevoy setevoy 1763 Apr  5 12:36 func.py
-rw-r--r-- 1 setevoy setevoy  390 Apr  5 12:36 func.yaml
-rw-r--r-- 1 setevoy setevoy   28 Apr  5 12:36 requirements.txt
-rw-r--r-- 1 setevoy setevoy  258 Apr  5 12:36 test_func.py

[/simterm]

У файлі func.yaml описується її конфіг:

[simterm]

$ cat hello-world/func.yaml 
specVersion: 0.35.0
name: hello-world
runtime: python
registry: ""
image: ""
imageDigest: ""
created: 2023-04-05T12:36:46.022291382+03:00
build:
  buildpacks: []
  builder: ""
  buildEnvs: []
run:
  volumes: []
  envs: []
deploy:
  namespace: ""
  remote: false
  annotations: {}
  options: {}
  labels: []
  healthEndpoints:
    liveness: /health/liveness
    readiness: /health/readiness

[/simterm]

А в func.py – сам код, який буде запускатись:

[simterm]

$ cat hello-world/func.py   
from parliament import Context
from flask import Request
import json


# parse request body, json data or URL query parameters
def payload_print(req: Request) -> str:
...

[/simterm]

func run

func run дозволяє перевірити, як функція буде працювати без необхідності її деплою до Knative. Для цього Knative CLI створить образ за допомогою вашого container runtime, та запустить його. Див. Running a function.

Запускаємо, вказуючи шлях до коду:

[simterm]

$ kn func run --path hello-world/
   🙌 Function image built: docker.io/setevoy/hello-world:latest
Function already built.  Use --build to force a rebuild.
Function started on port 8080

[/simterm]

Перевіряємо Docker-контейнери:

[simterm]

$ docker ps
CONTAINER ID   IMAGE                      COMMAND            CREATED          STATUS          PORTS                     NAMES
3869fa00ac51   setevoy/hello-world:latest "/cnb/process/web" 52 seconds ago   Up 50 seconds   127.0.0.1:8080->8080/tcp  sweet_vaughan

[/simterm]

Та пробуємо curl:

[simterm]

$ curl localhost:8080
{}

[/simterm]

В аутпуті func run:

[simterm]

Received request
GET http://localhost:8080/ localhost:8080
  Host: localhost:8080
  User-Agent: curl/8.0.1
  Accept: */*
URL Query String:
  {}

[/simterm]

Або можна перевірити за допомогою func invoke:

[simterm]

$ kn func invoke --path hello-world/
Received response
{"message": "Hello World"}

[/simterm]

І в самій функцї:

[simterm]

Received request
POST http://localhost:8080/ localhost:8080
  Host: localhost:8080
  User-Agent: Go-http-client/1.1
  Content-Length: 25
  Content-Type: application/json
  Accept-Encoding: gzip
Request body:
  {"message": "Hello World"}

[/simterm]

func deploy

func deploy створить образ, запушить його до registry, та задеплоїть у Kubernetes як Knative Service.

Якшо в коді зробити зміни, або просто виконати func build – то в Knative для функції буде створено нову ревізію, на яку буде переключений відповідний route:

[simterm]

$ kn func deploy --build --path hello-world/ --registry docker.io/setevoy
   🙌 Function image built: docker.io/setevoy/hello-world:latest
   ✅ Function updated in namespace "default" and exposed at URL: 
   http://hello-world.default.10.106.17.160.sslip.io

[/simterm]

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

[simterm]

$ kk get pod 
NAME                                           READY   STATUS    RESTARTS   AGE
hello-world-00003-deployment-74dc7fcdd-7p6ql   2/2     Running   0          9s

[/simterm]

Kubernetes Service:

[simterm]

$ kk get svc 
NAME                                    TYPE           CLUSTER-IP       EXTERNAL-IP                                         PORT(S)                                              AGE
example-broker-kne-trigger-kn-channel   ExternalName   <none>           imc-dispatcher.knative-eventing.svc.cluster.local   80/TCP                                               33m
hello-world                             ExternalName   <none>           kourier-internal.kourier-system.svc.cluster.local   80/TCP                                               3m50s
hello-world-00001                       ClusterIP      10.109.117.53    <none>                                              80/TCP,443/TCP                                       4m1s
hello-world-00001-private               ClusterIP      10.105.142.206   <none>                                              80/TCP,443/TCP,9090/TCP,9091/TCP,8022/TCP,8012/TCP   4m1s
hello-world-00002                       ClusterIP      10.107.71.130    <none>                                              80/TCP,443/TCP                                       2m58s
hello-world-00002-private               ClusterIP      10.108.40.73     <none>                                              80/TCP,443/TCP,9090/TCP,9091/TCP,8022/TCP,8012/TCP   2m58s
hello-world-00003                       ClusterIP      10.103.142.53    <none>                                              80/TCP,443/TCP                                       2m37s
hello-world-00003-private               ClusterIP      10.110.95.58     <none>                                              80/TCP,443/TCP,9090/TCP,9091/TCP,8022/TCP,8012/TCP   2m37s

[/simterm]

Або за допомогою Knative CLI:

[simterm]

$ kn service list
NAME          URL                                                 LATEST              AGE     CONDITIONS   READY   REASON
hello-world   http://hello-world.default.10.106.17.160.sslip.io   hello-world-00003   4m41s   3 OK / 3     True    

[/simterm]

Або можна за kubectl отримати ресурс kvcs:

[simterm]

$ kubectl get ksvc
NAME          URL                                                 LATESTCREATED       LATESTREADY         READY   REASON
hello         http://hello.default.10.106.17.160.sslip.io         hello-00001         hello-00001         True    
hello-world   http://hello-world.default.10.106.17.160.sslip.io   hello-world-00003   hello-world-00003   True

[/simterm]

Визиваємо нашу функцію:

[simterm]

$ kn func invoke --path hello-world/
Received response
{"message": "Hello World"}

[/simterm]

І через хвилину відповідний Pod буде вбито, тобто Deployment заскейлиться в нуль:

[simterm]

$ kk get pod 
NAME                                           READY   STATUS        RESTARTS   AGE
hello-world-00003-deployment-74dc7fcdd-7p6ql   2/2     Terminating   0          79s

[/simterm]

Knative Serving

Functions – чудово, але ж ми начебто інженери, і нам цікаво щось більш наближене до Kubernetes, тож давайте глянемо на Knative Service.

Як вже говорилось, Knative Service – це “повний workload”, включаючи Kubernetes Service, Kubernetes Deployment, Knative Pod Autoscaler та відповідні роути і конфіги. Див. Deploying a Knative Service.

Тут використаємо YAML маніфест (хоча можно і kn service create), в якому описано образ, з якого буде створено под:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: hello
spec:
  template:
    spec:
      containers:
        - image: gcr.io/knative-samples/helloworld-go
          ports:
            - containerPort: 8080
          env:
            - name: TARGET
              value: "World"

Деплоїмо до Kubernetes:

[simterm]

$ kubectl apply -f knative-hello-go-svc.yaml 
service.serving.knative.dev/hello created

[/simterm]

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

[simterm]

$ kn service list
NAME          URL                                                 LATEST              AGE     CONDITIONS   READY   REASON
hello         http://hello.default.10.106.17.160.sslip.io         hello-00001         70s     3 OK / 3     True    

[/simterm]

Пробуємо з curl:

[simterm]

$ curl http://hello.default.10.106.17.160.sslip.io
Hello World!

[/simterm]

Глянемо поди:

[simterm]

$ kk get pod
NAME                                      READY   STATUS    RESTARTS   AGE
hello-00001-deployment-5897f54974-wdhq8   2/2     Running   0          10s

[/simterm]

Та Kubernetes Deployments:

[simterm]

$ kk get deploy
NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
hello-00001-deployment         1/1     1            1           19m
hello-world-00001-deployment   0/0     0            0           26m
hello-world-00002-deployment   0/0     0            0           25m
hello-world-00003-deployment   0/0     0            0           24m

[/simterm]

Autoscaling

Knative Serving використовує власний Knative Pod Autoscaler (KPA), хоча можна замість нього створити “класичний” Horizontal Pod Autoscaler (HPA).

KPA підтримує scale to zero, але не вміє в CPU-based autoscaling. Крім того, у KPA можемо використовувати concurrency або requests per second метрики для скейлінгу.

HPA ж навпаки – вміє CPU-based autoscaling (і багато іншого), але не знає як скейлити в нуль, а для concurrency або RPS йому потрібні додаткові налаштування (див. Kubernetes: HorizontalPodAutoscaler – обзор и примеры).

Ми вже маємо KPA, які були створені при func deploy та під час створення нашого Knative Service:

[simterm]

$ kk get kpa
NAME                DESIREDSCALE   ACTUALSCALE   READY   REASON
hello-00001         0              0             False   NoTraffic
hello-world-00001   0              0             False   NoTraffic
hello-world-00002   0              0             False   NoTraffic
hello-world-00003   0              0             False   NoTraffic

[/simterm]

Запустимо curl в 10 потоків:

[simterm]

$ seq 1 10 | xargs -n1 -P 10 curl http://hello.default.10.106.17.160.sslip.io

[/simterm]

А в іншому вікні термінала глянемо на kubectl get pod --watch:

[simterm]

$ kk get pod -w
NAME                                      READY   STATUS              RESTARTS   AGE
hello-00001-deployment-5897f54974-9lxwl   0/2     ContainerCreating   0          1s
hello-00001-deployment-5897f54974-9lxwl   1/2     Running             0          2s
hello-00001-deployment-5897f54974-9lxwl   2/2     Running             0          2s

[/simterm]

“Воу, воно скейлиться!” 🙂

Knative Revisions

Knative завдяки Istio (або Kourier чи Kong) вміє розподіляти трафік між різними версіями (revisions) системи, що дозволяє виконувати blue/green або canary deployments.

Див. Traffic splitting.

В нашому Knative Service ми задавали змінну оточення $TARGET зі значенням World – давайте замінимо його на Knative:

[simterm]

$ kn service update hello --env TARGET=Knative
Updating Service 'hello' in namespace 'default':

  0.025s The Configuration is still working to reflect the latest desired specification.
  2.397s Traffic is not yet migrated to the latest revision.
  2.441s Ingress has not yet been reconciled.
  2.456s Waiting for load balancer to be ready
  2.631s Ready to serve.

Service 'hello' updated to latest revision 'hello-00002' is available at URL:
http://hello.default.10.106.17.160.sslip.io

[/simterm]

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

[simterm]

$ kn revision list
NAME                SERVICE       TRAFFIC   TAGS   GENERATION   AGE   CONDITIONS   READY   REASON
hello-00002         hello         100%             2            20s   4 OK / 4     True    
hello-00001         hello                          1            24m   3 OK / 4     True    

[/simterm]

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

[simterm]

$ curl http://hello.default.10.106.17.160.sslip.io
Hello Knative!

[/simterm]

Тепер спробуємо поділити трафік – 50 відсотків відправити на попередню ревізію, а 50% – на нову:

[simterm]

$ kn service update hello --traffic hello-00001=50 --traffic @latest=50

[/simterm]

Глянемо revisions зараз:

[simterm]

$ kn revision list
NAME                SERVICE       TRAFFIC   TAGS   GENERATION   AGE    CONDITIONS   READY   REASON
hello-00002         hello         50%              2            115s   4 OK / 4     True    
hello-00001         hello         50%              1            26m    4 OK / 4     True    

[/simterm]

Та знов пробуємо curl, повторюємо кілька раз:

[simterm]

$ curl http://hello.default.10.106.17.160.sslip.io
Hello Knative!
$ curl http://hello.default.10.106.17.160.sslip.io
Hello World!
$ curl http://hello.default.10.106.17.160.sslip.io
Hello Knative!
$ curl http://hello.default.10.106.17.160.sslip.io
Hello World!

[/simterm]

Nice!

Knative Eventing

Knative Eventing – це набор сервісів та ресурсів, які дозволяють нам побудувати event-driven applications, коли Функції викликаються якимось евентом. Для цього можемо підключити sources, які ці події будуть генерувати, та sinks, тобто “споживачі”, які на ці події реагують.

У подальшій документації по Quickstart є приклад роботи з Broker та Trigger за допомогою cloudevents-player, але як на мене, там не дуже-то демонструються можливості Knative Eventing, тож виберемо трошки the hard way.

Як вже писалось, Knative підтримує три типи реалізації event-driven системи – Source to Sink, Channel and Subscription та Broker and Trigger.

Source to Sink

Source to Sink – сама базова модель, створюється за допомогою ресурсів Source та Sink, де Source – це Event Producer, а Sink – ресурс, який можна визвати або адресувати йому повідомлення.

Серед прикладів Sources – APIServerSource (Kubernetes API server), GitHub та GitLab, RabbitMQ та інші, див. Event sources.

В ролі Sink може бути Knative Service, Channel або Broker (тобто, “раковина”, куди ми “зливаємо” наші івенти). Хоча при побудові саме Source to Sink моделі, в ролі sink має бути саме Knative Service – про Channel та Broker поговоримо далі.

Channel and Subscription

Channel та Subscription являє собой even pipe (як pipe в bash, коли ми через | перенаправляємо stdout одніє програми у stdin іншої).

Тут Channel – це інтерфейс між event source та subscriber цього каналу. При цьому channel являє собою тип sink, так як він може зберігати івенти, щоб потім віддати їх своїм subscribers.

Knative підтримує три типи каналів:

  • In-memory Channel
  • Apache Kafka Channel
  • Google Cloud Platform Pub-sub Channel

In-memory Channel – тип за-замовченням, і не має змоги відновити івенти або зберігати їх постійно – для цього використовуйте типи накшталт Apache Kafka Channel.

Далі, Subscription – відповідає за з’єднання Channel з відповідним Knative Service – Сервіси підписуються на Канал через Subscription, та починають отримувати з Каналу повідомлення.

Broker and Trigger

Broker та Trigger являють собою event mesh модель, дозволяючи передавати події необхідним сервісам.

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

Приклад створення Source to Sink

Створимо Knative Service, який буде нашим sink, тобто отримувачем:

[simterm]

$ kn service create knative-hello --concurrency-target=1 --image=quay.io/redhattraining/kbe-knative-hello:0.0.1
...
Service 'knative-hello' created to latest revision 'knative-hello-00001' is available at URL:
http://knative-hello.default.10.106.17.160.sslip.io

[/simterm]

concurrency-target тут вказує, що наш Service може обробляти тільки один запит одночасно. Якщо їх буде більше – то відповідний KPA створить додаткові поди.

Створюємо Event Source, наприклад – PingSource, який кожну хвилину буде слати повідомлення в вигляді JSON до нашого knative-hello Service:

[simterm]

$ kn source ping create knative-hello-ping-source --schedule "* * * * *" --data '{"message": "Hello from KBE!"}' --sink ksvc:knative-hello
Ping source 'knative-hello-ping-source' created in namespace 'default'.

[/simterm]

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

[simterm]

$ kn source list
NAME                        TYPE         RESOURCE                          SINK                 READY
knative-hello-ping-source   PingSource   pingsources.sources.knative.dev   ksvc:knative-hello   True

[/simterm]

І глянемо логи поду з Service knative-hello:

[simterm]

$ kubectl logs -f knative-hello-00001-deployment-864756c67d-sk76m
...
2023-04-06 10:23:00,329 INFO  [eventing-hello] (executor-thread-1) ce-id=bd1093a9-9ab7-4b79-8aef-8f238c29c764
2023-04-06 10:23:00,331 INFO  [eventing-hello] (executor-thread-1) ce-source=/apis/v1/namespaces/default/pingsources/knative-hello-ping-source
2023-04-06 10:23:00,331 INFO  [eventing-hello] (executor-thread-1) ce-specversion=1.0
2023-04-06 10:23:00,331 INFO  [eventing-hello] (executor-thread-1) ce-time=2023-04-06T10:23:00.320053265Z
2023-04-06 10:23:00,332 INFO  [eventing-hello] (executor-thread-1) ce-type=dev.knative.sources.ping
2023-04-06 10:23:00,332 INFO  [eventing-hello] (executor-thread-1) content-type=null
2023-04-06 10:23:00,332 INFO  [eventing-hello] (executor-thread-1) content-length=30
2023-04-06 10:23:00,333 INFO  [eventing-hello] (executor-thread-1) POST:{"message": "Hello from KBE!"}

[/simterm]

Наразі це все, що хотілося дізнатись про Knative.

Виглядає в цілому досить цікаво, але можуть бути з автоскейлінгом та Istio, бо Istio сам по собі може бути тим ще гемороєм. Хоча на поточному проекті Knative у нас вже в production, та особливих проблем з ним поки не бачили.

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

Loading

GitLab: gitlab-shell timeouts та /metrics Connection refused
0 (0)

21 Березня 2023

Запустились ми в production, і вилізла дуже неприємна бага – при git операціях clone/pull/push запит іноді зависав на 1-2 хвилини.

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

Проблеми

gitlab-shell timeouts

Наприклад – раз нормально:

[simterm]

$ time git clone [email protected]:example/platform/tables-api.git
Cloning into 'tables-api'...
...
real    0m1.380s

[/simterm]

А потім clone того ж самого репозиторію – 2 хвилини:

[simterm]

$ time git clone [email protected]:example/platform/tables-api.git
Cloning into 'tables-api'...
...
real    2m10.497s

[/simterm]

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

На щастя, не став глибокого копатись, бо спершу вирішив зафіксити проблему з метриками, щоб мати змогу в моніторингу побачити що взагалі коїться з GitLab Shell.

gitlab-shell /metrics Connection refused

Про проблему з метриками вже говорив, коли описував налаштування моніторингу – GitLab: моніторинг – Prometheus, метрики, та Grafana dashboard, і там була проблема з метриками Git/SSH з поду gitlab-shell.

Це виглядало так: відкриваємо порт 9122 (див. values):

[simterm]

$ kk -n gitlab-cluster-prod port-forward gitlab-cluster-prod-gitlab-shell-744675c985-5t8wn 9122

[/simterm]

Пробуємо curl:

[simterm]

$ curl localhost:9122/metrics
curl: (52) Empty reply from server

[/simterm]

І под нам каже, що “Connection refused”:

[simterm]

...
Handling connection for 9122
E0315 12:40:43.712508  826225 portforward.go:407] an error occurred forwarding 9122 -> 9122: error forwarding port 9122 to pod 51856f9224907d4c1380783e46b13069ef5322ae1f286d4301f90a2ed60483c0, uid : exit status 1: 2023/03/15 10:40:43 socat[28712] E connect(5, AF=2 127.0.0.1:9122, 16): Connection refused
E0315 12:40:43.713039  826225 portforward.go:233] lost connection to pod

[/simterm]

Рішення

Як виявилось, GitLab Shell підтримує два SSH-демони – openssh та gitlab-sshd, при чьому саме openssh являється дефолтним значенням, див. values:

...## Allow to select ssh daemon that would be executed inside container
## Possible values: openssh, gitlab-sshd
sshDaemon: openssh
...

Тож, оновлюємо свій values:

...
    gitlab-shell:
      enabled: true
      metrics:
        enabled: true
      sshDaemon: gitlab-sshd
...

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

[simterm]

$ curl localhost:9122/metrics
# HELP gitlab_build_info Current build info for this GitLab Service
# TYPE gitlab_build_info gauge
gitlab_build_info{built="20230309.174623",version="v14.17.0"} 1
# HELP gitlab_shell_gitaly_connections_total Number of Gitaly connections that have been established
# TYPE gitlab_shell_gitaly_connections_total counter
gitlab_shell_gitaly_connections_total{status="ok"} 2
...

[/simterm]

Проблема з таймаутами теж вирішилась – тепер результат не більше 1 секунди – real    0m0.846s.

Loading

GitLab: моніторинг – Prometheus, метрики, та Grafana dashboard
0 (0)

11 Березня 2023

Отже, продовжуємо нашу подорож з міграцією GitLab до себе в Kubernetes. Див. попередні частини:

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

GitLab та Prometheus

Документація по моніторингу GitLab:

У нас в Kubernetes кластері розгорнутий свій Prometehus за допомогою Kube Prometheus Stack (далі – KPS) та його Prometheus Operator.

GitLab вміє запускати власний Prometheus, якому відразу налаштовує збір метрик з усіх подів та сервісів, які мають аннотацію gitlab.com/prometheus_scrape=true.

Крім того, всі поди та сервіси мають аннотацію prometheus.io/scrape=true, але KPS не вміє працювати з аннотаціями, див. документацію:

The prometheus operator does not support annotation-based discovery of services

Тож маємо два варіанти збору метрик:

  • вимкнути Promethus самого GitLab, та через ServiceMonitor-и збирати метрики з компонентів відразу в KPS Prometheus – але тоді всім компонентам доведеться включати ServiceMonitor (і не всі їх мають, тож деякі доведеться додавати вручну через окремі маніфести)
  • або ми можемо лишити “вбудований” Prometehus, в якому вже все налаштовано, і через Prometheus federation просто збирати потрібні нам метрики до KPS Prometheus

В другому випадку ми будемо витрачати зайві ресурси на роботу додаткового Prometheus, але знімаємо з себе необхідність в додатковій конфігурації чартів самого GitLab та Prometheus з KPS.

Налаштування Prometheus federation

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

Спочатку, перевіримо налаштування Prometheus самого GitLab – чи є метрики і які є джоби.

Знаходимо Prometheus Service:

[simterm]

$ kk -n gitlab-cluster-prod get svc gitlab-cluster-prod-prometheus-server
NAME                                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
gitlab-cluster-prod-prometheus-server   ClusterIP   172.20.194.14   <none>        80/TCP    27d

[/simterm]

Відкриваємо до нього доступ:

[simterm]

$ kk -n gitlab-cluster-prod port-forward svc/gitlab-cluster-prod-prometheus-server 9090:80

[/simterm]

Заходимо в браузері на http://localhost:9090, переходимо в Status > Configuration, та дивимось які там є джоби:

Далі ще є job_name: kubernetes-service-endpoints та job_name: kubernetes-services, але ніяких метрик по ним зараз нема:

Джоби prometheus та kubernetes-apiservers нам не потрібні, бо це лише ганяти зайві метрики в KPS Prometheus: в job=prometheus метрики по самому GitLab Prometheus, в job=kubernetes-apiservers – дані по Kubernetes API, які Prometheus KPS збирає і так.

Перевіримо, що метрики в GitLab Prometheus взагалі є. Візьмемо, наприклад, метрику sidekiq_concurrency, див. GitLab Prometheus metrics:

Далі налаштовуємо федерацію – в values Kube Prometheus Stack в блоці prometheus додаємо additionalScrapeConfigs, де вказуємо ім’я джоби, шлях для federation, в params – задаємо match, за яким з GitLab Prometheus вибираємо тільки потрібні нам метрики, а в static_configs задаємо таргет – GitLab Prometheus Service URL:

...
      additionalScrapeConfigs:
        - job_name: 'gitlab_federation'
          honor_labels: true
          metrics_path: '/federate'
          params:
            'match[]':
              - '{job="kubernetes-pods"}'
              - '{job="kubernetes-service-endpoints"}'
              - '{job="kubernetes-services"}'
          static_configs:
          - targets: ["gitlab-cluster-prod-prometheus-server.gitlab-cluster-prod:80"]
...

Деплоїмо, та перевіряємо Targets в KPS Prometheus:

І за хвилину-дві перевіряємо чи пішли метрики до Prometheus KPS:

Метрики GitLab

Тепер, як маємо метрики в нашому Prometheus, давайте поглянемо що взагалі можно і треба моніторити в GitLab.

По-перше – це ресурси Kubernetes, але про них поговоримо, коли будемо створювати власний Grafana dashboard.

Але ще у нас є компоненти самого GitLab, які мають власні метрики:

  • PostgreSQL: моніториться власним експортером
  • KeyDB/Redis: моніториться власним експортером
  • Gitaly: віддає метрики сам, включені по дефолту, див. values
  • Runner: віддає метрики сам, виключені по дефолту, див. values
  • Shell: віддає метрики сам, виключені по дефолту, див. values
  • Registry: віддає метрики сам, виключені по дефолту, див. values
  • Sidekiq: віддає метрики сам, включені по дефолту, див. values
  • Toolbox && backups: нічого по метрикам, див. values
  • Webservice: віддає метрики сам, включені по дефолту, див. values
    • додатково метрики від workhorse, виключені по дефолту, див. values

Також є GitLab Exporter з власними метриками – values.

На сторінці GitLab Prometheus metrics є багато метрик, але не всі, тож має сенс пройтись руками по подах, та переглянути метрики прямо з сервісів.

Наприклад, у Gitaly є метрика gitaly_authentications_total, якої нема в документації.

Відкриваємо доступ до порту з метриками (є у його values):

[simterm]

$ kk -n gitlab-cluster-prod port-forward gitlab-cluster-prod-gitaly-0 9236:9236

[/simterm]

І перевіряємо їх:

[simterm]

$ curl localhost:9236/metrics
# HELP gitaly_authentications_total Counts of of Gitaly request authentication attempts
# TYPE gitaly_authentications_total counter
gitaly_authentications_total{enforced="true",status="ok"} 5511
...

[/simterm]

Далі – список цікавих (на мій власний погляд) метрик з компонентів, які можна буде потім використати для побудови Grafana dashboards per GitLab service та алертів.

Gitaly

Тут метрики:

  • gitaly_authentications_total: Counts of of Gitaly request authentication attempts
  • gitaly_command_signals_received_total: Sum of signals received while shelling out
  • gitaly_connections_total: Total number of connections to Gitaly
  • gitaly_git_protocol_requests_total: Counter of Git protocol requests
  • gitaly_gitlab_api_latency_seconds_bucket: Latency between posting to GitLab’s `/internal/` APIs and receiving a response
  • gitaly_service_client_requests_total: Counter of client requests received by client, call_site, auth version, response code and deadline_type
  • gitaly_supervisor_health_checks_total: Count of Gitaly supervisor health checks
  • grpc_server_handled_total: Total number of RPCs completed on the server, regardless of success or failure
  • grpc_server_handling_seconds_bucket: Histogram of response latency (seconds) of gRPC that had been application-level handled by the server

Runner

Тут метрики:

  • gitlab_runner_api_request_statuses_total: The total number of api requests, partitioned by runner, endpoint and status
  • gitlab_runner_concurrent: The current value of concurrent setting
  • gitlab_runner_errors_total: The number of caught errors
  • gitlab_runner_jobs: The current number of running builds
  • gitlab_runner_limit: The current value of concurrent setting
  • gitlab_runner_request_concurrency: The current number of concurrent requests for a new job
  • gitlab_runner_request_concurrency_exceeded_total: Count of excess requests above the configured request_concurrency limit

Shell

Тут чомусь не працює ендпоінт метрик, не став копатись:

[simterm]

$ kk -n gitlab-cluster-prod port-forward gitlab-cluster-prod-gitlab-shell-744675c985-5t8wn 9122:9122
Forwarding from 127.0.0.1:9122 -> 9122
Forwarding from [::1]:9122 -> 9122
Handling connection for 9122
E0311 09:36:35.695971 3842548 portforward.go:407] an error occurred forwarding 9122 -> 9122: error forwarding port 9122 to pod 51856f9224907d4c1380783e46b13069ef5322ae1f286d4301f90a2ed60483c0, uid : exit status 1: 2023/03/11 07:36:35 socat[10867] E connect(5, AF=2 127.0.0.1:9122, 16): Connection refused

[/simterm]

Registry

Тут метрики:

  • registry_http_in_flight_requests: A gauge of requests currently being served by the http server
  • registry_http_request_duration_seconds_bucket: A histogram of latencies for requests to the http server
  • registry_http_requests_total: A counter for requests to the http server
  • registry_storage_action_seconds_bucket: The number of seconds that the storage action takes
  • registry_storage_rate_limit_total: A counter of requests to the storage driver that hit a rate limit

Sidekiq

Тут метрики:

  • Jobs:
    • sidekiq_jobs_cpu_seconds: Seconds of CPU time to run Sidekiq job
    • sidekiq_jobs_db_seconds: Seconds of DB time to run Sidekiq job
    • sidekiq_jobs_gitaly_seconds: Seconds of Gitaly time to run Sidekiq job
    • sidekiq_jobs_queue_duration_seconds: Duration in seconds that a Sidekiq job was queued before being executed
    • sidekiq_jobs_failed_total: Sidekiq jobs failed
    • sidekiq_jobs_retried_total: Sidekiq jobs retried
    • sidekiq_jobs_interrupted_total: Sidekiq jobs interrupted
    • sidekiq_jobs_dead_total: Sidekiq dead jobs (jobs that have run out of retries)
    • sidekiq_running_jobs: Number of Sidekiq jobs running
    • sidekiq_jobs_processed_total: (from gitlab-exporter)
  • Redis:
    • sidekiq_redis_requests_total: Redis requests during a Sidekiq job execution
    • gitlab_redis_client_exceptions_total: Number of Redis client exceptions, broken down by exception class
  • Queue (from gitlab-exporter):
    • sidekiq_queue_size
    • sidekiq_queue_latency_seconds
  • Misc:
    • sidekiq_concurrency: Maximum number of Sidekiq jobs

Webservice

Трохи про сервіси:

  • Action Cable: is a Rails engine that handles websocket connections – див. Action Cable
  • Puma: is a simple, fast, multi-threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications – див. GitLab Puma

Тут метрики:

  • Database:
    • gitlab_database_transaction_seconds: Time spent in database transactions, in seconds
    • gitlab_sql_duration_seconds: SQL execution time, excluding SCHEMA operations and BEGIN / COMMIT
    • gitlab_transaction_db_count_total: Counter for total number of SQL calls
    • gitlab_database_connection_pool_size: Total connection pool capacity
    • gitlab_database_connection_pool_connections: Current connections in the pool
    • gitlab_database_connection_pool_waiting: Threads currently waiting on this queue
  • HTTP:
    • http_requests_total: Rack request count
    • http_request_duration_seconds: HTTP response time from rack middleware for successful requests
    • gitlab_external_http_total: Total number of HTTP calls to external systems
    • gitlab_external_http_duration_seconds: Duration in seconds spent on each HTTP call to external systems
  • ActionCable:
    • action_cable_pool_current_size: Current number of worker threads in ActionCable thread pool
    • action_cable_pool_max_size: Maximum number of worker threads in ActionCable thread pool
    • action_cable_pool_pending_tasks: Number of tasks waiting to be executed in ActionCable thread pool
    • action_cable_pool_tasks_total: Total number of tasks executed in ActionCable thread pool
  • Puma:
    • puma_workers: Total number of workers
    • puma_running_workers: Number of booted workers
    • puma_running: Number of running threads
    • puma_queued_connections: Number of connections in that worker’s “to do” set waiting for a worker thread
    • puma_active_connections: Number of threads processing a request
    • puma_pool_capacity: Number of requests the worker is capable of taking right now
    • puma_max_threads: Maximum number of worker threads
  • Redis:
    • gitlab_redis_client_requests_total: Number of Redis client requests
    • gitlab_redis_client_requests_duration_seconds: Redis request latency, excluding blocking commands
  • Cache:
    • gitlab_cache_misses_total: Cache read miss
    • gitlab_cache_operations_total: Cache operations by controller or action
  • Misc:
    • user_session_logins_total: Counter of how many users have logged in since GitLab was started or restarted

Workhorse

Про сервіс: GitLab Workhorse is a smart reverse proxy for GitLab, див. GitLab Workhorse.

Тут метрики:

  • gitlab_workhorse_gitaly_connections_total: Number of Gitaly connections that have been established
  • gitlab_workhorse_http_in_flight_requests: A gauge of requests currently being served by the http server
  • gitlab_workhorse_http_request_duration_seconds_bucket: A histogram of latencies for requests to the http server
  • gitlab_workhorse_http_requests_total: A counter for requests to the http server
  • gitlab_workhorse_internal_api_failure_response_bytes: How many bytes have been returned by upstream GitLab in API failure/rejection response bodies
  • gitlab_workhorse_internal_api_requests: How many internal API requests have been completed by gitlab-workhorse, partitioned by status code and HTTP method
  • gitlab_workhorse_object_storage_upload_requests: How many object storage requests have been processed
  • gitlab_workhorse_object_storage_upload_time_bucket: How long it took to upload objects
  • gitlab_workhorse_send_url_requests: How many send URL requests have been processed

Ух… Багацько.

Але було цікаво і корисно, щоб більш-менш поринути в те, що взагалі відбувається всередині GitLab кластеру.

Grafana GitLab Overview dashboard

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

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

Що нам цікаво?

З ресурсів Kubernetes:

  • pods: рестарти, pendings
  • PVC: зайнято/вільно місця на дисках, IOPS
  • CPU/Memory по подах та CPU throttling (якби у подів були ліміти, по-дефолту нема)
  • network: in/out bandwich, errors rate

Крім того – хотілося бі мати перед очима статус компонентів GitLab, дані по базі даних, Redis та якусь статистику по HTTP/Git/SSH.

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

Чисто для мене – бажано, щоб всі дані були на одному екрані/моніторі – потім зручно відразу бачити все, що треба.

Колись давно, коли ще ходив до офісу, це виглядало так – load testing нашого першого Kubernetes-кластеру на колишній роботі:

Поїхали.

Variables

Щоб мати змогу вивести інформацю по конкретному компоненту кластера – додамо змінну component.

Значення формуємо за запитом до kube_pod_info:

label_values(kube_pod_info{namespace="gitlab-cluster-prod", pod!~".*backup.*"}, pod)

З якої отримаємо лейблу pod, і потім регуляркою /^([^\d]+)-/ вирізаємо все до цифр:

А далі можемо використовувати $component, щоб отримати тільки потрібні поди.

Статус компонентів GitLab

Тут досить просто: знаємо кількість подів кожного сервісу. Рахуємо їх, та виводимо UP/DEGRADED/DOWN.

На прикладі Webservice – використовуємо такий запит:

sum(kube_pod_info{namespace="gitlab-cluster-prod", pod=~"gitlab-cluster-prod-webservice-.+"})

Створюємо панель з типом Stat, отримуємо кількість подів:

Задаємо Text mode = Value:

Unit = number:

Створюємо Value mapping:

У нас наразі 2 поди в Deployment, тож якщо буде нуль – то пишему DOWN, якщо тільки один – то DEGRADED, ну а 2 і більше – то ОК, UP.

Повторюємо для всіх сервісів:

Pods status та кількість WorkerNodes

Друге, за чим важливо стежити – це статуси подів та кількість EC2 у AWS EC2 AutoScale групі, бо маємо виділенний node pool під GitLab кластер.

Pod restarts table

Для Pod restarts використаємо тип Table:

Запит:

sum(delta(kube_pod_container_status_restarts_total{namespace="gitlab-cluster-prod", pod=~"$component.*"}[5m])) by (pod)

І ставимо тип Table:

Додаємо Value mappins – в залежності від значення в колонці рестартів ячейка буде змінювати колір:

В Override ховаємо колонку Time, поле Value називаємо Restarts, змінюємо колір колонки Pod та її ім’я:

Результат:

Pods status graph

Далі, виведомо графік статусів подів – рестарти, Pending, etc.

Для статусу використаємо:

sum(avg(kube_pod_status_phase{namespace="gitlab-cluster-prod", phase!="Succeeded", pod=~"$component.*"}) by(namespace, pod, phase)) by(phase)

Для відображення рестартів:

sum(delta(kube_pod_container_status_restarts_total{namespace="gitlab-cluster-prod", pod=~"$component.*"}[5m]))

Результат:

Cluster Autoscaler Worker Nodes

Тут трохи цікавіше: треба порахувати всі Kubernetes Worker Nodes, на яких є поди GitLab, але метрики від самого Cluster AutoScaler на мають лейбли типу “namespace”, тож використаємо метрику kube_pod_info, яка має лейбли namespace та node, і по сумі node дізнаємось кількість EC2 інстансів:

count(count(kube_pod_info{namespace="gitlab-cluster-prod", pod!~"logical-backup.+"}) by (node))

Для Max nodes довелося значення задавати вручну, але навряд чи воно буде часто змінюватись.

В Thresholds задаємо значення, коли треба напрягатись, нехай буде 10, і включаємо Show thresholds = As filled regions and lines, щоб бачити його на графіку:

Результат:

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

CPU та Memory by Pod

CPU by Pod

Рахуємо % від доступного CPU по кількості ядер. Тут цю кількість теж задав руками, знаючи тип ЕС2, але можна пошукати метрики типу “cores allocatable”:

sum(rate(container_cpu_usage_seconds_total{namespace="gitlab-cluster-prod", container!="POD",pod!="", image=~"", pod=~"$component.*"}[5m]) / 2 * 100) by (pod)

Не пам’ятаю вже, звідки сам запит – але результат у kubectl top pod підтверджує дані – перевіримо на поді з Sidekiq:

І top:

121 millicpu з 2000 доступних (2 ядра) це:

[simterm]

>>> 121/2000*100
6.05

[/simterm]

На графіку 5,43 – виглядає ок.

В Legend переносимо список вправо, та включаємо Values = Last, щоб сортувати по значеннях:

Результат:

Memory by Pod

Тут рахуємо по container_memory_working_set_bytes, налаштування таблиці аналогічні:

Доречі, можна було вивести % від доступної пам’яті на ноді, але нехай краще буде в “чистих” байтах.

Або можна додати Threshold с максимум 17179869184 байт – але тоді не так добре буде видно графікі з подів.

І разом маємо таке:

Статистка по дисках

Gitaly PVC used space

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

Запити брав з якоїсь дефолтної дашборди з комплекту Kube Prometheus Stack.

Для отримання % зайнятого місця на Gitaly використовуємо запит:

100 - ( 
    kubelet_volume_stats_available_bytes{namespace="gitlab-cluster-prod", persistentvolumeclaim="repo-data-gitlab-cluster-prod-gitaly-0"} / 
    kubelet_volume_stats_capacity_bytes{namespace="gitlab-cluster-prod", persistentvolumeclaim="repo-data-gitlab-cluster-prod-gitaly-0"}
    * 100
)

Та тип Gauge, Unit – Percent 0-100, і додаємо Thresholds:

Disc IOPS

Додамо operations per second на дисках, запит теж десь з готових борд брав:

ceil(sum by(pod) (rate(container_fs_reads_total{job="kubelet", metrics_path="/metrics/cadvisor", container!="", device=~"(/dev/)?(mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+)", namespace="gitlab-cluster-prod", pod=~"$component.*"}[$__rate_interval]) + rate(container_fs_writes_total{job="kubelet", metrics_path="/metrics/cadvisor", container!="", namespace="gitlab-cluster-prod", pod=~"$component.*"}[$__rate_interval])))

Сумуємо по подах, у Legend знову додаємо Values = Last, щоб мати змогу сортування:

Disc Throughput

Тут все в принципі аналогічно, тільки інший запит:

sum by(pod) (rate(container_fs_reads_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", container!="", device=~"(/dev/)?(mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+)", namespace="gitlab-cluster-prod", pod=~"$component.*"}[$__rate_interval]) + rate(container_fs_writes_bytes_total{job="kubelet", metrics_path="/metrics/cadvisor", container!="", namespace="gitlab-cluster-prod", pod=~"$component.*"}[$__rate_interval]))

І все разом:

Networking

Ще мабуть буде корисно бачити що з мережею – помилки, та рейти In/Out.

Received/Transmitted Errors

Додамо Gauge, де будемо виводити % помилок – container_network_receive_errors_total, який рахуємо за запитом:

sum(rate(container_network_receive_errors_total{namespace="gitlab-cluster-prod"}[5m]))
/
sum(rate(container_network_receive_packets_total{namespace="gitlab-cluster-prod"}[5m]))
* 100

Та аналогічно – для Transmitted:

sum(rate(container_network_transmit_errors_total{namespace="gitlab-cluster-prod"}[5m]))
/
sum(rate(container_network_transmit_packets_total{namespace="gitlab-cluster-prod"}[5m]))
* 100

Network Bandwidth Bytes/second

Тут рахуємо кількість байт в секунду на кожному поді – container_network_receive_bytes_total та container_network_transmit_bytes_total:

Network Packets/second

Аналогічно, тільки з метриками container_network_receive_packets_total/container_network_transmit_packets_total:

Не впевнений, що воно буде корисно, але поки що нехай буде.

Webservice HTTP statistic

Для загальної картини – додамо трохи даних по HTTP-запитам на Webservice.

HTTP requests/second

Використаємо метрику http_requests_total:

Додамо Override, щоб змінити колір для даних по 4хх та 5хх кодам:

Webservice HTTP request duration

Тут можна було б побудувати Heatmap використовуєчи http_request_duration_seconds_bucket, але як на мене – то звичайний графік по типам запитів буде кращий:

sum(increase(http_request_duration_seconds_sum{kubernetes_namespace="gitlab-cluster-prod"}[5m])) by (method) 
/ 
sum(increase(http_request_duration_seconds_count{kubernetes_namespace="gitlab-cluster-prod"}[5m])) by (method)

Але можна й Heatmap:

sum(increase(http_request_duration_seconds_bucket[10m])) by (le)

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

Див:

Статистика сервісів GitLab

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

Але з того, що поки приходить в голову – це Sidekiq та його джоби, Redis, PostgreSQL, GitLab Runner.

Sidekiq Jobs Errors rate

Запит:

sum(sidekiq_jobs_failed_total) / sum(sidekiq_jobs_processed_total) * 100

GitLab Runner Errors rate

Запит:

sum(gitlab_runner_errors_total) / sum(gitlab_runner_api_request_statuses_total) * 100

GitLab Redis Errors rate

Запит:

sum(gitlab_redis_client_exceptions_total) / sum(gitlab_redis_client_requests_total) * 100

Gitaly Supervisor errors rate

Запит:

sum(gitaly_supervisor_health_checks_total{status="bad"}) / sum(gitaly_supervisor_health_checks_total{status="ok"}) * 100

Database transactions latency

Запит:

sum(rate(gitlab_database_transaction_seconds_sum[5m])) by (kubernetes_pod_name) / sum(rate(gitlab_database_transaction_seconds_count[5m])) by (kubernetes_pod_name)

User Sessions

Запит:

sum(user_session_logins_total)

Git/SSH failed connections/second – by Grafana Loki

А тут використаємо значення, отримані з Loki – рейт помилок “kex_exchange_identification: Connection closed by remote host“, в Loki це виглядає так:

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

В панелі вказуємо Data source = Loki, та використовуємо sum() і rate() для отримання значень:

Налаштовуємо Thresholds та Overrides, і маємо такий графік:

І взагалі вся борда тепер виглядає так:

Побачимо, як воно буде далі, і що стане у нагоді, а що буде видалитись, та що можна буде додати ще.

Ну і самі алерти теж треба буде зробити.

Loading

GitLab: міграція даних з GitLab cloud та процес backup-restore у self-hosted версії в Kubernetes
0 (0)

1 Березня 2023

Продовжуємо рухатись до запуску нашого self-hosted GitLab в production.

Див. попередні частини – GitLab: компоненти, архітектура, інфраструктура та запуск з Helm-чарту в Minikube та GitLab: Helm-чарт values, залежності та деплой у Kubernetes з AWS S3.

Міграція даних

Ще дуже хотілося написати пост про міграцію даних з GitLab Cloud до self-hosted, але немає часу, та й пройшло це майже повз мене – автоматизували через GitLab API та скрипт силами девелоперів.

Проте, я таки трохи інвестигейтив це питання, і в двох словах – “дефолтна” міграція, тобто direct transfer (recommended) не підтягувала проекти, тож використали depricated, але робочий варіант з file export, тобто коли для групи або проекту у Cloud-версії робиться експорт у файл, а потім імпортується вже у self-hosted.

Ну і поки девелопери працюють над допилюванням міграції – я займуся тествування та налаштування backups/restore на впипадок, якщо (коли?) “щось піде не так” (с).

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

GitLab Toolbox

“Recommended way” – це використовувати Toolbox pod, в якому по cron можна створювати бекапи, та при потребі – відновити дані.

Под Toolbox має декілька корисних утіліт:

  • gitlab-rails: дозволяє виконувати з консолі всякі адміністративні задачі в GitLab, пов’язані з траблшутінгом або для отримання якихось даних
  • gitlab-rake: теж для консолі, теж для адміністративних задач, в тому числі по бекапу та відновленню
  • backup-utility: ну й та сама утіліта, яку ми будемо використовувати для створення повного бекапу GitLab та всіх його залежностей типу бази даних

GitLab backup

Документація – Backing up a GitLab installation та Backup and restore.

backup-utility

Створення резервної копії виконується за допомогою поду з GitLab Toolbox, в якому є утіліта backup-utility:

[simterm]

git@gitlab-cluster-prod-toolbox-778bb8dc9f-knsr8:/$ which backup-utility
/usr/local/bin/backup-utility

[/simterm]

Яка являє собою bash-скрипт:

[simterm]

git@gitlab-cluster-prod-toolbox-778bb8dc9f-knsr8:/$ cat /usr/local/bin/backup-utility
#!/bin/bash
set -e

ACTION=""
export BACKUP_BUCKET_NAME=${BACKUP_BUCKET_NAME-gitlab-backups}
export BACKUP_BACKEND=${BACKUP_BACKEND-s3}
export S3_TOOL=${S3_TOOL-s3cmd}
export AWS_KMS_SETTINGS=""
export AWS_S3_SETTINGS=""
AWS_KMS_SETTINGS_LIST=()
AWS_S3_SETTINGS_LIST=()
S3_CMD_BACKUP_OPTION=""

rails_dir=/srv/gitlab
backups_path=$rails_dir/tmp/backups
backup_tars_path=$rails_dir/tmp/backup_tars
object_storage_backends=( registry uploads artifacts lfs packages external_diffs terraform_state pages ci_secure_files )
...

[/simterm]

По дефолту бекап виконується за допомогую s3cmd:

...
--s3tool TOOL                          S3 CLI tool to use. Can be either 's3cmd' or 'awscli'
...

Всі опції можна побачити за допомогою backup-utility --help.

При створенні бекапу, backup-utility виконає:

  • бекап бази даних Постгрі
  • бекап репозиторієв
  • бекап сторейджів – Package registry, Container registry, etc

Див. Sequence of execution.

Так як у нас вже налаштовано ServiceAccount з AWS IAM роллю, яка дозволяє доступ до S3, то замість s3cmd, до якоїх потрібен окремий конфіг, спробуємо AWS CLI.

Поки відкрите питання по розміру диску для самого Toolbox, бо бекап у вигляді TAR-архіву спочатку створюється у поді, а потім вигружається до S3-бакету. Поки зилишимо, як є, але маємо на увазі на майбутнє – якщо що, то можна буде збільшити диск для Toolbox, там це не страшно робити, бо ніяких даних він не сберігає (тобто под з Toolbox є stateless-сервісом).

UPD – схоже, що можна додати окремий диск через values та gitlab.toolbox.backups.persistence.

UPD-2: А ось знайшлось про розмір диску у документації по restore:

GitLab repositories data

Найбільше цікавить, звісно, бекап репозиторіїв.

Самі репозиторії зберігаються у Kubernetes PersistenVolume, який підключено до поду з Gitaly:

[simterm]

$ kk -n gitlab-cluster-prod describe pod gitlab-cluster-prod-gitaly-0
...
Containers:
  gitaly:
  ...
    Mounts:
      ...
      /home/git/repositories from repo-data (rw)
...

[/simterm]

В якому і є весь наш код:

[simterm]

$ kk -n gitlab-cluster-prod exec -ti gitlab-cluster-prod-gitaly-0 -- ls -l /home/git/repositories
Defaulted container "gitaly" out of: gitaly, certificates (init), configure (init)
total 28
drwxrwsr-x  3 git  git  4096 Feb 28 17:10 +gitaly
drwxrws--- 19 git  git  4096 Feb 28 16:04 @hashed
drwxrwsr-x  4 git  git  4096 Feb 24 11:39 gitlab.com-import
drwxrws---  2 root git 16384 Feb 21 13:31 lost+found

[/simterm]

Я б ще окремо подумав би про бекап цього PV/EBS за допомогою External Snaphotter та/або чи Amazon Data Lifecycle Manager.

Ручний запуск бекапу

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

[simterm]

git@gitlab-cluster-prod-toolbox-778bb8dc9f-knsr8:/$ backup-utility --s3tool awscli
2023-03-01 11:59:41 UTC -- Dumping main_database ... 
Dumping PostgreSQL database gitlabhq_production ... pg_dump: warning: could not find where to insert IF EXISTS in statement "-- *not* dropping schema, since initdb creates it
"
[DONE]
2023-03-01 11:59:45 UTC -- Dumping main_database ... done
2023-03-01 11:59:45 UTC -- Dumping ci_database ... [DISABLED]
2023-03-01 11:59:45 +0000 -- Deleting backup and restore lock file
2023-03-01 11:59:55 UTC -- Dumping repositories ... 
{"command":"create","gl_project_path":"gitlab-root.wiki","level":"info","msg":"started create","relative_path":"@groups/d4/73/d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35.wiki.git","storage_name":"default","time":"2023-03-01T11:59:55.890Z"}
{"command":"create","error":"manager: repository empty: repository skipped","gl_project_path":"gitlab-root.wiki","level":"warning","msg":"skipped create","relative_path":"@groups/d4/73/d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35.wiki.git","storage_name":"default","time":"2023-03-01T11:59:55.898Z"}
...
{"command":"create","gl_project_path":"migration-testing/companyname/apps/apple","level":"info","msg":"started create","relative_path":"@hashed/f6/e0/f6e0a1e2ac41945a9aa7ff8a8aaa0cebc12a3bcc981a929ad5cf810a090e11ae.git","storage_name":"default","time":"2023-03-01T12:00:00.994Z"}
...
{"command":"create","gl_project_path":"migration-testing/companyname/apps/apple","level":"info","msg":"completed create","relative_path":"@hashed/f6/e0/f6e0a1e2ac41945a9aa7ff8a8aaa0cebc12a3bcc981a929ad5cf810a090e11ae.git","storage_name":"default","time":"2023-03-01T12:00:05.826Z"}
...
2023-03-01 12:00:05 UTC -- Dumping repositories ... done
2023-03-01 12:00:05 +0000 -- Deleting backup and restore lock file
Dumping registry ...
empty
Dumping uploads ...
done
Dumping artifacts ...
done
Bucket not found: or-git-lfs-prod. Skipping backup of lfs ...
Dumping packages ...
empty
Dumping external_diffs ...
empty
Dumping terraform_state ...
empty
Bucket not found: gitlab-pages. Skipping backup of pages ...
Dumping ci_secure_files ...
empty
Packing up backup tar
[DONE] Backup can be found at s3://or-gitlab-backups-prod/1677671971_2023_03_01_15.8.1-ee_gitlab_backup.tar

[/simterm]

Воу… Працює)

Перевіримо зміст S3:

[simterm]

$ aws --profile internal s3 ls or-gitlab-backups-prod
2023-03-01 14:01:31 1006673920 1677671971_2023_03_01_15.8.1-ee_gitlab_backup.tar

[/simterm]

Backup Cron

Теперь додамо запуск створення бекапу за роскладом, наприклад – о 6 ранку по Києву, що наче як виходить 23.00 у США, бо наша команда є і там, і там, і навряд чи хтось буде пушити в GitLab в ці години.

Див. Configuration та values самого чарту Toolbox.

Використаємо https://crontab.guru.

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

Перевіряємо час в поді, бо може бути інша таймзона:

[simterm]

git@gitlab-cluster-prod-toolbox-778bb8dc9f-knsr8:/$ date
Wed Mar  1 12:32:12 UTC 2023

[/simterm]

А в Києві зараз 14:32, тож поставимо запуск на 13:00, як перевіримо – змінимо на 6 ранку.

Оновлюємо конфіг:

...
    toolbox:
      backups:
        cron:
          enabled: true
          concurrencyPolicy: Replace
          failedJobsHistoryLimit: 1
          schedule: "0 13 * * *"
          successfulJobsHistoryLimit: 3
          suspend: false
          backoffLimit: 6
          restartPolicy: "OnFailure"
          extraArgs: "--s3tool awscli"
          resources:
            requests:
              cpu: 50m
              memory: 350M
          persistence:
            enabled: false
            accessMode: ReadWriteOnce
            size: 10Gi
        objectStorage:
          backend: s3
...

Я тут навмисно вписав дефолтні resources.requests та persistence, щоб потім легше було їх змінити.

У objectStorage.config.secret спробуємо той самий сікрет, який ми створювали для appConfig.object_store, бо Toolbox його вимагає.

І в extraArgs: "--s3tool awscli" задаємо використання AWS CLI.

Деплоїмо, переверіяємо чи додалась кронджоба:

[simterm]

$ kk -n gitlab-cluster-prod get cj     
NAME                                        SCHEDULE     SUSPEND   ACTIVE   LAST SCHEDULE   AGE
gitlab-cluster-prod-toolbox-backup          0 13 * * *   False     0        <none>          6s
logical-backup-devops-gitlab-cluster-psql   0 1 * * *    False     0        11h             19d

[/simterm]

І чекаємо на 13.00 – спрацює, чи ні?

І такі да – о 13.00 з’явився под gitlab-cluster-prod-toolbox-backup-27961260-x4n45, в якому почав виконуватись бекап:

Ну – виглядає, наче ОК?

Давайте спробує відновити наш GitLab.

GitLab restore

Документація – Restoring the backup file.

Що хочеться протестити:

  • відновлення репозиторія
  • відновлення бази Postgres

Що зробимо:

  • створимо табличку в базі
  • створимо тестовий репозиторій

Потім створимо бекап, потім видалимо тестові дані, і спробуємо запустити рестор.

Тестовий проект

Створюємо проект:

Побудемо трошки рубістами:

Тестова PostgreSQL

Далі, логінимось до PostgreSQL.

В нас, нагадаю, PostgreSQL Operator (давно в чернетках пост по ньому, якось закінчу).

Знаходимо пароль – беремо із сікрету postgres.devops-gitlab-cluster-psql.credentials.postgresql.acid.zalan.do, виводимо в json, з jq отримаємо значення поля .data.password, та декодимо з base64:

[simterm]

$ kk -n gitlab-cluster-prod get secret postgres.devops-gitlab-cluster-psql.credentials.postgresql.acid.zalan.do -o json | jq -r '.data.password' | base64 -d
IcL***dPP

[/simterm]

Знаходимо master-інстанс:

[simterm]

$ kk -n gitlab-cluster-prod get pod -l application=spilo -L spilo-role
NAME                           READY   STATUS    RESTARTS   AGE     SPILO-ROLE
devops-gitlab-cluster-psql-0   2/2     Running   0          21h     replica
devops-gitlab-cluster-psql-1   2/2     Running   0          6d23h   replica
devops-gitlab-cluster-psql-2   2/2     Running   0          11d     master

[/simterm]

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

[simterm]

$ kk -n gitlab-cluster-prod port-forward devops-gitlab-cluster-psql-2 5432:5432
Forwarding from 127.0.0.1:5432 -> 5432
Forwarding from [::1]:5432 -> 5432

[/simterm]

Логінимось в кластер з юзером postgres:

[simterm]

$ psql -h localhost -U postgres -d gitlabhq_production
Password for user postgres: 
psql (15.1, server 14.4 (Ubuntu 14.4-1.pgdg18.04+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

gitlabhq_production=# 

[/simterm]

Створюємо табличку в базі gitlabhq_production:

[simterm]

gitlabhq_production=# CREATE TABLE test_restore(key_id serial PRIMARY KEY, key_name VARCHAR (255) UNIQUE NOT NULL);
CREATE TABLE

[/simterm]

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

[simterm]

gitlabhq_production-# \dt test_restore
            List of relations
 Schema |     Name     | Type  |  Owner   
--------+--------------+-------+----------
 public | test_restore | table | postgres
(1 row)

[/simterm]

Переходимо до поду з Toolbox, та запускаємо бекап:

[simterm]

git@gitlab-cluster-prod-toolbox-6fc9c7fc89-8jws7:/$ backup-utility --s3tool awscli
2023-03-01 14:34:22 UTC -- Dumping main_database ... 
Dumping PostgreSQL database gitlabhq_production ... pg_dump: error: query failed: ERROR:  permission denied for table test_restore
pg_dump: detail: Query was: LOCK TABLE public.test_restore IN ACCESS SHARE MODE
[FAILED]
2023-03-01 14:34:22 UTC -- Dumping main_database failed: Failed to create compressed file '/srv/gitlab/tmp/backups/db/database.sql.gz' when trying to backup the main database:
 - host: 'devops-gitlab-cluster-psql'
 - port: '5432'
 - database: 'gitlabhq_production'
...

[/simterm]

Oops… “Щось пішло не так”.

Читаємо доку про GRANT, та задаємо права всім:

[simterm]

gitlabhq_production=# GRANT ALL ON test_restore TO PUBLIC;
GRANT

[/simterm]

Пробуємо ще раз, і:

[simterm]

git@gitlab-cluster-prod-toolbox-6fc9c7fc89-8jws7:/$ backup-utility --s3tool awscli
2023-03-01 14:37:39 UTC -- Dumping main_database ... 
Dumping PostgreSQL database gitlabhq_production ... pg_dump: error: query failed: ERROR:  permission denied for sequence test_restore_key_id_seq
pg_dump: detail: Query was: SELECT last_value, is_called FROM public.test_restore_key_id_seq
...

[/simterm]

Твою ж…

Додаємо ще прав на SEQUENCE:

[simterm]

gitlabhq_production=# GRANT USAGE, SELECT ON SEQUENCE test_restore_key_id_seq TO PUBLIC;
GRANT

[/simterm]

Тепер пішло:

[simterm]

git@gitlab-cluster-prod-toolbox-6fc9c7fc89-8jws7:/$ backup-utility --s3tool awscli
2023-03-01 14:40:38 UTC -- Dumping main_database ... 
Dumping PostgreSQL database gitlabhq_production ... pg_dump: warning: could not find where to insert IF EXISTS in statement "-- *not* dropping schema, since initdb creates it
"
[DONE]
2023-03-01 14:40:43 UTC -- Dumping main_database ... done
...
Packing up backup tar
[DONE] Backup can be found at s3://or-gitlab-backups-prod/1677681628_2023_03_01_15.8.1-ee_gitlab_backup.tar

[/simterm]

Добре.

Видалення даних

Тепер – видаляємо таблицю з бази:

[simterm]

gitlabhq_production=# DROP TABLE test_restore;
DROP TABLE
gitlabhq_production=# \dt test_restore
Did not find any relation named "test_restore".

[/simterm]

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

Відновлення даних з бекапу

Be sure to stop Puma, Sidekiq, and any other process

Ну і пробуємо – визиваємо той самий backup-utility з опцією --restore:

[simterm]

git@gitlab-cluster-prod-toolbox-6fc9c7fc89-8jws7:/$ backup-utility --s3tool awscli --restore -t 1677681628_2023_03_01_15.8.1-ee
Unpacking backup
2023-03-01 14:48:06 UTC -- Restoring main_database ... 
2023-03-01 14:48:06 UTC -- Be sure to stop Puma, Sidekiq, and any other process that
connects to the database before proceeding. For Omnibus
installs, see the following link for more information:
https://docs.gitlab.com/ee/raketasks/backup_restore.html#restore-for-omnibus-gitlab-installations

Before restoring the database, we will remove all existing
tables to avoid future upgrade problems. Be aware that if you have
custom tables in the GitLab database these tables and all data will be
removed.

[/simterm]

Ага… А в документації про це не говориться… Хоча в цілому варто переглянути Back up and restore GitLab – там ще багато чого цікавого.

Добре – згадаємо, що у нас використовує Postgres – див. Інфраструктура:

Ну, наче тільки Webservice та Sidekiq.

Стопаємо їх:

[simterm]

$ kk -n gitlab-cluster-prod scale deploy gitlab-cluster-prod-sidekiq-all-in-1-v2 --replicas=0
deployment.apps/gitlab-cluster-prod-sidekiq-all-in-1-v2 scaled

$ kk -n gitlab-cluster-prod scale deploy gitlab-cluster-prod-webservice-default --replicas=0
deployment.apps/gitlab-cluster-prod-webservice-default scaled

[/simterm]

Запускаємо рестор знову:

[simterm]

git@gitlab-cluster-prod-toolbox-6fc9c7fc89-8jws7:/$ backup-utility --s3tool awscli --restore -t 1677681628_2023_03_01_15.8.1-ee
Unpacking backup
2023-03-01 15:01:25 UTC -- Restoring main_database ... 
2023-03-01 15:01:25 UTC -- Be sure to stop Puma, Sidekiq, and any other process that
...

[/simterm]

Та ну твою ж дивізію…

Ну, ок… Може це через Postgres Operator? Бо він точно якісь конекти тримає. Чи через моніторинг – експортер?

Глянемо за таким нагугленим запитом:

select pid as process_id, 
       usename as username, 
       datname as database_name, 
       client_addr as client_address, 
       application_name,
       backend_start,
       state,
       state_change
from pg_stat_activity;

Виконуємо:

[simterm]

gitlabhq_production=# select pid as process_id, 
       usename as username, 
       datname as database_name, 
       client_addr as client_address, 
       application_name,
       backend_start,
       state,
       state_change
from pg_stat_activity;
 process_id |            username            |    database_name    | client_address |            application_name            |         backend_start         | state  |         state_change          
------------+--------------------------------+---------------------+----------------+----------------------------------------+-------------------------------+--------+-------------------------------
      25175 |                                |                     |                |                                        | 2023-02-21 10:48:54.37466+00  |        | 
        152 | postgres                       | postgres            |                | Patroni                                | 2023-02-17 14:51:34.098346+00 | idle   | 2023-03-01 15:04:27.471442+00
         77 | postgres                       | postgres            |                |                                        | 2023-02-17 14:51:22.942046+00 |        | 
      25177 | postgres                       | postgres            |                | pg_cron scheduler                      | 2023-02-21 10:48:54.376082+00 | idle   | 2023-02-21 10:48:56.389663+00
      25179 | postgres                       |                     |                |                                        | 2023-02-21 10:48:54.377106+00 |        | 
      25178 | postgres                       |                     |                | TimescaleDB Background Worker Launcher | 2023-02-21 10:48:54.376869+00 |        | 
       5744 | standby                        |                     | 10.0.101.37    | devops-gitlab-cluster-psql-0           | 2023-02-28 16:20:44.085971+00 | active | 2023-02-28 16:20:44.092053+00
      25292 | gitlabhq_production_owner_user | gitlabhq_production | 10.0.81.98     | /usr/bin/gitlab-exporter               | 2023-02-21 10:51:42.135462+00 | idle   | 2023-03-01 15:03:53.761035+00
      15286 | standby                        |                     | 10.0.41.195    | devops-gitlab-cluster-psql-1           | 2023-02-22 14:24:15.551548+00 | active | 2023-02-22 14:24:15.560445+00
      24578 | postgres                       | gitlabhq_production | 127.0.0.1      | psql                                   | 2023-03-01 15:04:24.063071+00 | active | 2023-03-01 15:04:36.803864+00
        147 |                                |                     |                |                                        | 2023-02-17 14:51:33.082497+00 |        | 
      25176 |                                |                     |                |                                        | 2023-02-21 10:48:54.376307+00 |        | 
        146 |                                |                     |                |                                        | 2023-02-17 14:51:33.081686+00 |        | 
      25174 |                                |                     |                |                                        | 2023-02-21 10:48:54.374117+00 |        | 

[/simterm]

Оу, май… Ну, так… Багацько…

ERROR: must be owner of view pg_stat_statements_info

Добре – думаю, можно з цими конектами запускати – ігноруємо помилку, відповідаємо yes на запит “Do you want to continue?“:

[simterm]

git@gitlab-cluster-prod-toolbox-6fc9c7fc89-8jws7:/$ backup-utility --s3tool awscli --restore -t 1677681628_2023_03_01_15.8.1-ee
Unpacking backup
2023-03-01 15:06:43 UTC -- Restoring main_database ... 
2023-03-01 15:06:43 UTC -- Be sure to stop Puma, Sidekiq, and any other process that
...
Do you want to continue (yes/no)? yes
Removing all tables. Press `Ctrl-C` within 5 seconds to abort
2023-03-01 15:07:02 UTC -- Cleaning the database ... 
2023-03-01 15:07:02 +0000 -- Deleting backup and restore lock file
rake aborted!
ActiveRecord::StatementInvalid: PG::InsufficientPrivilege: ERROR:  must be owner of view pg_stat_statements_info
...

[/simterm]

SUUUUUKA!)

Окей… Ми вперті.

Гуглимо ішью – Cannot restore to helm-installed version.

Пробуємо видалити Postgres Extention:

[simterm]

gitlabhq_production=# DROP EXTENSION pg_stat_statements;
ERROR:  cannot drop extension pg_stat_statements because other objects depend on it
DETAIL:  function metric_helpers.pg_stat_statements(boolean) depends on type pg_stat_statements
view metric_helpers.pg_stat_statements depends on function metric_helpers.pg_stat_statements(boolean)
extension pg_stat_kcache depends on extension pg_stat_statements
HINT:  Use DROP ... CASCADE to drop the dependent objects too.

[/simterm]

Ну, ок… залежності не виглядають як якісь супер-критичні, і можливо потім ми таки зможемо повернути цей pg_stat_statements.

Пробуємо з CASCADE:

[simterm]

gitlabhq_production=# DROP EXTENSION pg_stat_statements CASCADE;
NOTICE:  drop cascades to 3 other objects
DETAIL:  drop cascades to function metric_helpers.pg_stat_statements(boolean)
drop cascades to view metric_helpers.pg_stat_statements
drop cascades to extension pg_stat_kcache
DROP EXTENSION

[/simterm]

Запускаємо…

І – чудо! Воно пішло, хоча з купою помилок на права доступа.

Знов спитало, чи продовжувати – знов відповідаємо yes:

[simterm]

git@gitlab-cluster-prod-toolbox-6fc9c7fc89-8jws7:/$ backup-utility --s3tool awscli --restore -t 1677681628_2023_03_01_15.8.1-ee
Unpacking backup
2023-03-01 15:14:41 UTC -- Restoring main_database ... 
2023-03-01 15:14:41 UTC -- Be sure to stop Puma, Sidekiq, and any other process that
connects to the database before proceeding. For Omnibus
installs, see the following link for more information:
https://docs.gitlab.com/ee/raketasks/backup_restore.html#restore-for-omnibus-gitlab-installations

Before restoring the database, we will remove all existing
tables to avoid future upgrade problems. Be aware that if you have
custom tables in the GitLab database these tables and all data will be
removed.

Do you want to continue (yes/no)? yes
Removing all tables. Press `Ctrl-C` within 5 seconds to abort
2023-03-01 15:14:49 UTC -- Cleaning the database ... 
2023-03-01 15:14:53 UTC -- done
Restoring PostgreSQL database gitlabhq_production ... ERROR:  permission denied for schema metric_helpers
ERROR:  permission denied for schema metric_helpers
ERROR:  permission denied for schema metric_helpers
ERROR:  permission denied for schema user_management
ERROR:  permission denied for schema user_management
...
ERROR:  permission denied for schema metric_helpers
------ END ERRORS -------
[DONE]
2023-03-01 15:15:39 UTC -- Restoring main_database ... done
2023-03-01 15:15:39 UTC -- There were errors in restoring the schema. This may cause
issues if this results in missing indexes, constraints, or
columns. Please record the errors above and contact GitLab
Support if you have questions:
https://about.gitlab.com/support/

Do you want to continue (yes/no)? yes
...
2023-03-01 15:16:45 UTC -- Restoring repositories ... done
2023-03-01 15:16:45 +0000 -- Deleting backup and restore lock file
Restoring uploads ...
done
Restoring artifacts ...
done

[/simterm]

Перевіряємо нашу тестову таблицю:

[simterm]

gitlabhq_production=# \dt test_restore
                       List of relations
 Schema |     Name     | Type  |             Owner              
--------+--------------+-------+--------------------------------
 public | test_restore | table | gitlabhq_production_owner_user
(1 row)

[/simterm]

Запускаємо стопнуті сервіси:

[simterm]

$ kk -n gitlab-cluster-prod scale deploy gitlab-cluster-prod-webservice-default --replicas=2
deployment.apps/gitlab-cluster-prod-webservice-default scaled

$ kk -n gitlab-cluster-prod scale deploy gitlab-cluster-prod-sidekiq-all-in-1-v2 --replicas=1
deployment.apps/gitlab-cluster-prod-sidekiq-all-in-1-v2 scaled

[/simterm]

Перевіряємо наш репозиторій:

Все є…

Дивно…)

Але ок – виглядає так, наче воно працює.

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

Хоча дуже сподіваюсь, що використовувати restore ніколи не доведеться.

Loading

Kubernetes: збільшення Persistent Volume з PersistentVolumeClaim та volumeClaimTemplates
0 (0)

21 Лютого 2023

GitLab для зберігання репозиторіїв по дефолту створює диск розміром 50 GB, чого для Production явно буде мало.

Persistent Volume resize опція доступна як general availability з версії Kubernetes 1.24, див. Kubernetes 1.24: Volume Expansion Now A Stable Feature, а в Beta – з версії 1.11, див. Resizing Persistent Volumes using Kubernetes.

Доступно для дисків AWS EBS, GCE PD, Azure Disk, Azure File, Glusterfs, Cinder, Portworx та Ceph RBD.

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

Збільшення диску через PersistentVolumeClaim

Перевіряємо чи можливо це з нашим “залізом”, тобто StorageClass, який використовується, в нашому випадку це AWS Elastic Block Store з типом gp3 – дивимось ресурс StorageClass, та його колонку ALLOWVOLUMEEXPANSION:

[simterm]

$ kk get StorageClass
NAME              PROVISIONER       RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
cold-storage      ebs.csi.aws.com   Delete          WaitForFirstConsumer   true                   362d
encrypted         ebs.csi.aws.com   Delete          WaitForFirstConsumer   true                   362d
gp2               ebs.csi.aws.com   Delete          WaitForFirstConsumer   true                   362d
gp3 (default)     ebs.csi.aws.com   Delete          WaitForFirstConsumer   true                   362d
high-iops-ssd     ebs.csi.aws.com   Delete          WaitForFirstConsumer   true                   362d
medium-iops-ssd   ebs.csi.aws.com   Delete          WaitForFirstConsumer   true                   362d
throughput        ebs.csi.aws.com   Delete          WaitForFirstConsumer   true                   362d

[/simterm]

Також памя’таємо, що після зміни розміру EBS потрібно змінити розмір файлової системи. У Kubernetes з версії 1.24 це робиться автоматично завдяки параметру ExpandInUsePersistentVolumesonline file system expansion. Між 1.11 та 1.24 потрібно перезапускати поди, які використовують цей PVC.

Ну і перевіримо.

Створимо Pod з PersistentVolumeClaim на 1 гігабайт:

---
apiVersion: v1
kind: Pod
metadata:
  name: test-resize-pv-pod
  labels:
    app: test-resize
spec:
  containers:
    - name: test-resize-pv-container
      image: nginxdemos/hello
      ports:
        - containerPort: 80
      volumeMounts:
        - mountPath: "/data"
          name: test-resize-pv-volume
  volumes:
  - name: test-resize-pv-volume
    persistentVolumeClaim:
      claimName: test-resize-pv-volume-pvc
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-resize-pv-volume-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: gp3

Деплоїмо:

[simterm]

$ kk -n tests apply -f test-pod-w-pvc.yaml 
pod/test-resize-pv-pod created
persistentvolumeclaim/test-resize-pv-volume-pvc created

[/simterm]

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

[simterm]

$ kk -n tests get pvc test-resize-pv-volume-pvc

[/simterm]

По ID подивимось на сам PersistentVolume (тобто AWS EBS):

[simterm]

$ kk -n tests get pv pvc-8e75b850-8ef2-431b-a525-2a3200d17634
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                             STORAGECLASS   REASON   AGE
pvc-8e75b850-8ef2-431b-a525-2a3200d17634   1Gi        RWO            Delete           Bound    tests/test-resize-pv-volume-pvc   gp3                     86s

[/simterm]

Редагуємо PersistentVolumeClaim – збільшимо в resources.requests.storage розмір до 2 ГБ:

---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-resize-pv-volume-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
  storageClassName: gp3

Оновлюємо:

[simterm]

$ kk -n tests apply -f test-pod-w-pvc.yaml 
pod/test-resize-pv-pod configured
persistentvolumeclaim/test-resize-pv-volume-pvc configured

[/simterm]

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

[simterm]

$ kk -n tests get pvc test-resize-pv-volume-pvc
NAME                        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
test-resize-pv-volume-pvc   Bound    pvc-8e75b850-8ef2-431b-a525-2a3200d17634   2Gi        RWO            gp3            4m50s

[/simterm]

В самому поді:

[simterm]

$ kk -n tests exec -ti test-resize-pv-pod -- df -h /data
Filesystem                Size      Used Available Use% Mounted on
/dev/nvme4n1              1.9G     24.0K      1.9G   0% /data

[/simterm]

Гуд.

Збільшення диску з StatefulSet та volumeClaimTemplates

Створюємо маніфест з STS та volumeClaimTemplates – цей volumeClaimTemplates створить PersistentVolumeClaim:

---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: registry.k8s.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

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

[simterm]

$ kk -n tests get pvc www-web-0
NAME        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
www-web-0   Bound    pvc-ca9d909f-1d34-40d9-acac-78ab778f500f   1Gi        RWO            gp3            2m7s

[/simterm]

The StatefulSet is invalid: spec: Forbidden

Пробуємо змінити volumeClaimTemplates.spec.resources.requests.storage на 2GB, аплаїмо:

[simterm]

$ kubectl -n tests apply -f test-sts-w-pvc.yaml 
The StatefulSet "web" is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'template', 'updateStrategy' and 'minReadySeconds' are forbidden

[/simterm]

Ага…

Тож лишаємо значення в volumeClaimTemplates, та редагуємо PVC вручну:

Або з kubectl patch:

[simterm]

$ kubectl -n tests patch pvc www-web-0 -p '{"spec":{"resources":{"requests":{"storage":"2Gi"}}}}'

[/simterm]

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

[simterm]

$ kk -n tests get pvc www-web-0
NAME        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
www-web-0   Bound    pvc-ca9d909f-1d34-40d9-acac-78ab778f500f   2Gi        RWO            gp3            3m43s

[/simterm]

Та сам Persistent Volume:

[simterm]

$ kk -n tests get pv pvc-ca9d909f-1d34-40d9-acac-78ab778f500f
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM             STORAGECLASS   REASON   AGE
pvc-ca9d909f-1d34-40d9-acac-78ab778f500f   2Gi        RWO            Delete           Bound    tests/www-web-0   gp3                     4m14s

[/simterm]

Готово.

Loading