Об AWS

2018-01-27

Они говорили: «Большие облачные провайдеры — это хорошо». Они говорили: «Там есть отличная поддержка Docker контейнеров». Они говорили: «Контейнеры идеально подходят для микросервисов». Они говорили: «Микросервисы — это лучшая современная архитектура».

AWS Logo

На прошлой неделе меня развлекала Azure. На этой неделе мы переехали на Amazon Web Services aka AWS. В надежде, что в AWS будет меньше нежданчиков. Ну и просто, AWS был привычнее, и заказчику, и мне.

Нежданчики были. Но они имели объяснение, и часто даже описание в документации.

Документация в AWS мне нравится больше. Вроде объем сравним с азуровской. Но технических подробностей больше. Объясняется, что это, зачем это и с чем это взаимодействует. Меньше пошаговых туториалов со скриншотами. Почти нет маркетинговой чепухи.

Консоль управления (не Portal, а Console) — другая. В Азуре всё напихали панельками в одно большущее веб-приложение. В Амазоне слепили из лоскутков чудовище Франкенштейна. Но это хорошо. Каждый лоскуток — сам по себе, прост и понятен. Разные лоскутки — консольки разных сервисов, оформлены немного по-разному, в разных стилях. Где-то подновлено, попросторнее, настройки чётко сгруппированы. Где-то более старенькое, с большущими боковыми меню.

Единственный мозговзрывающий пункт — список всех сервисов. Его сделали, ура, в несколько колонок на весь экран. И только текстом. И с поиском. Не заблудишься. И, ура, отдельно вынесены последние сервисы, которые открывал.

Самой большой проблемой с консолью были права доступа. Мне не дали суперадмина на весь аккаунт. Пришлось постоянно натыкаться на отсутствие прав и запрашивать их отдельно. И в Азуре, и в Амазоне сущностей, на которые можно раздать права, так много, что просто невозможно заранее выдать нужные ограниченные права.

CLI тоже есть. Известный aws. Тоже написан на Python, хошь на втором, хошь на третьем. Под капотом — библиотечка boto.

В отличие от Азуры, где клиент срёт кучей разных файлов в ~/.azure, с aws и boto всё чётко. Есть лишь два файла: ~/.aws/config, где прописывается нужный регион (десятки их у Амазона), и ~/.aws/credentials, где нужно указать ключ и секрет. И эти файлы нужно создать руками. Можно и несколько профилей указать, если одновременно нужно в разных аккаунтах рулить. Ключа и секрета достаточно, чтобы от имени владельца ключа рулить Амазоном. И из Ansible тоже.

Ансибль может AWS. По сравнению с Азурой, модулей больше, они разнообразнее, они старше минимум на пару лет. Но некоторые свежие фичи, которые в AWS завезли в 2017, Ансиблем ещё не поддерживаются.

Console screenshot

Single Page Applications aka SPA, типа на React, можно заливать непосредственно в файлопомойку Амазона под названием S3. Что, кстати, расшифровывается как Simple Storage Service, любят они слово «Simple». И прикрыть это дело CDN под называнием CloudFront. В обоих сервисах магия сводится к настройке, чтобы в случае любой ошибки, в том числе 404 Not Found, возвращать /index.html.

SPA Lifecycle

Докеры. Реестр для образов имеется: Elastic Container Registry aka ECR. Слово «Elastic» в Амазоне любят ещё больше.

В отличие от Азуры, где пароль для docker login выдаётся раз и навсегда, Амазон выдаёт длииииинющий пароль, который действует лишь 12 часов. Мол, раз вы пароль передаёте в docker login, то он остаётся в истории команд, а это нехорошо. Ну и кто из них больше думает о безопасности?

Новый пароль приходится получать командой aws ecr get-login --no-include-email. Или даже так:

$ $(aws ecr get-login --no-include-email)

Потому что выхлопом команды является уже готовый к исполнению docker login с аргументами. Так что бы не выполнить его сразу?

Можно прикрутить ECR Credentials Helper, штуку такую, расширение для Докера, чтобы Докер сам ходил в Амазон за авторизацией. Но что-то у меня оно сразу не заработало.

В отличие от Азуры, Docker Hub и прочих Docker Registry, ECR не создаёт репозиторий для образа автоматически. Нельзя сделать docker push для образа, про который ECR ещё ничего не знает. Нужно явно создавать репозитории, через консоль, через CLI или через Ansible.

ECR, ECS relations

Образы есть. Они вполне доступны из реестра образов. Теперь нужно деплоить контейнеры. Для этого есть Elastic Container Service aka ECS. Эта штука не совместима ни с чем. И работать с ней нужно через консольку, CLI или Ansible.

Для начала нам нужен ECS кластер. В котором будут выполняться задачи (Task), представляющие собой какие-то сервисы (Service), описанные в определении задач (Task Definition).

В Ansible определение задачи выглядит примерно так:

- name: create ECS task definition
  ecs_taskdefinition:
    state: present
    aws_access_key: '{{ aws_access_key }}'
    aws_secret_key: '{{ aws_secret_key }}'
    family: '{{ service_name }}'
    network_mode: host
    containers:
    - name: '{{ service_name }}'
      image: '{{ service_image_name }}'
      memoryReservation: 64
      portMappings:
      - containerPort: '{{ service_port }}'
      logConfiguration:
        logDriver: 'awslogs'
        options:
          awslogs-group: '{{ ecs_cluster_name }}'
          awslogs-region: '{{ aws_region }}'
          awslogs-stream-prefix: '{{ ecs_cluster_name }}'
      environment:
      - name: SERVER_PORT
        value: '{{ service_port }}'
      - name: DATABASE_URL
        value: 'jdbc:postgresql://{{ service_database_host }}/{{ service_database_name }}'
      - name: DATABASE_USER
        value: '{{ service_database_user }}'
      - name: DATABASE_PASSWORD
        value: '{{ service_database_password }}'
  register: task_output

Для консольки или CLI то же самое нужно переписать в JSON. Поэтому, кстати, внутри определения контейнера появился camelCase, не свойственный Ансиблю.

Как видите, тут нужны ключ и секрет для доступа к AWS.

family — это «семейное» имя для определения задачи. Определения версионируются, поэтому актуальное имя определения будет вида «family:revision».

Про network_mode поговорим попозже.

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

Контейнер, как положено, запускается из определённого образа, image.

Контейнеру обязательно нужно указать требования по памяти. memoryReservation — это мягкий лимит, в мегабайтах.

Как обычно в Docker, можно прокинуть порты, portMappings. Порт внутри, порт снаружи. Впрочем, для network_mode: host порт нужен только один.

logConfiguration — это такая специальная штука, где с помощью драйвера awslogs можно засовывать логи контейнера в CloudWatch — фиговину для мониторинга.

В environment контейнеру передаются переменные окружения, как обычно.

Результат создания описания задачи мы записываем в task_output, потому что он будет нужен для создания сервиса.

Сервис из Ansible создаётся так:

- name: create ECS service
  ecs_service:
    state: present
    aws_access_key: '{{ aws_access_key }}'
    aws_secret_key: '{{ aws_secret_key }}'
    name: '{{ service_name }}'
    cluster: '{{ ecs_cluster_name }}'
    task_definition: '{{ task_output.taskdefinition["family"] }}:{{ task_output.taskdefinition["revision"] }}'
    desired_count: 1
    deployment_configuration:   # allows to shut down and restart the service
      minimum_healthy_percent: 0
      maximum_percent: 100

Снова нужны ключи доступа в Амазон.

name — это имя сервиса. Уникальное в пределах кластера.

cluster — это имя кластера. Он уже должен быть. Так как его обновлять не нужно, проще создать его один раз из консоли. Хотя, конечно, есть и другие варианты.

task_definition — это определение задачи для этого сервиса. Тут нужно указать конкретное определение, включая его фамилию и ревизию. Именно для этого мы записывали результат создания определения в task_output.

desired_count — сколько экземпляров сервиса мы хотим видеть живыми. Получается, что сервис — единица масштабирования. И все контейнеры, объявленные в определении задачи, будут созданы в данном количестве экземпляров.

Но не обязательно. deployment_configuration задаёт требуемые параметры «живости» сервиса. По умолчанию, если мы сменили определение и перезапустили сервис и его контейнеры, ECS сначала создаст вторую копию сервиса, а лишь потом удалит старую копию. Он стремится к zero downtime. Но это создаёт проблемы, если контейнер занимает определённый порт хоста, а хост с Докером у нас только один. Второй контейнер на том же порту не запустишь, поэтому всё ломается. В этом примере разрешается нулевая «живость», и сначала убивается старый контейнер, а потом создаётся новый.

Tasks in ECS

В ECS нужно создать кластер, который будет рулить сервисами, создавать задачи по определениям, в нужном количестве. Но что такое кластер? Тут есть два варианта.

Недавно в AWS появилась штука под названием Fargate. Это магическая фиговина, которая запускает контейнеры незнамо где. Имхо, довольно удобно. И ECS, конечно же, умеет работать через неё. Но Fargate пока доступен только в одном датацентре Амазона, в Северной Вирджинии.

Остальным нужно запускать кластер на старых добрых инстансах EC2, то есть на виртуалках. Всё честно, вам сразу говорят, нужны виртуалки, чтобы запускать контейнеры. На этих инстансах нужно поставить Docker и запустить ECS агента. Но логичнее и проще взять готовый ECS-optimized образ виртуалки, где всё уже стоит. Можно запускать EC2 инстансы не руками, а через Auto Scaling группу. Тогда ECS сам будет заботиться, сколько инстансов нужно, в зависимости от ресурсов, которые будут есть контейнеры.

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

Важен один нюанс. EC2 инстансы должны иметь выход в интернеты. Ибо агентам на них нужно общаться собственно с ECS, а он для них находится где-то там.

Кстати, Auto Scaling можно включить и для сервисов в ECS. Тогда количество задач, то есть количество запущенных контейнеров для этого сервиса, тоже будет динамически подстраиваться в зависимости от нагрузки.

ECS Instances

Контейнеры деплоятся. Хорошо. Но у нас микросервисы. К каким-то сервисам нужен доступ снаружи. А какие-то сервисы нужны для внутренних нужд. Нужно, чтобы сервисы знали друг о друге и имели прямой сетевой доступ друг к другу.

Собственно, эти вопросы в рамках одного хоста умеет решать Docker Compose. Контейнеры видят друг друга по имени сервиса, указанного в docker-compose.yml. И вполне имеют сетевой доступ друг к другу, если находятся в одной сети. В рамках нескольких Docker хостов эти вопросы решает Swarm, с помощью своей overlay сети. Kubernetes, полагаю, имеет схожие механизмы.

Что нам предлагает ECS для организации сети между контейнерами? Он нам предлагает то же, что голый Docker Engine двухгодичной давности. Плюс одна амазоноспецифичная плюшка.

network_mode: host. В этом случае никаких дополнительных сетевых прослоек не имеется. Процесс в контейнере слушает порт на хост машине. Соответственно, на одном порту можно запустить лишь один контейнер на хост. Нужно больше? Тогда нужно больше EC2 инстансов cнизу. Просто и эффективно. Но в общем случае контейнер не знает, на каких EC2 инстансах (с какими IP адресами) запущены другие контейнеры.

network_mode: bridge. Обычный сетевой мост Докера. Внутри контейнера процессы могут вешаться на любой порт. Этот порт может быть выставлен наружу, то есть на EC2 хост, под любым другим номером. Контейнеры на одном EC2 инстансе могут ходить по сети друг к другу. Но, во-первых, нет никакой гарантии, что они окажутся на одном инстансе, только если не были определены в одном ECS таске. Во-вторых, они всё равно не знают ни доменных имён, ни IP адресов (внутренних) друг друга. Поддерживаются старые добрые линки. Но это опять-таки работает только в рамках одного EC2 хоста. К тому же линки требуют, чтобы контейнер, на который ссылаются, был уже запущен. А такой жёсткий порядок запуска, мягко говоря, не удобен.

network_mode: awsvpc. Новая штука. Поддерживается в Fargate. Требует дополнительной настройки агента на EC2 инстансах. В этом случае каждый контейнер становится полноценным участником сетевого обмена в инфраструктуре AWS. Каждый контейнер получает свой собственный elastic network interface в рамках VPC. Проблема в том, что количество этих интерфейсов на один EC2 инстанс ограничено, от двух до пятнадцати, в зависимости от размера инстанса. А это значит, что на одной EC2 машинке можно будет запустить лишь от двух до пятнадцати контейнеров ECS.

Bridge network

В любом случае всё остаётся на уровне голого Docker Engine. Механизма контейнерам обнаружить друг друга и ходить друг другу в гости ECS не предоставляет.

В моих микросервисах уже сложился свой механизм service discovery. Я лишь решил слегка допилить его. Хранить реестр сервисов во внешнем Redis, который в AWS представлен в виде ElastiCache. А код для регистрации себя и обнаружения других сервисов встроить в сами сервисы.

Кстати, узнать IP адрес текущего EC2 инстанса (и контейнеров в нём) легко посредством запроса к Instance Metadata. Буквально вот так:

$ curl http://169.254.169.254/latest/meta-data/local-ipv4

Существуют и более амазоноспецифичные способы самообнаружения контейнеров. Можно запрашивать Load Balancer, ибо он может знать обо всех контейнерах, которые он балансит, где они находятся и на каких портах. Можно внедрить в каждый контейнер агента, который будет регистрировать адрес и порты контейнера в амазоновом DNS (который называется Route 53).

Можно рассматривать это как прикладное применение теоремы Гёделя о неполноте. Контейнеры сами по себе не могут решить задачу самообнаружения. Нужна внешняя сущность в виде базы данных (хотя бы DNS сервер), к которой имеют прямой доступ все контейнеры.

Discovery with DNS

Контейнеры есть. Друг к другу ходить могут. Как обеспечить к ним доступ извне? Логичнее всего использовать Elastic Load Balancing aka ELB. Его вариант, разбирающийся в HTTP, под названием Application Load Balancer.

Балансировщик нагрузки принимает входящий HTTP или HTTPS трафик. Можно навешать свой домен. Можно навешать свой сертификат. И по правилам направляет запросы на Target Groups. В качестве правил можно использовать либо пути, либо заголовки запроса.

В целевых группах указываются либо EC2 инстансы, либо IP адреса, в рамках VPC. Но ведь наши EC2 инстансы для наших контейнеров могут создаваться и удаляться, autoscaling ведь. Чтобы решить эту проблему, ECS может управлять балансировщиком, прописывая туда, где сейчас и на каком порту висит соответствующий сервис. Нужно только в настройках сервиса указать, каким балансировщиком и какой Target Group управлять.

Application Load Balancer в AWS — такая же тупая штука, что и в Azure. Url rewriting не умеет. Проверяет работоспособность бэкендов HTTP GET запросами. Так что все прибабахи для сервисов, типа специального контроллера на /health, остаются в силе. А ещё он не умеет CORS.

CORS aka Cross-Origin Resource Sharing — это такая фигня, которую современные браузеры делают, когда API, куда стучится JavaScript, находится не в том же домене, что страница, на которой выполняется JavaScript. Вполне полезная штука в плане безопасности. Браузер делает OPTIONS запрос, спрашивая: «А вот с этого домена можно?» Сервер отвечает в заголовке Access-Control-Allow-Origin: «С этого можно».

Поддержку CORS нужно делать в контейнере. Можно водрузить Nginx. Можно добавить чуток конфигурации в Spring.

Есть в AWS и более мощная штука под названием API Gateway, которая это всё вроде может. Но API Gateway — это скорее про конструирование API-заглушек, например, для мобильных приложений. Чтобы потом заполнять их вызовами кода из Lambda. Это скорее не про микросервисы, а про конструирование API в облаке на лету, API as a service. Боязно и странно.

Load Balancing

Ладно. Есть докеры двухгодичной свежести. А есть что поудобнее? Есть. Elastic Container Service for Kubernetes aka EKS. Похоже, это заразно, играть буквой «C» в аббревиатурах. Container → Kontainer → Kubernetes.

Вот только EKS в Амазоне «is in Preview». А превью в Амазоне — значительно серьёзнее, чем в Азуре. Это значит написать им, объяснить, зачем нам это надо, и ждать две недели, может быть включат.

EKS

С этими облаками и контейнерами всё получается так. Сначала создаются все эти сущности: балансировщики, кластеры, репозитории. Потом собираются образы и выкладываются в реестры. Потом запускаются или перезапускаются контейнеры по этим образам.

Ансибль в этом мире Immutable Infrastructure выглядит чужеродно. Ибо всё равно все его задачи выполняются на localhost с типом подключения local. Всё равно ведь нужно к AWS API подключаться.

Чувствую, что, когда в следующий раз пойду за облаками, я возьму Terraform. Ты просто описываешь, что, где, с какими свойствами, с чем связанное, в каком количестве тебе развернуть. И оно разворачивает. У AWS есть своя такая штука, CloudFormation называется. Но Terraform более переносим, он умеет не только AWS.

Terraform vs Ansible