Lambda — Для остановки EC2 инстансов, RDS инстансов и сдувания ASG во всех регионах

Данный Python скрипт получает список всех регионов, находит в них EC2 инстансы, RDS инстансы и ASG, и если на ресурсе нет тега "prevent_stop" равного значению "true", то останавливает данный ресурс, а в случае с ASG сжимает его до 0.

main.py:

import boto3

custom_ec2_filter = [
    {
        'Name': 'instance-state-name',
        'Values': ['running', 'pending']
    }
]

# Get list of EC2 regions
ec2 = boto3.client('ec2')
regions = [region['RegionName'] for region in ec2.describe_regions()['Regions']]

def main(event, context):
    for region in regions:
        print("Region: " + str(region))
        # ASG
        print("Searching for ASG...")
        asg = boto3.client('autoscaling', region_name = region)
        asg_list = [asg_name['AutoScalingGroupName'] for asg_name in asg.describe_auto_scaling_groups()['AutoScalingGroups']]
        for asg_name in asg_list:
            api_response = asg.describe_auto_scaling_groups(
                AutoScalingGroupNames = [
                    asg_name,
                ]
            )
            tags = (api_response["AutoScalingGroups"][0]["Tags"])
            asg_scaledown = False 
            if 'prevent_stop' not in [tag['Key'] for tag in tags]:
                asg_scaledown = True
            else:
                for tag in tags:
                    if tag["Key"] == 'prevent_stop' and tag["Value"] != 'true':
                        asg_scaledown = True
            if asg_scaledown == True:
                minSize = (api_response["AutoScalingGroups"][0]["MinSize"])
                maxSize = (api_response["AutoScalingGroups"][0]["MaxSize"])
                desiredCapacity = (api_response["AutoScalingGroups"][0]["DesiredCapacity"])
                if minSize != 0 or maxSize != 0 or desiredCapacity != 0:
                    print("Scalling down ASG: " + str(asg_name))
                    asg.update_auto_scaling_group(
                        AutoScalingGroupName = asg_name,
                        MinSize = 0,
                        MaxSize = 0,
                        DesiredCapacity = 0
                    )
        # EC2 Instances
        print("Searching for running instances...")
        ec2 = boto3.resource('ec2', region_name = region)
        instances_list = ec2.instances.filter(Filters = custom_ec2_filter)
        for instance in instances_list:
            if 'prevent_stop' not in [tag['Key'] for tag in instance.tags]:
                print("Stopping instance: " + str(instance.id))
                instance.stop()
            else:
                for tag in instance.tags:
                    if tag["Key"] == 'prevent_stop' and tag["Value"] != 'true':
                        print("Stopping instance: " + str(instance.id))
                        instance.stop()
        # RDS
        print("Searching for running RDS instances...")
        rds = boto3.client('rds', region_name = region)
        rds_list = [instance['DBInstanceIdentifier'] for instance in rds.describe_db_instances()['DBInstances']]
        for instance in rds_list:
            arn = rds.describe_db_instances(DBInstanceIdentifier = instance)['DBInstances'][0]['DBInstanceArn']
            tags = rds.list_tags_for_resource(ResourceName = arn)['TagList']
            stop_instance = False
            if 'prevent_stop' not in [tag['Key'] for tag in tags]:
                stop_instance = True
            else:
                for tag in tags:
                    if tag["Key"] == 'prevent_stop' and tag["Value"] != 'true':
                        stop_instance = True
            if stop_instance == True:
                instance_status = rds.describe_db_instances(DBInstanceIdentifier = instance)['DBInstances'][0]['DBInstanceStatus']
                if instance_status != "stopping" and instance_status != "stopped":
                    print("Stopping RDS instances: " + str(instance))
                    rds.stop_db_instance(DBInstanceIdentifier = instance)
        print("----------------------------------------")

if __name__ == '__main__':
    main()

 

Список нужных разрешений для запуска (помимо "AWSLambdaExecute" политики):

  • ec2:DescribeRegions
  • ec2:DescribeInstances
  • ec2:StopInstances
  • rds:ListTagsForResource
  • rds:DescribeDBInstances
  • rds:StopDBInstance
  • autoscaling:DescribeAutoScalingGroups
  • autoscaling:UpdateAutoScalingGroup

PagerDuty — Python скрипт для создания событий

Данный Python скрипт создает события в PagerDuty используя APIv2.

За основу был взят следующий скрипт.

Для начала нужно создать "Routing Key", он же "Integration Key", не путать с "API Access Key", который можно использовать для любых API вызовов, нам же нужен только ключ от определенного сервиса.

Переходим в настройки сервиса, в моем случае он называется "AWS", и переходим к вкладке  "Integrations"

 

Добавляем новую интеграцию — "Add a new integration"

 

Задаем имя и выбираем "Integration Type" -> "Use our API directly" -> "Events API v2" и получаем "Integration Key"

main.py:

import requests
import json

from typing import Any, Dict, List, Optional

routing_key = 'abc123'
api_url = 'https://events.pagerduty.com/v2/enqueue'

def create_event(
    summary: str,
    severity: str,
    source: str = 'aws',
    action: str = 'trigger',
    dedup_key: Optional[str] = None,
    custom_details: Optional[Any] = None,
    group: Optional[str] = None,
    component: Optional[str] = None,
    class_type: Optional[str] = None,
    images: Optional[List[Any]] = None,
    links: Optional[List[Any]] = None
) -> Dict:
    payload = {
        "summary": summary,
        "severity": severity,
        "source": source,
    }
    if custom_details is not None:
        payload["custom_details"] = custom_details
    if component:
        payload["component"] = component
    if group:
        payload["group"] = group
    if class_type:
        payload["class"] = class_type

    actions = ('trigger', 'acknowledge', 'resolve')
    if action not in actions:
        raise ValueError("Event action must be one of: %s" % ', '.join(actions))
    data = {
        "event_action": action,
        "routing_key": routing_key,
        "payload": payload,
    }
    if dedup_key:
        data["dedup_key"] = dedup_key
    elif action != 'trigger':
        raise ValueError(
            "The dedup_key property is required for event_action=%s events, and it must \
            be a string."
            % action
        )
    if images is not None:
        data["images"] = images
    if links is not None:
        data["links"] = links

    response = requests.post(api_url, json = data)
    print(response.content)

def main():
    create_event(summary = "Instance is down", severity = 'critical', custom_details = "Instance ID: i-12345")
  
if __name__ == '__main__':
    main()

 

Замените значение переменной "routing_key" на значение "Integration Key"

В данном примере будет создано событие с уровнем "critical", заголовком "Instance is down" и в деталях будет указано "Instance ID: i-12345"

Так же скрипт можно использовать для создания событий на основе AWS CloudWatch Alarms, не используя AWS интеграцию в PagerDuty. Для этого создайте SNS топик, Lambda функцию и укажите SNS топик как источник для Lambda функции. После чего можно создавать CloudWatch Alarm и в качестве действия при срабатывании указываем SNS топик.

main.py (Lambda + SNS):

import requests
import json

from typing import Any, Dict, List, Optional

routing_key = 'abc123'
api_url = 'https://events.pagerduty.com/v2/enqueue'

def get_message_info():
    subject = event["Records"][0]["Sns"]["Subject"]
    message = event["Records"][0]["Sns"]["Message"]
    return subject, message

def create_event(
    summary: str,
    severity: str,
    source: str = 'cloudwatch',
    action: str = 'trigger',
    dedup_key: Optional[str] = None,
    custom_details: Optional[Any] = None,
    group: Optional[str] = None,
    component: Optional[str] = None,
    class_type: Optional[str] = None,
    images: Optional[List[Any]] = None,
    links: Optional[List[Any]] = None
) -> Dict:
    payload = {
        "summary": summary,
        "severity": severity,
        "source": source,
    }
    if custom_details is not None:
        payload["custom_details"] = custom_details
    if component:
        payload["component"] = component
    if group:
        payload["group"] = group
    if class_type:
        payload["class"] = class_type

    actions = ('trigger', 'acknowledge', 'resolve')
    if action not in actions:
        raise ValueError("Event action must be one of: %s" % ', '.join(actions))
    data = {
        "event_action": action,
        "routing_key": routing_key,
        "payload": payload,
    }
    if dedup_key:
        data["dedup_key"] = dedup_key
    elif action != 'trigger':
        raise ValueError(
            "The dedup_key property is required for event_action=%s events, and it must \
            be a string."
            % action
        )
    if images is not None:
        data["images"] = images
    if links is not None:
        data["links"] = links

    response = requests.post(api_url, json = data)
    print(response.content)

def main(event, context):
    info = get_message_info()
    create_event(summary = info[0], severity='critical', custom_details = info[1])
  
if __name__ == '__main__':
    main()

 

Python пакет "requests" придется упаковывать в zip архив с Lambda функцией, так как в Lambda его нет. Для этого можно воспользоваться "virtualenv".

AWS Transfer — Public FTP

AWS Transfer поддерживает 3 протокола: SFTP, FTP и FTPS. И только SFTP может иметь публичный эндпоинт, FTP/FTPS можно запускать только внутри VPC. Так же для авторизации по логину/паролю, необходимо использовать кастомный провайдер, больше информации об этом вы можете найти тут.

Цель:

Создать AWS Transfer сервер для протокола FTP, сервис должен быть публичным и так же авторизация должна проходить по логину/паролю.

Протокол FTP является небезопасным, AWS не рекомендует его использовать в публичных сетях.

 

Первое что понадобится, это установленный AWS SAM CLI.

Создаем директорию, куда будем скачивать темплейт, переходим в нее и скачиваем:

wget https://s3.amazonaws.com/aws-transfer-resources/custom-idp-templates/aws-transfer-custom-idp-secrets-manager-sourceip-protocol-support-apig.zip

 

Разархивируем и выполняем следующую команду:

sam deploy --guided --stack-name aws-transfer-ftp

 

Где, "aws-transfer-ftp" — имя создаваемого CloudFormation стека, если укажите имя существующего, он его обновит.

 

После чего запустится интерактивная установка, где будет предложено указать следующие параметры:

  • Stack Name — имя CloudFormation стека, по умолчанию берется параметр ключа "—stack-name";
  • AWS Region — регион, в котором будет развернут CloudFormation стек;
  • Parameter CreateServer — будет ли создан AWS Transfer сервис (по умолчанию — true);
  • Parameter SecretManagerRegion — если ваш регион не поддерживает SecretsManager, то для него вы можете указать отдельный регион;
  • Parameter TransferEndpointTypePUBLIC или VPC, так как FTP не поддерживает публичные эндпоинты, указываем VPC;
  • Parameter TransferSubnetIDsID's сетей, в которых будет AWS Transfer эндпоинт;
  • Parameter TransferVPCIDVPC ID, в которой находятся сети, указанные в предыдущем параметре.

 

 

Сразу создадим SecurityGroup для FTP сервиса в нужной VPC. И разрешим входящий траффик на TCP порты 21 и 81928200 с любого адреса. Пока сохраняем созданную SG, в дальнейшем мы ее приатачим.

После чего переходим в AWS консоль — "AWS Transfer Family", находим созданный AWS Transfer сервер и редактируем его прокол, снимает галочку с протокола "SFTP" и ставим галочку напротив "FTP" протокола и сохраняем изменения.

Теперь нужно добавить доступ к FTP из мира, для этого будем использовать NLB. Для начала узнаем приватные IP адреса VPC эндпоинтов для AWS Transfer, для этого в блоке "Endpoint details" кликнем на ссылку на VPC эндпоинт.

 

Переходим во вкладку "Subnets" и копируем все IP адреса, они понадобятся для создания таргет групп.

 

Тут же переходим во вкладку "Security Groups" и меняем дефолтную группу безопасности на созданную раннее.

Теперь создадим таргет группы, для это в AWS консоли переходим "EC2" -> "Load Balancing" -> "Target Groups" и создаем первую таргет группу для TCP 21 порта.

  • Target type: IP address
  • Protocol: TCP
  • Port: 21

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

Так же укажем VPC, в которой был создан сервис AWS Transfer. В следующей вкладке один за одним укажем IP адреса VPC эндпоинта, которые мы смотрели раннее. Сохраняем таргет группу.

Теперь нужно создать еще 9 таргет групп, для диапазона портов: TCP 81928200. Процедура такая же, как для таргет группы для порта 21, за исключением того, что нужно HeathCheck порт указать 21. Для этого в блоке "Health checks" открываем вкладку "Advanced health check setting", выбираем "Overrive" и указываем номер порта — 21.

После того, как закончили с таргет группами, нужно создать Network Load Balancer с типом "internet-facing" и разместить его в публичных сетях той же VPC, где и AWS Transfer сервис. Так же создаем 10 листенеров, для TCP портов 21-го, и диапазона 81928200, и для каждого листенера указываем нужную таргет группу, соответствующую номеру порта. После чего FTP сервис должен быть доступ из вне.

Для того, чтобы добавить FTP пользователя нужно в AWS консоли перейти в "Secrets Manager" и создать секрет с типом "Other type of secrets"

 

Создаем 3 пары "key/value":

  • Password — пароль для нового FTP пользователя;
  • RoleARN роли, у которой есть права записи в нужный S3 бакет;
  • HomeDirectoryDetails — [{"Entry": "/", "Target": "/s3-bucket/user-name"}]

Где "s3-bucket" — имя S3 корзины, "user-name" — имя директории, в которую будет попадать пользователь при подключении к FTP серверу (имя директории не обязательно должно соответствовать имени пользователя, и так же может находится не в корне корзины)

 

Сохраняем секрет обязательно с именем в формате: "server_id/user_name", где "server_id" — это ID сервера AWS Transfer, "user_name" — имя пользователя, которое будет использоваться для подключения к FTP серверу.

Так же для удобства можно создать DNS CNAME запись на NLB запись.

FIX ERROR — RDS: Error creating DB Parameter Group: InvalidParameterValue: ParameterGroupFamily

При создании RDS указав не верное значение параметра "ParameterGroupFamily" может возникнуть похожая ошибка:

Error creating DB Parameter Group: InvalidParameterValue: ParameterGroupFamily default.mariadb10.2 is not a valid parameter group family

 

Чтобы посмотреть список всех возможных значений параметра "ParameterGroupFamily" можно использовать следующую команду:

aws rds describe-db-engine-versions --query "DBEngineVersions[].DBParameterGroupFamily"

Docker Compose — Задаем лимит размера логов

По умолчанию Docker Compose не задает никаких лимитов на размер логов. Для примера зададим лимит в 10 Мб и максимальное количество файлов для ротации — 10.

version: "3.8"
services:
  some-service:
    image: some-service
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "10"

Nginx — Regular Expression Tester

 

Для быстрого тестирования регулярных выражений Nginx'а, можно воспользоваться готовым докер образом. Для этого нужно клонировать репозиторий NGINX-Demos:

git clone https://github.com/nginxinc/NGINX-Demos

 

Переходим в директорию "nginx-regex-tester":

cd NGINX-Demos/nginx-regex-tester/

 

И запускаем контейнер с помощью "docker-compose":

docker-compose up -d

 

И открываем следующую страницу:

http://localhost/regextester.php

 

AWS — EKS Fargate — Fluentd CloudWatch

На момент написания статьи EKS Fargate не поддерживал драйверлог для записи в CloudWatch. Единственный вариант — использовать Sidecar

Создадим ConfigMap, в котором укажем имя EKS кластера, регион и namespace:

kubectl create configmap cluster-info \
--from-literal=cluster.name=YOUR_EKS_CLUSTER_NAME \
--from-literal=logs.region=YOUR_EKS_CLUSTER_REGION -n KUBERNETES_NAMESPACE

 

Далее создадим сервис аккаунт и ConfigMap с файлом конфигурации для Fluentd. Для этого скопируем текст ниже и сохраним его как файл "fluentd.yaml"

apiVersion: v1
kind: ServiceAccount
metadata:
  name: fluentd
  namespace: {{NAMESPACE}}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: fluentd-role
rules:
  - apiGroups: [""]
    resources:
      - namespaces
      - pods
      - pods/logs
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: fluentd-role-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: fluentd-role
subjects:
  - kind: ServiceAccount
    name: fluentd
    namespace: {{NAMESPACE}}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
  namespace: {{NAMESPACE}}
  labels:
    k8s-app: fluentd-cloudwatch
data:
  fluent.conf: |
    @include containers.conf

    <match fluent.**>
      @type null
    </match>
  containers.conf: |
    <source>
      @type tail
      @id in_tail_container_logs
      @label @containers
      path /var/log/application.log
      pos_file /var/log/fluentd-containers.log.pos
      tag *
      read_from_head true
      <parse>
        @type none
        time_format %Y-%m-%dT%H:%M:%S.%NZ
      </parse>
    </source>

    <label @containers>
      <filter **>
        @type kubernetes_metadata
        @id filter_kube_metadata
      </filter>

      <filter **>
        @type record_transformer
        @id filter_containers_stream_transformer
        <record>
          stream_name "#{ENV.fetch('HOSTNAME')}"
        </record>
      </filter>

      <filter **>
        @type concat
        key log
        multiline_start_regexp /^\S/
        separator ""
        flush_interval 5
        timeout_label @NORMAL
      </filter>

      <match **>
        @type relabel
        @label @NORMAL
      </match>
    </label>

    <label @NORMAL>
      <match **>
        @type cloudwatch_logs
        @id out_cloudwatch_logs_containers
        region "#{ENV.fetch('REGION')}"
        log_group_name "/aws/containerinsights/#{ENV.fetch('CLUSTER_NAME')}/application"
        log_stream_name_key stream_name
        remove_log_stream_name_key true
        auto_create_stream true
        <buffer>
          flush_interval 5
          chunk_limit_size 2m
          queued_chunks_limit_size 32
          retry_forever true
        </buffer>
      </match>
    </label>

 

И применим его:

curl fluentd.yaml | sed "s/{{NAMESPACE}}/default/" | kubectl apply -f -

 

Где "default" имя нужного неймспейса

 

Пример деплоймента с сайдкаром:

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: testapp
  name: testapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: testapp
  strategy: {}
  template:
    metadata:
      labels:
        app: testapp
    spec:
      serviceAccountName: fluentd
      terminationGracePeriodSeconds: 30
      initContainers:
        - name: copy-fluentd-config
          image: busybox
          command: ['sh', '-c', 'cp /config-volume/..data/* /fluentd/etc']
          volumeMounts:
            - name: config-volume
              mountPath: /config-volume
            - name: fluentdconf
              mountPath: /fluentd/etc
      containers:
      - image: alpine:3.10
        name: alpine
        command: ["/bin/sh"]
        args: ["-c", "while true; do echo hello 2>&1 | tee -a /var/log/application.log; sleep 10;done"]
        volumeMounts:
        - name: fluentdconf
          mountPath: /fluentd/etc
        - name: varlog
          mountPath: /var/log
      - image: fluent/fluentd-kubernetes-daemonset:v1.7.3-debian-cloudwatch-1.0
        name: fluentd-cloudwatch
        env:
          - name: REGION
            valueFrom:
              configMapKeyRef:
                name: cluster-info
                key: logs.region
          - name: CLUSTER_NAME
            valueFrom:
              configMapKeyRef:
                name: cluster-info
                key: cluster.name
          - name: AWS_ACCESS_KEY_ID
            value: "XXXXXXXXXXXXXXX"
          - name: "AWS_SECRET_ACCESS_KEY"
            value: "YYYYYYYYYYYYYYY"
        resources:
          limits:
            memory: 400Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
          - name: config-volume
            mountPath: /config-volume
          - name: fluentdconf
            mountPath: /fluentd/etc
          - name: varlog
            mountPath: /var/log
      volumes:
        - name: config-volume
          configMap:
            name: fluentd-config
        - name: fluentdconf
          emptyDir: {}
        - name: varlog
          emptyDir: {}

 

В данном деплойменте в переменных заданы "AWS_ACCESS_KEY_ID" и "AWS_SECRET_ACCESS_KEY", так как в данный момент существуют эндпоинты для IAM ролей только для сервисов: EC2, ECS Fargate и Lambda. Чтобы избежать этого, вы можете использовать OpenID Connect провайдер для EKS.

Kubernetes — Одна роль для нескольких пространств имен

 

 

Цель:

Есть 2 пространства имен, это "kube-system" и "default". Нужно запускать крон задачу в пространстве "kube-system", которая будет очищать выполненные джобы и поды в пространстве "default". Для этого создадим сервис аккаунт в пространстве "kube-system", роль с необходимыми правами в пространстве "default", и для созданного аккаунта привязываем созданную роль.

cross-namespace-role.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: jobs-cleanup
  namespace: kube-system
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: jobs-cleanup
  namespace: default
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "watch", "list", "delete"]
- apiGroups: ["batch", "extensions"]
  resources: ["jobs"]
  verbs: ["get", "list", "watch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: jobs-cleanup
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: jobs-cleanup
subjects:
- kind: ServiceAccount
  name: jobs-cleanup
  namespace: kube-system

FIX ERROR — EKS: cloudwatch — 0/1 nodes are available: 1 Insufficient pods.

После установки CloudWatch Agent'а в EKS кластер, его поды повисают в состоянии "Pending"

 

Смотрим дескрайб пода

 

Решение:

Решение было найдено тут. В данном EKS кластере для системных подов выделена отдельная NodeGroup'а с типом истансов: t3.micro и для запуска еще и CloudWatch Agent'a просто не хватает капасити.

 

После изменения типа инстанса в большую сторону — t3.small, все поды перешли в статус "Running"

EKS имеет лимиты на количество подов на каждой ноде, данный лимит можно посчитать используя следующую формулу:

N * (M-1) + 2

Где, N — количество Elastic Network Interfaces (ENI) для данного типа инстанса, M — количество IP адрессов для одного ENI

Значения N и M для конкретного инстанса можно взять тут.