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

Автор |  12/05/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 є ще приклади роботи, більше “продвинуті”, тож рекомендую.