AWS — Lambda: kubectl

Пример того, как можно создавать сущности в Kubernetes, используя AWS Lambda.

Функция будет на Python3, так что воспользуемся Kubernetes Python Client

Больше примеров по использованию можно найти тут.

Так как AWS Lambda не поддерживает данный пакет, упакуем в нашу функцию модули "kubernetes" и "boto3".

"boto3" понадобится для обращения в AWS SSM, где будет хранится kubeconfig

 

Подготовка

Создадим директорию для лямбды и перейдем в нее:

1
2
mkdir lambda
cd lambda

 

Дальше понадобится "virtualenv", если его нет, можно его установить используя "pip3":

1
pip3 install virtualenv

 

Создаем виртуальное окружение и активируем его:

1
2
python3 -m virtualenv .
source bin/activate

 

И ставим необходимые модули:

1
2
pip3 install kubernetes
pip3 install boto3

 

Дальше нам нужно только содержимое данной директории:

1
$VIRTUAL_ENV/lib/python3.7/site-packages

 

"python3.7" — замените на свою версию Python

 

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

1
2
echo $VIRTUAL_ENV/lib/python3.7/site-packages
/private/tmp/lambda/lib/python3.7/site-packages

 

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

1
2
mkdir ~/lambda_upload
cp -R /private/tmp/lambda/lib/python3.7/site-packages/. ~/lambda_upload/

 

SSM Parameter Store

В консоли AWS перейдем в "Systems Manager" -> "Parameter Store"

 

И создадим параметр с типом "SecureString" и в качестве значение скопируем содержимое "kubeconfig" файла. Если это EKS, предварительно создайте сервис аккаунт, чтобы он не требовал AWS авторизации, как в примере ниже:

Kubeconfig с AWS авторизацией:

1
2
3
4
5
6
7
8
9
10
11
12
...
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      args:
      - --region
      - eu-central-1
      - eks
      - get-token
      - --cluster-name
      - artem-services-stage-eks
      command: aws

 

Функция

Теперь в директории "~/lambda_upload" создадим файл с именем "lambda_function.py" и вставим в него следующий текст:

lambda_function.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import os
import time
import random
import string
import boto3
 
from kubernetes import config
from kubernetes.client import Configuration
from kubernetes.client.api import core_v1_api
from kubernetes.client.rest import ApiException
from kubernetes.stream import stream
 
# Get Kubeconfig from SSM
def get_kube_config():
    awsRegion = os.environ['AWS_REGION']
    ssmParameter = os.environ['SSM']
 
    ssm = boto3.client('ssm', region_name=awsRegion)
    parameter = ssm.get_parameter(Name=ssmParameter, WithDecryption=True)
 
    kubeconfig = open( '/tmp/kubeconfig.yaml', 'w' )
    kubeconfig.write(parameter['Parameter']['Value'])
    kubeconfig.close()
 
# Generate random string for unique name of Pod
def randomString(stringLength=8):
    letters = string.ascii_lowercase + string.digits
    return ''.join(random.choice(letters) for i in range(stringLength))
 
def exec_commands(api_instance):
    name = "busybox-" + randomString()
    namespace = "default"
    resp = None
 
    print("Creating pod...")
 
    pod_manifest = {
        'apiVersion': 'v1',
        'kind': 'Pod',
        'metadata': {
            'name': name
        },
        'spec': {
            'containers': [{
                'image': 'busybox',
                'name': 'busybox',
                "args": [
                    "/bin/sh",
                    "-c",
                    "while true;do date;sleep 5; done"
                ]
            }]
        }
    }
    resp = api_instance.create_namespaced_pod(body=pod_manifest, namespace=namespace)
 
    while True:
        resp = api_instance.read_namespaced_pod(name=name, namespace=namespace)
        if resp.status.phase == 'Pending' or resp.status.phase == 'Running':
            print("Done. Pod " + name + " was created.")
            break
        time.sleep(1)
 
def main(event, context):
    get_kube_config()
 
    config.load_kube_config(config_file="/tmp/kubeconfig.yaml")
    c = Configuration()
    c.assert_hostname = False
    Configuration.set_default(c)
    core_v1 = core_v1_api.CoreV1Api()
 
    exec_commands(core_v1)
 
 
if __name__ == '__main__':
    main()

 

Теперь все содержимое директории можно упаковывать в "zip" архив и загружать в Lambda функцию.

В переменном окружении создайте 2 переменных, в которых укажите ваш AWS Region и имя SSM Parameter Store, который создали раннее.

 

Для Lambda функции потребуются права на чтение из SSM, так что приатачте к роли, которую использует Lambda следующую политику:

arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess

 

После запуска функции, она создаст pod с именем "busybox-" + сгенерированная строка, из 8-ми символов. Проверки состояния pod'а нет, так как данный скрипт планируется использовать для EKS Fargate, чтобы не ждать 1-2 минуты, пока Fargate инстанс поднимется, так что при любом из состояний, "Pending" или "Running" считаем, что pod был успешно создан.

FIX ERROR — AWS Lambda Python: "main() takes 0 positional arguments but 2 were given"

При попытке выполнить Lambda Python функцию возникает следующая ошибка:

{
  "errorMessage": "main() takes 0 positional arguments but 2 were given",
  "errorType": "TypeError",
  "stackTrace": [
    "  File \"/var/runtime/bootstrap.py\", line 131, in handle_event_request\n    response = request_handler(event, lambda_context)\n"
  ]
}

 

Решение:

Из сообщения видим, что мы в качестве хендлера используем функцию "main", которая не имеет входящих аргументов.

1
def main():

 

А для запуска требуется хендлера 2 аргумента "event" и "context", так что нужно привести объявление функции к следующему виду:

1
def main(event, context):

AWS — Скрипт получения метрик из CloudWatch

Пример Python3 скрипта, для получения метрики из AWS CloudWatch. В примере получаем максимальное значение за последнюю минуту и выводим только значение, это необходимо если вы хотите собирать метрики к примеру в Zabbix.

 

Script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python3
 
import boto3
import datetime
 
awsRegion = "eu-west-1"
namespace = "AWS/ElastiCache"
metric = "CurrConnections"
statistics = "Maximum"
period = 60 # Seconds
timeRange = 1 # Minutes
 
client = boto3.client('cloudwatch', region_name=awsRegion)
 
startTime = (datetime.datetime.utcnow() - datetime.timedelta(minutes=timeRange))
startTime = startTime.strftime("%Y-%m-%dT%H:%M:%S")
endTime = datetime.datetime.utcnow()
endTime = endTime.strftime("%Y-%m-%dT%H:%M:%S")
 
response = client.get_metric_statistics(
    Namespace=namespace,
    MetricName=metric,
    StartTime=startTime,
    EndTime=endTime,
    Period=period,
    Statistics=[
        statistics,
    ]
)
 
for cw_metric in response['Datapoints']:
    print(cw_metric['Maximum'])

Jenkins — Active Choice: Harbor — Images tag

 

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

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

 

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

 

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

Устанавливаем его. Так же необходим плагин Amazon Web Services SDK

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

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import jenkins.model.*
import groovy.json.JsonSlurper
 
credentialsId = 'harbor_credentials'
 
def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
  com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class, Jenkins.instance, null, null ).find{
    it.id == credentialsId}
 
def addr = "${harborRepo}/tags"
def authString = "${creds.username}:${creds.password}".getBytes().encodeBase64().toString()
def conn = addr.toURL().openConnection()
conn.setRequestProperty( "Authorization", "Basic ${authString}" )
def response_json = "${conn.content.text}"
def slurper = new JsonSlurper()
def parsed = slurper.parseText(response_json)
 
def tags=[]
 
for (tag in parsed){
    tags.add(tag.name);
}
 
return tags

 

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

"harborRepo" — полный путь к нужному репозиторию;

 

 

Тоже самое, но уже через Pipeline

Pipeline:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
properties([
  parameters([
    [$class: 'CascadeChoiceParameter',
      choiceType: 'PT_SINGLE_SELECT',
      description: 'Select Image',
      filterLength: 1,
      filterable: false,
      name: 'ImageTag',
      script: [
        $class: 'GroovyScript',
        script: [
          classpath: [],
          sandbox: false,
          script:
            '''
            import jenkins.model.*
            import groovy.json.JsonSlurper
 
            credentialsId = 'harbor_credentials'
            harborRepo = 'https://harbor.artem.services/api/repositories/artem/site'
 
            def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
              com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class, Jenkins.instance, null, null ).find{
                it.id == credentialsId}
 
            def addr = "${harborRepo}/tags"
            def authString = "${creds.username}:${creds.password}".getBytes().encodeBase64().toString()
            def conn = addr.toURL().openConnection()
            conn.setRequestProperty( "Authorization", "Basic ${authString}" )
            def response_json = "${conn.content.text}"
            def slurper = new JsonSlurper()
            def parsed = slurper.parseText(response_json)
 
            def tags=[]
 
            for (tag in parsed){
                tags.add(tag.name);
            }
 
            return tags
            '''
        ]
      ]
    ]
  ])
])

FIX ERROR — CentOS+Nginx+Jenkins: 502 Bad Gateway

При проксировании Nginx' а на Jenkins в ОС CentOS может возникать 502 ошибка. Лог ошибок Nginx'а будет следующим:

2020/05/07 13:32:33 [crit] 9665#9665: *1 connect() to 127.0.0.1:8080 failed (13: Permission denied) while connecting to upstream, client: 1.2.3.4, server: jenkins.artem.services, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8080/", host: "jenkins.artem.services"

 

Решение:

Причина SELinux. Можно как разрешить Jenkins, так и вовсе отключить SELinux.

Вариант с отключением SELinux

Откроем файл конфигурации:

1
vim /etc/selinux/config

 

И поменяем "enforcing" на "disabled"

1
SELINUX=disabled

 

И перезагрузим сервер.

В CentOS 6 можно отключить SELinux без перезагрузки (CentOS 7/8 — требуют перезагрузку), выполнив следующую команду:

1
echo 0 > /selinux/enforce

Linux — Монтирование раздела по лейблу

Список лейблов

Посмотреть список существующих лейблов можно по следующему пути:

1
/dev/disk/by-label/

 

Если данной директории не существует, значит в системе нет ни одного лейбла

 

Добавление лейбла

Для разных файловых систем лейбл добавляется по разному

ext2/ext3/ext4:

1
e2label /dev/sda1 LABEL

reiserfs:

1
reiserfstune -l LABEL /dev/sda1

jfs:

1
jfs_tune -L LABEL /dev/sda1

xfs:

1
xfs_admin -L LABEL /dev/sda1

 

Где, "LABEL" — уникальный лейбл, "/dev/sda1" — нужный раздел

 

fstab

Пример записи в fstab, для монтирования xfs раздела по лейблу "SITE"

1
LABEL=SITE  /var/www/html/site  xfs defaults    0 0

 

Jenkins — Active Choice: AWS ECR Images tag (AWS SDK)

 

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

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

 

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

 

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

Устанавливаем его. Так же необходим плагин Amazon Web Services SDK

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

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.ecr.AmazonECR;
import com.amazonaws.services.ecr.AbstractAmazonECR;
import com.amazonaws.services.ecr.AmazonECRClient;
import com.amazonaws.services.ecr.model.ListImagesRequest;
import com.amazonaws.services.ecr.model.ListImagesResult;
import com.amazonaws.services.ecr.AmazonECRClientBuilder;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.RegionUtils;
import com.amazonaws.regions.Regions;
import jenkins.model.*
 
AmazonECR client = AmazonECRClientBuilder.standard().withRegion("eu-west-1").build();
ListImagesRequest request = new ListImagesRequest().withRepositoryName("artem-services");
res = client.listImages(request);
 
 
def result = []
for (image in res) {
   result.add(image.getImageIds());
}
 
return result[0].imageTag;

 

Где, "eu-west-1" — регион, в котором находится ECR репозиторий;

"artem-services" — имя вашего ECR репозитория;

 

 

Тоже самое, но уже через Pipeline

Pipeline:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
properties([
  parameters([
    [$class: 'CascadeChoiceParameter',
      choiceType: 'PT_SINGLE_SELECT',
      description: 'Select Image',
      filterLength: 1,
      filterable: false,
      name: 'ImageTag',
      script: [
        $class: 'GroovyScript',
        script: [
          classpath: [],
          sandbox: false,
          script:
            '''
            import com.amazonaws.client.builder.AwsClientBuilder;
            import com.amazonaws.services.ecr.AmazonECR;
            import com.amazonaws.services.ecr.AbstractAmazonECR;
            import com.amazonaws.services.ecr.AmazonECRClient;
            import com.amazonaws.services.ecr.model.ListImagesRequest;
            import com.amazonaws.services.ecr.model.ListImagesResult;
            import com.amazonaws.services.ecr.AmazonECRClientBuilder;
            import com.amazonaws.regions.Region;
            import com.amazonaws.regions.RegionUtils;
            import com.amazonaws.regions.Regions;
            import jenkins.model.*
 
            AmazonECR client = AmazonECRClientBuilder.standard().withRegion("eu-west-1").build();
            ListImagesRequest request = new ListImagesRequest().withRepositoryName("artem-services");
            res = client.listImages(request);
 
 
            def result = []
            for (image in res) {
               result.add(image.getImageIds());
            }
 
            return result[0].imageTag;
            '''
        ]
      ]
    ]
  ])
])

Jenkins — Active Choice: AWS ECR Images tag (AWS Cli)

 

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

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

 

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

 

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

Устанавливаем его.

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

 

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

1
2
3
def command = ['/bin/sh', '-c', '/var/lib/jenkins/.local/bin/aws ecr describe-images --region eu-west-1 --repository-name artem-services --query "sort_by(imageDetails,& imagePushedAt)[ * ].imageTags[ * ]" --output text']
  def proc = command.execute()
  return proc.text.readLines()

 

Где, "/var/lib/jenkins/.local/bin/aws" — полный путь к AWS Cli;

"eu-west-1" — регион, в котором находится ECR репозиторий;

"artem-services"  — имя вашего ECR репозитория;

 

 

Тоже самое, но уже через Pipeline

Pipeline:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
properties([
  parameters([
    [$class: 'CascadeChoiceParameter',
      choiceType: 'PT_SINGLE_SELECT',
      description: 'Select Image',
      filterLength: 1,
      filterable: false,
      name: 'ImageTag',
      script: [
        $class: 'GroovyScript',
        script: [
          classpath: [],
          sandbox: false,
          script:
            '''
            def command = ['/bin/sh', '-c', '/var/lib/jenkins/.local/bin/aws ecr describe-images --region eu-west-1 --repository-name artem-services --query "sort_by(imageDetails,& imagePushedAt)[ * ].imageTags[ * ]" --output text']
              def proc = command.execute()
              return proc.text.readLines()
            '''
        ]
      ]
    ]
  ])
])

AWS Cli — ECR вывести только теги образов в репозитории

Пример только с использованием AWS Cli, без сторонних утилит

1
2
3
4
5
aws ecr describe-images \
--region eu-west-1 \
--repository-name artem-services \
--query "sort_by(imageDetails,& imagePushedAt)[ * ].imageTags[ * ]" \
--output text

 

Пример с использованием утилиты jq

1
2
3
4
aws ecr describe-images \
--region eu-west-1 \
--repository-name artem-services \
| jq '.imageIds | map (.imageTag)|sort|.[]'