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 для конкретного инстанса можно взять тут.

FIX ERROR — fdisk: DOS partition table format cannot be used on drives for volumes larger than 2199023255040 bytes for 512-byte sectors

При попытке создать раздел утилитой "fdisk" размером 2 и более терабайта появляется следующее сообщение:

The size of this disk is 2 TiB (2199023255552 bytes). DOS partition table format cannot be used on drives for volumes larger than 2199023255040 bytes for 512-byte sectors. Use GUID partition table format (GPT).

 

Решение:

Поставить на новый диск лейбл "gpt"

parted /dev/nvme2n1

 

Где "nvme2n1" — ваш диск

 

Ставим лейбл:

mklabel gpt

 

Проверяем:

print

 

 

После чего возвращаемся в fdisk и создаем нужный раздел. Раздел так же можно создать и в утилите "parted"

 

Создадим файловую систему, для этого скопируем полный путь созданного раздела

 

И создаем файловую систему, в данном примере это ext4:

mkfs.ext4 /dev/nvme2n1p1

 

Jenkins — Active Choice: GitHub — Commit

 

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

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

 

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

 

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

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

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

 

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

import jenkins.model.*
import groovy.json.JsonSlurper

credentialsId = 'artem-github'
gitUri = '[email protected]:artem-gatchenko/ansible-openvpn-centos-7.git'

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

def slurper = new JsonSlurper()

def account = gitUri.split(":")[-1].split("/")[0]
def repo = gitUri.split(":")[-1].split("/")[-1].split("\\.")[0]

def addr = "https://api.github.com/repos/${account}/${repo}/commits"
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 parsed = slurper.parseText(response_json)
def commit = []
commit.add("Latest")
for (int i = 0; i < parsed.size(); i++) {
    commit.add(parsed.get(i).sha)
}
return commit

 

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

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

 

 

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

Pipeline:

properties([
  parameters([
    [$class: 'StringParameterDefinition',
      defaultValue: '[email protected]:artem-gatchenko/ansible-openvpn-centos-7.git',
      description: 'Git repository URI',
      name: 'gitUri',
      trim: true
    ],
    [$class: 'CascadeChoiceParameter', 
      choiceType: 'PT_SINGLE_SELECT', 
      description: 'Select Image',
      filterLength: 1,
      filterable: false,
      referencedParameters: 'GIT_URI',
      name: 'GIT_COMMIT_ID', 
      script: [
        $class: 'GroovyScript', 
        script: [
          classpath: [], 
          sandbox: false, 
          script: 
            '''
            import jenkins.model.*
            import groovy.json.JsonSlurper

            credentialsId = 'artem-github'

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

            def slurper = new JsonSlurper()

            def account = gitUri.split(":")[-1].split("/")[0]
            def repo = gitUri.split(":")[-1].split("/")[-1].split("\\\\.")[0]

            def addr = "https://api.github.com/repos/${account}/${repo}/commits"
            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 parsed = slurper.parseText(response_json)
            def commit = []
            commit.add("Latest")
            for (int i = 0; i < parsed.size(); i++) {
                commit.add(parsed.get(i).sha)
            }
            return commit
            '''
        ]
      ]
    ]
  ])
])