Jenkins — Active Choice: PostgeSQL — Вернуть результат SELECT запроса

Для параметризованной сборки с выбором результата на основе SQL запроса, понадобится плагин Active Choices

Переходим в настройки Jenkins

 

Раздел "Управление плагинами"

 

Переходим к вкладке "Доступные" и в поиске указываем "Active Choices"

Устанавливаем его. Так же необходим плагины:

Создаем "New Item" — "Pipeline", указываем, что это будет параметризованная сборка, и добавляем параметр "Active Choices Parameter"

 

 

 

Указываем, что это "Groovy Script" и вставляем туда следующее:

import groovy.sql.Sql
import java.sql.Driver

credentialsId = 'artem-services-rds-credentials'
url = 'artem-services-rds.xxxxxxxxxxxx.eu-central-1.rds.amazonaws.com:5432/postgres'

def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
  com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class, Jenkins.instance, null, null ).find{
    it.id == credentialsId
  }

def driver = Class.forName('org.postgresql.Driver').newInstance() as Driver 

def props = new Properties()
props.setProperty("user", "${creds.username}") 
props.setProperty("password", "${creds.password}")

def conn = driver.connect("jdbc:postgresql://${url}", props) 
def sql = new Sql(conn)

def artifact = []

artifact.add("Not selected")

def rows = sql.rows("select * from users").each { row ->
  artifact.add("$row.first_name")
}

return artifact

 

Где значение переменных, "credentialsId" — Jenkins Credentials ID с логином и паролем для подключения к базе данных;

"url" — строка подключения к базе (endpoint + port + db name);

 

Данный Active Choice делает SELECT запрос к таблице "users" и возвращяет только значения полей "first_name", так же на первую позицию результата добавляет "Not selected".

AWS — S3 разрешить доступ для организации

Для того, чтобы разрешить доступ на чтение с S3 корзины для всех аккаунтов входящих в организацию, на бакет нужно повесить следующую политику:

{
  "Version": "2012-10-17",
  "Statement": {
    "Sid": "AllowOrganizationToReadBucket",
    "Effect": "Allow",
    "Principal": "*",
    "Action": [
      "s3:GetObject",
      "s3:ListBucket"
    ],
    "Resource": [
      "arn:aws:s3:::stackset-lambdas",
      "arn:aws:s3:::stackset-lambdas/*"
    ],
    "Condition": {
      "StringEquals": {"aws:PrincipalOrgID":["o-xxxxxxxxxx"]}
    }
  }
}

 

Где "stackset-lambdas" имя S3 корзины, а "o-xxxxxxxxxx" — ваш Organization ID.

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":
                    engine = rds.describe_db_instances(DBInstanceIdentifier = instance)['DBInstances'][0]['Engine']
                    print("Engine: " + str(engine))
                    if engine.startswith('aurora') == True:
                        cluster_id = rds.describe_db_instances(DBInstanceIdentifier = instance)['DBInstances'][0]['DBClusterIdentifier']
                        print("Stopping Aurora cluster: " + str(cluster_id))
                        rds.stop_db_cluster(DBClusterIdentifier = cluster_id)
                    else:
                        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