Задача — добавить ресурсы 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 потребуется три ресурса:
AWS::EC2::SecurityGroup: для ограничения доступа к EFSAWS::EFS::MountTarget: точка доступа к EFS, со своим IP/URL, документация тут>>>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:
[simterm]
$ mkdir -p roles/efs/{tasks,vars}
[/simterm]
В роли нам потребуется:
- создать каталог
/storage - установить пакет
nfs-common - смонтировать 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 }}, используя которую можно переопределить файл с параметрами для каждого окружения:
[simterm]
$ cat hosts.ini | grep -w env env=test env=dev env=production
[/simterm]
Файл 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
...
Запускаем:
[simterm]
... 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.
[/simterm]
Проверяем на инстансе App1:
[simterm]
root@ip-10-0-6-12:/home/admin# ls -l /storage/ total 0
[/simterm]
Создаём тестовый файл:
[simterm]
root@ip-10-0-6-12:/home/admin# touch /storage/test.txt
[/simterm]
Проверяем на инстансе App2:
[simterm]
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
[/simterm]
Готово.
