AWS: Lambda – копирование тегов EC2 на EBS, часть 1 – Python и boto3

Автор: | 10/11/2021
 

Есть кластер AWS Elastic Kubernetes Service, у которого есть несколько WorkerNode Group, которые созданы как AWS AutoScaling Groups, всё создаётся через eksctl, см. AWS Elastic Kubernetes Service: — автоматизация создания кластера, часть 2 — Ansible, eksctl.

Для WorkerNode Group в конфигурации eksctl прописан набор тегов, которые позволяют проводить инвентаризацию сервисов:

---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: "{{ eks_cluster_name }}"
  region: "{{ region }}"
  version: "{{ k8s_version }}"

nodeGroups:

### Common ###

  - name: "{{ k8s_common_nodegroup_name }}-{{ item }}-v2021-09"
    instanceType: "{{ k8s_common_nodegroup_instance_type }}"
    privateNetworking: true
    labels:
      role: common-workers
    ...
    tags:
      Tier: "Devops"
      Domain: "eks.devops.{{ region }}.{{ env | lower }}.bttrm.local"
      ServiceType: "EC2"
      Env: {{ env }}
      Function: "Kubernetes WorkerNode"
      NetworkType: "Private"
      DataClass: "Public"
      AssetOwner: "{{ asset_owner }}"
      AssetCustodian: "{{ asset_custodian }}"
      OperatingSystem: "Amazon Linux"
      JiraTicket: "{{ jira_ticket }}"
      ConfidentialityRequirement: "Med"
      IntegrityRequirement: "Med"
      AvailabilityRequirement: "Med"
...

Теги задаются в AutoScale Group:

И потом копируются к создаваемым инстансам ЕС2.

Проблема в том, что диски, подключенные к этим ЕС2, теги не копируют. Кроме того, помимо Kubernetes WorkerNodes, в AWS-аккаунте существуют и другие ЕС2, и у некоторых только один, корневой диск, а у  некоторых подключены дополнительные EBS для данных или бекапов.

Плюс надо копировать не только существующие теги с ЕС2, к которому подключен диск(и), но к диску(ам) требуется добавить один новый тег, описывающий роль этого диска – Root Volume, Data Volume или Kubernetes PVC Volume.

Как можем это организовать? Как и всё в AWS, что не реализовано в самой AWS Console – с помощью AWS Lambda: создадим функцию, которая будет триггерится при создании новых ЕС2, и будет копировать теги с ЕС2 на все подключенные к нему Elastic Block Store volumes.

Итак, что нам надо в логике работы этой Lambda-функции:

  1. при создании любого ЕС2 в аккаунте – триггерим Lambda-функцию
  2. ей передаём ID EC2, используя который находим ID всех его EBS
  3. копируем теги EC2 на все EBS
  4. задаём новый тег Role:
    1. если EBS создан из Kubernetes PVC и смонтирован к ЕС2, созданной из Kubernetes WorkerNode AWS AutoScale Group – то задаём ему тег Role: "PvcVolume"
    2. если EBS создан во время запуска обычного EC2 – то определяем точку монтирования, и по нему решаем какой тег задать – Role: "RootVolume", или Role: "DataVolume"

Поехали.

Python скрипт – копирование тегов

Сначала напишем сам скрипт, протестируем его, а потом перейдём к Lambda. Скрипт будет перебирать все ЕС2 в регионе, и для всех EBS задавать теги, а потом, когда дойдём до Lambda, обновим его, и добавим передачу EC2 ID в функцию.

boto3: получение списка ЕС2 и их EBS

Сначала набросаем получение списка дисков. Для этого нам надо будет авторизоваться в AWS, получить все ЕС2 в регионе, а потом для каждого ЕС2 получить список подключенных к нему EBS.

Потом, имея эту информацию, уже сможем поиграться с тегами.

Пишем скрипт:

#!/usr/bin/env python

import os   
import boto3
            
ec2 = boto3.resource('ec2',
        region_name=os.getenv("AWS_DEFAULT_REGION"),
        aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
        aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY")
    )
    
def lambda_handler(event, context):
            
    base = ec2.instances.all()
                
    for instance in base:
            
        print("\n[DEBUG] EC2\n\t\tID: " + str(instance))
        print("\tEBS")

        for vol in instance.volumes.all():
    
            vol_id = str(vol)
            print("\t\tID: " + vol_id)
        
if __name__ == "__main__":
    lambda_handler(0, 0)

Тут в переменной ec2 создаём объект boto3.resource с типом ec2, и аутентифицируемся в AWS используя переменные $AWS_ACCESS_KEY_ID и $AWS_SECRET_ACCESS_KEY. В Lambda аутентификация будет выполняться через IAM Role, а тут пока тестируем используем ключи. См. Python: boto3 — примеры аутентификации и авторизации (пост старенький, но вроде ещё актуален).

В конце вызываем lambda_handler(), если скрипт был вызван как отдельный модуль, см. Python: зачем нужен if __name__ == «__main__»?, а в lambda_handler() через вызов метода ec2.instances.all() получаем все инстансы в регионе, а потом в цикле for для каждого ЕС2 через вызов instance.volumes.all() получаем список всех его EBS.

Аргументы в lambda_handler() пока передаём в виде 0 и 0, потом в Lambda будем передавать сюда event и context.

Задаём переменные:

export AWS_ACCESS_KEY_ID=AKI***D4Q
export AWS_SECRET_ACCESS_KEY=QUC***BTI
export AWS_DEFAULT_REGION=eu-west-3

Запускаем скрипт:

./ec2_tags.py
[DEBUG] EC2
ID: ec2.Instance(id='i-0df2fe9ec4b5e1855')
EBS
ID: ec2.Volume(id='vol-0d11fd27f3702a0fc')
[DEBUG] EC2
ID: ec2.Instance(id='i-023529a843d02f680')
EBS
ID: ec2.Volume(id='vol-0f3548ae321cd040c')
[DEBUG] EC2
ID: ec2.Instance(id='i-02ab1438a79a3e475')
EBS
ID: ec2.Volume(id='vol-09b6f60396e56c363')
ID: ec2.Volume(id='vol-0d75c44a594e312a1')
...

Отлично – всё работает. Получили все ЕС2 в регионе eu-west-3, и для каждого из ЕС2 получили список всех дисков, которые к нему подключены.

Что дальше? Дальше надо определить – что за диски мы нашли, и в соответствии с их точкой монтирования можно будет определить, что за диск – корневой, или какой-то дополнительный.

Используем атрибут attachments(), из которого получим значение Device.

Задаём новую локальную переменную device_id:

...
        for vol in instance.volumes.all():
            
            vol_id = str(vol)
            device_id = "ec2.vol.Device('" + str(vol.attachments[0]['Device']) + "')"
            
            print("\t\tID:  " + vol_id + "\n\t\tDev: " + device_id + "\n")
...

Запускаем:

./ec2_tags.py
[DEBUG] EC2
ID: ec2.Instance(id='i-0df2fe9ec4b5e1855')
EBS
ID:  ec2.Volume(id='vol-0d11fd27f3702a0fc')
Dev: ec2.vol.Device('/dev/xvda')
[DEBUG] EC2
ID: ec2.Instance(id='i-023529a843d02f680')
EBS
ID:  ec2.Volume(id='vol-0f3548ae321cd040c')
Dev: ec2.vol.Device('/dev/xvda')
[DEBUG] EC2
ID: ec2.Instance(id='i-02ab1438a79a3e475')
EBS
ID:  ec2.Volume(id='vol-09b6f60396e56c363')
Dev: ec2.vol.Device('/dev/xvda')
ID:  ec2.Volume(id='vol-0d75c44a594e312a1')
Dev: ec2.vol.Device('/dev/xvdbm')
...

И вот мы нашли ЕС2 i-02ab1438a79a3e475, у которого подключены два диска – vol-09b6f60396e56c363 как /dev/xvda, и vol-0d75c44a594e312a1 как /dev/xvdbm.

/dev/xvda очевидно, что корневой диск, а /dev/xvdbm – какой-то дополнительный.

boto3: добавление тегов к EBS

Далее, создадим тег Role, в который будем вносить одно из трёх допустимых значений:

  1. проверяем наличие тега kubernetes.io/created-for/pvc/name, и если он найден, то задаём Role: "PvcVolume"
  2. проверяем точку монтирования, если device == “/dev/xvda” – то задаём Role: "RootVolume"
  3. если у device любое другое значение – то задаём Role: "DataDisk"

Добавим функцию, которая будет заниматься перебором и созданием списка тегов, и отдельно в функцию is_pvc() вынесем проверку является ли раздел PVC – проверим наличие тега kubernetes.io/created-for/pvc/name:

...
def is_pvc(vol):

    try:
        for tag in vol.tags:
            if tag['Key'] == 'kubernetes.io/created-for/pvc/name':
                return True
                break
    except TypeError:
            return False

def set_role_tag(vol):

    device = vol.attachments[0]['Device']
    tags_list = []
    values = {}

    if is_pvc(vol):
        values['Key'] = "Role"
        values['Value'] = "PvcDisk"
        tags_list.append(values)
    elif device == "/dev/xvda":
        values['Key'] = "Role"
        values['Value'] = "RootDisk"
        tags_list.append(values)
    else:
        values['Key'] = "Role"
        values['Value'] = "DataDisk"
        tags_list.append(values)

    return tags_list
...

Тут в функции set_role_tag() сначала задаём переменные, затем передаём значение параметра vol аргументом в функцию is_pvc(), в которой через try/except проверяем наличие тега ‘kubernetes.io/created-for/pvc/name’. try/except тут нужен для проверки есть ли теги у EBS вообще.

Если тег ‘kubernetes.io/created-for/pvc/name’ есть – то is_pvc() вернёт True, если тег не надён – то False.

Далее, проверяем через if/elif/else условия – является ли диск PVC, если да – то задаём Role: "PvcVolume", если нет – то смонтирован ли как “/dev/xvda“, если да – то задаём Role: "RootVolume", и если нет – то задаём Role: "DataDisk".

Добавляем вызов set_role_tag() в lambda_handler() – передаём её аргументом в vol.create_tags():

...
def lambda_handler(event, context):
            
    base = ec2.instances.all()

    for instance in base:

        print("\n[DEBUG] EC2\n\t\tID:  " + str(instance))
        print("\tEBS")
            
        for vol in instance.volumes.all():
                
            vol_id = str(vol)
            device_id = "ec2.vol.Device('" + str(vol.attachments[0]['Device']) + "')"
            print("\t\tID:  " + vol_id + "\n\t\tDev: " + device_id)

            role_tag = vol.create_tags(Tags=set_role_tag(vol))
            print("\t\tTags set:\n\t\t\t" + str(role_tag))
...

Запускаем:

./ec2_tags.py
[DEBUG] EC2
ID:  ec2.Instance(id='i-0df2fe9ec4b5e1855')
EBS
ID:  ec2.Volume(id='vol-0d11fd27f3702a0fc')
Dev: ec2.vol.Device('/dev/xvda')
Tags set:
[ec2.Tag(resource_id='vol-0d11fd27f3702a0fc', key='Role', value='RootDisk')]
...
[DEBUG] EC2
ID:  ec2.Instance(id='i-02ab1438a79a3e475')
EBS
ID:  ec2.Volume(id='vol-09b6f60396e56c363')
Dev: ec2.vol.Device('/dev/xvda')
Tags set:
[ec2.Tag(resource_id='vol-09b6f60396e56c363', key='Role', value='RootDisk')]
ID:  ec2.Volume(id='vol-0d75c44a594e312a1')
Dev: ec2.vol.Device('/dev/xvdbm')
Tags set:
[ec2.Tag(resource_id='vol-0d75c44a594e312a1', key='Role', value='PvcDisk')]

Проверим диски инстанса i-02ab1438a79a3e47.

Корневой vol-09b6f60396e56c363:

И Kubernetes PVC vol-0d75c44a594e312a1:

Окей – добавление тега Role мы вроде сделали, теперь надо добавить копирование тегов с “родительского” ЕС2.

boto3: копирование тегов ЕС2 к его EBS

Копирование тегов тоже вынесем отдельной функций, назовём её copy_ec2_tags(), и в функцию аргументом будем передавать EC2 ID:

...
def copy_ec2_tags(instance):
            
    tags_list = []
    values = {} 
        
    for instance_tag in instance.tags:

        if instance_tag['Key'] == 'Env':
            tags_list.append(instance_tag)
        elif instance_tag['Key'] == 'Tier':
            tags_list.append(instance_tag)
        elif instance_tag['Key'] == 'DataClass':
            tags_list.append(instance_tag)

    return tags_list
...

В фунцкции в цикле проверяем все теги инстанса, если находим один из трёх нужных тегов – то копируем его в список tags_list[], который потом вернём в vol.create_tags().

Добавляем её вызов в lambda_handler():

...
def lambda_handler(event, context):

    base = ec2.instances.all()

    for instance in base:

        print("\n[DEBUG] EC2\n\t\tID:  " + str(instance))
        print("\tEBS")

        for vol in instance.volumes.all():

            vol_id = str(vol)
            device_id = "ec2.vol.Device('" + str(vol.attachments[0]['Device']) + "')"
            print("\t\tID:  " + vol_id + "\n\t\tDev: " + device_id)

            role_tag = vol.create_tags(Tags=set_role_tag(vol))
            ec2_tags = vol.create_tags(Tags=copy_ec2_tags(instance))
            print("\t\tTags set:\n\t\t\t" + str(role_tag) + "\n\t\t\t" + str(ec2_tags))
...

Запускаем:

./ec2_tags.py
[DEBUG] EC2
ID:  ec2.Instance(id='i-0df2fe9ec4b5e1855')
EBS
ID:  ec2.Volume(id='vol-0d11fd27f3702a0fc')
Dev: ec2.vol.Device('/dev/xvda')
Tags set:
[ec2.Tag(resource_id='vol-0d11fd27f3702a0fc', key='Role', value='RootDisk')]
[ec2.Tag(resource_id='vol-0d11fd27f3702a0fc', key='DataClass', value='Public'), ec2.Tag(resource_id='vol-0d11fd27f3702a0fc', key='Env', value='Dev'), ec2.Tag(resource_id='vol-0d11fd27f3702a0fc', key='Tier', value='Devops')]
...
[DEBUG] EC2
ID:  ec2.Instance(id='i-02ab1438a79a3e475')
EBS
ID:  ec2.Volume(id='vol-09b6f60396e56c363')
Dev: ec2.vol.Device('/dev/xvda')
Tags set:
[ec2.Tag(resource_id='vol-09b6f60396e56c363', key='Role', value='RootDisk')]
[ec2.Tag(resource_id='vol-09b6f60396e56c363', key='Env', value='Dev'), ec2.Tag(resource_id='vol-09b6f60396e56c363', key='DataClass', value='Public'), ec2.Tag(resource_id='vol-09b6f60396e56c363', key='Tier', value='Devops')]
ID:  ec2.Volume(id='vol-0d75c44a594e312a1')
Dev: ec2.vol.Device('/dev/xvdbm')
Tags set:
[ec2.Tag(resource_id='vol-0d75c44a594e312a1', key='Role', value='PvcDisk')]
[ec2.Tag(resource_id='vol-0d75c44a594e312a1', key='Env', value='Dev'), ec2.Tag(resource_id='vol-0d75c44a594e312a1', key='DataClass', value='Public'), ec2.Tag(resource_id='vol-0d75c44a594e312a1', key='Tier', value='Devops')]
...

Проверяем:

Целиком скрипт теперь выглядит так:

#!/usr/bin/env python

import os
import boto3

ec2 = boto3.resource('ec2',
        region_name=os.getenv("AWS_DEFAULT_REGION"),
        aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
        aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY")
    )

def lambda_handler(event, context):

    base = ec2.instances.all()

    for instance in base:

        print("[DEBUG] EC2\n\t\tID:  " + str(instance))
        print("\tEBS")

        for vol in instance.volumes.all():

            vol_id = str(vol)
            device_id = "ec2.vol.Device('" + str(vol.attachments[0]['Device']) + "')"
            print("\t\tID:  " + vol_id + "\n\t\tDev: " + device_id)

            role_tag = vol.create_tags(Tags=set_role_tag(vol))
            ec2_tags = vol.create_tags(Tags=copy_ec2_tags(instance))
            print("\t\tTags set:\n\t\t\t" + str(role_tag) + "\n\t\t\t" + str(ec2_tags) + "\n")

def is_pvc(vol): 

    try:
        for tag in vol.tags:
            if tag['Key'] == 'kubernetes.io/created-for/pvc/name':
                return True
                break
    except TypeError:
            return False

def set_role_tag(vol):
    
    device = vol.attachments[0]['Device']
    tags_list = []
    values = {}
        
    if is_pvc(vol):
        values['Key'] = "Role"
        values['Value'] = "PvcDisk"
        tags_list.append(values)
    elif device == "/dev/xvda":
        values['Key'] = "Role"
        values['Value'] = "RootDisk"
        tags_list.append(values)
    else:   
        values['Key'] = "Role"
        values['Value'] = "DataDisk"
        tags_list.append(values)

    return tags_list

def copy_ec2_tags(instance):
            
    tags_list = []
    values = {} 
        
    for instance_tag in instance.tags:

        if instance_tag['Key'] == 'Env':
            tags_list.append(instance_tag)
        elif instance_tag['Key'] == 'Tier':
            tags_list.append(instance_tag)
        elif instance_tag['Key'] == 'DataClass':
            tags_list.append(instance_tag)
        elif instance_tag['Key'] == 'JiraTicket':
            tags_list.append(instance_tag)

    return tags_list

if __name__ == "__main__":
    lambda_handler(0, 0)

С этим готово, осталось запустить его из AWS Lambda-функции, см. AWS: Lambda – копирование тегов EC2 на EBS, часть 2 – создание Lambda-функции.