AWS: CloudFormation – создание EFS + Ansible роль

By | 08/01/2018
 

Задача – добавить ресурсы AWS Elastic File System в существующий CloudFormation стек.

В CloudFormation для этого имеется ресурс AWS::EFS::FileSystem, который и используем.

Шаблон для CloudFormation уже создан, и в примерах ниже будут отсылки к его ресурсам.

Опции EFS

Перед тем, как создавать EFS – немного рассмотрим доступные опции.

EFS имеет два основных доступных параметра – Performance mode (“тип производительности”), и Throughput mode (“пропускной режим”).

Performance mode

Performance mode не влияет на стоимость и не может быть изменён после создания EFS.

В свою очередь Performance mode имеет два типа – General Purpose Performance Mode и Max I/O Performance Mode.

При использовании General Purpose Performance Mode вы получаете стабильные время ответа от файловой системы и скорость передачи данных, но ограничены 7000 операциями в секунду для всех подключенных к EFS клиентов.

Использование типа Max I/O – вы получите больше I/O в секунду и пропускную способность, но время операций чтения/записи может быть незначительно выше.

См. Limits for Amazon EFS File Systems и Performance Modes.

Так же есть достаточно интересный пост тут>>>.

Основной совет при выборе типа производительности – создать EFS с типом General Purpose, нагрузить её до планируемой в Production нагрузки, и проверить значения PercentIOLimit в CloudWatch: если значение достигает 100% – то стоит попробовать Max I/O.

Throughput Modes

Кроме Performance mode, который отвечает за кол-во операций в секунду и время выполнения этих операций – имеет значение и пропускная возможность EFS, которая ограничивает скорость передачи от хостов EFS к клиентам.

Throughput Modes также имеет два типа – Bursting и Provisioned.

В Bursting типе скорость передачи растёт пропорциональность размеру EFS, а в Provisioned задаётся фиксированным значением при создании EFS (и может быть изменена после создания).

При использовании Bursting типа сразу после создания EFS, пока используемый размер менее 10 GB – вы получаете 0.5 мб/с постоянной скорости передачи данных, и возможность временно увеличить скорость до 100 мб/с на 72 минуты в день (!):

File System Size (GiB) Baseline Aggregate Throughput (MiB/s) Burst Aggregate Throughput (MiB/s) Maximum Burst Duration (Min/Day) % of Time File System Can Burst (Per Day)
10 0.5 100 7.2 0.5%
256 12.5 100 180 12.5%
512 25.0 100 360 25.0%
1024 50.0 100 720 50.0%
1536 75.0 150 720 50.0%
2048 100.0 200 720 50.0%
3072 150.0 300 720 50.0%
4096 200.0 400 720 50.0%

Т.е., чем больше будет размер EFS – тем выше будет постоянная скорость доступа и дольше возможность её увеличения.

Для мониторинга доступной скорости используются Bust Credits – BurstCreditBalance в CloudWatch.

При использовании Provisioned Mode – вы можете задать скорость доступа независимо от кол-ва данных в EFS, но при этом будете оплачивать как за GB в EFS, так и за пропускной канал.

См. детали в pricing.

CloudFormation

Т.к. у нас никаких особых требований к EFS нет – то будем использовать дефолтные General Purpose Performance Mode и Bursting Throughput Mode.

Для создания EFS в CloudFormation потребуется три ресурса:

  1. AWS::EC2::SecurityGroup: для ограничения доступа к EFS
  2. AWS::EFS::MountTarget: точка доступа к EFS, со своим IP/URL, документация тут>>>
  3. AWS::EFS::FileSystem: собственно, сам EFS

Приложение состоит из трёх ЕС2 – два для самого приложения, которые обслуживают клиентов и доступны через Application Load Balancer, это App1 и App2, и третий инстанс – Console, на котором работают cron-задачи, RabbitMQ, Redis и т.д.

Каждый EC2 находится в отдельной подсети.

IAM пользователь, от которого создаётся EFS, должен иметь права AmazonElasticFileSystemFullAccess.

AWS::EC2::SecurityGroup

Добавляем Security Group, в которой разрешаем доступ к порту 2049 для всех EC2 из SG AppSecurityGroup.

AppSecurityGroup содержит правила для доступа к EC2, и все создаваемые EC2 будут подключены к ней:

...
"AppSecurityGroup": {
  "Type": "AWS::EC2::SecurityGroup",
  "Properties" : {
    "GroupDescription" : "Backend server access",
    "VpcId"            : { "Ref": "VPC" },
    "SecurityGroupIngress" : [
      {
        "Description": "Allow SSH from Office1",
        "IpProtocol" : "tcp",
        "FromPort"   : 22,
        "ToPort"     : 22,
        "CidrIp"     : { "Ref": "OfficeAllowLocation1" }
      },
...

Добавляем ресурс MountTargetSecurityGroup, в которой используем ID AppSecurityGroup:

...
    "MountTargetSecurityGroup": {
      "Type": "AWS::EC2::SecurityGroup",
      "Properties": {
        "VpcId": { "Ref": "VPC" },
        "GroupDescription": "Security group for mount target",
        "SecurityGroupIngress": [
          {
            "IpProtocol": "tcp",
            "FromPort": "2049",
            "ToPort": "2049",
            "SecurityGroupIngress": { "Ref": "AppSecurityGroup" }
          }
        ]
      }
    },
...

AWS::EFS::MountTarget

Добавляем создание точек монтирования, которые используют добавленную выше Security Group и подсети, которые уже имеются в шаблоне.

Для каждого инстанса создаётся своя подсеть, поэтому создаём три Mount Target-а:

...
    "App1MountTarget": {
      "Type": "AWS::EFS::MountTarget",
      "Properties": {
        "FileSystemId": { "Ref": "ReceiptsFileSystem" },
        "SubnetId": { "Ref": "App1Subnet" },
        "SecurityGroups": [ { "Ref": "MountTargetSecurityGroup" } ]
      }
    },

    "App2MountTarget": {
      "Type": "AWS::EFS::MountTarget",
      "Properties": {
        "FileSystemId": { "Ref": "ReceiptsFileSystem" },
        "SubnetId": { "Ref": "App2Subnet" },
        "SecurityGroups": [ { "Ref": "MountTargetSecurityGroup" } ]
      }
    },

    "ConsoleMountTarget": {
      "Type": "AWS::EFS::MountTarget",
      "Properties": {
        "FileSystemId": { "Ref": "ReceiptsFileSystem" },
        "SubnetId": { "Ref": "ConsoleSubnet" },
        "SecurityGroups": [ { "Ref": "MountTargetSecurityGroup" } ]
      }
    },
...

При монтировании “шары” на каждом EC2 будет использоваться общий URL для EFS, который будет резолвиться в IP Mount Target-а в Availability зоне каждого инстанса.

AWS::EFS::FileSystem

Добавляем создание самой EFS:

...
    "ReceiptsFileSystem": {
      "Type": "AWS::EFS::FileSystem",
      "Properties": {
        "PerformanceMode": "generalPurpose",
        "FileSystemTags": [
          {"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "receipts-efs"] ] } },
          {"Key" : "Env", "Value" : {"Ref" : "ENV"} }
        ]
      }
    },
...

Outputs

Для подключения этой EFS к инстансам – используем DNS-имя, которое создаётся из efs-ID + .efs.us-east-2.amazonaws.com.

Для этого – выведем в Outputs данные, которые потом будем использовать в Ansible:

...
    "ReceiptsFileSystemDNS" : {
      "Description" : "EFS DNS name to mount",
      "Value" : { "Fn::Join": [ ".", [ { "Ref": "ReceiptsFileSystem" }, "efs.us-east-2.amazonaws.com" ]] } 
    },
...

Запускаем создание/апдейт стека:

Ansible

Следующим шагом – созданную EFS надо смонтировать к создаваемым EC2.

Добавим роль efs:

mkdir -p roles/efs/{tasks,vars}

В роли нам потребуется:

  1. создать каталог /storage
  2. установить пакет nfs-common
  3. смонтировать EFS, используя DNS-имя в каталог /storage

Монтирование EFS описано тут>>>.

Создаём файл roles/efs/tasks/main.yml:

- name: "Read ENV specific variables"
  include_vars:
    file: "{{ env }}_vars.yml"

- name: "Install nfs-common package"
  apt: 
    name: "nfs-common"
    state: present

- name: "Create {{ efs_mount_path }} directory"
  file:
    path: "{{ efs_mount_path }}"
    state: directory
    mode: 0755
    recurse: yes

- name: "Mount EFS {{ efs_dns_name }} to the {{ efs_mount_path }}"
  mount:
    path: "{{ efs_mount_path }}"
    src: "{{ efs_dns_name }}"
    state: mounted
    fstype: nfs4
    opts: "nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport"

Добавляем два файла переменных: в roles/efs/vars/main.yml будут общие переменные, такие как имя каталога для точки монтирования, а в roles/efs/vars/{{ env }}_vars.yml – DNS имя EFS.

Для каждого окружения в инвентори-файле задаётся переменная {{ env }}, используя которую можно переопределить файл с параметрами для каждого окружения:

cat hosts.ini | grep -w env
env=test
env=dev
env=production

Файл roles/efs/vars/main.yml:

efs_mount_path: "/storage"

И файл roles/efs/vars/test_vars.yml – тут задаём ReceiptsFileSystemDNS из Outputs стека + :/ в конце:

efs_dns_name: "fs-***.efs.us-east-2.amazonaws.com:/"

Добавляем вызов роли в плейбук ко всем остальным ролям:

...
    - role: common
      tags: app
    - role: efs
      tags: app
    - role: exim
      tags: app
...

Запускаем:

...
PLAY [all] ****
TASK [Gathering Facts] ****
ok: [test.app2.mobilebackend.domain.tld]
ok: [test.console.mobilebackend.domain.tld
ok: [test.app1.mobilebackend.domain.tld]
TASK [efs : Read ENV specific variables] ****
ok: [test.app1.mobilebackend.domain.tld]
ok: [test.app2.mobilebackend.domain.tld]
ok: [test.console.mobilebackend.domain.tld]
TASK [efs : Install nfs-common package] ****
ok: [test.console.mobilebackend.domain.tld]
ok: [test.app2.mobilebackend.domain.tld]
ok: [test.app1.mobilebackend.domain.tld]
TASK [efs : Create /storage directory] ****
ok: [test.app2.mobilebackend.domain.tld]
ok: [test.console.mobilebackend.domain.tld]
ok: [test.app1.mobilebackend.domain.tld]
TASK [efs : Mount EFS fs-***.efs.us-east-2.amazonaws.com:/ to the /storage] ****
changed: [test.app2.mobilebackend.domain.tld]
changed: [test.console.mobilebackend.domain.tld]
changed: [test.app1.mobilebackend.domain.tld]
PLAY RECAP ****
test.app1.mobilebackend.domain.tld : ok=5    changed=1    unreachable=0    failed=0
test.app2.mobilebackend.domain.tld : ok=5    changed=1    unreachable=0    failed=0
test.console.mobilebackend.domain.tld : ok=5    changed=1    unreachable=0    failed=0
Provisioning done.

Проверяем на инстансе App1:

root@ip-10-0-6-12:/home/admin# ls -l /storage/
total 0

Создаём тестовый файл:

root@ip-10-0-6-12:/home/admin# touch /storage/test.txt

Проверяем на инстансе App2:

root@ip-10-0-6-21:/home/admin# ls -l /storage/
total 4
-rw-r--r-- 1 root root 0 Aug  1 12:07 test.txt

Готово.