Продолжаем миграцию. Предыдущие посты серии:
- AWS: миграция RTFM 2.1 – CloudFormation для EC2 c Jenkins: создание CloudFormation шаблона для EC2 с Jenkins
- Ansible: миграция RTFM 2.2 – RTFM Jenkins provision: создание ролей Ansible для установки и запуска Jenkins
- AWS: миграция RTFM 2.3 – инфраструктура для RTFM и создание CloudFormation шаблона – VPC, subnets, EC2: создание CloudFormation шаблона для инстансов самого блога (bastion, services и db), настройка VPC
- Jenkins: миграция RTFM 2.4 – Jenkins Pipeline для CloudFormation RTFM стека: создание Jenkins pipeline для провижена стека блога
- AWS: миграция RTFM 2.5 – настройка NAT на Bastion EC2 как замена NAT Gateway: настройка NAT на Bastion EC2, настройка маршрутизации в подсетях VPC
- Jenkins: миграция RTFM 2.6 – Jenkins Pipeline для Ansible: создание Jenkins pipeline для настройки сервисов на EC2 интансах
Задача на сегодня следующая.
В посте AWS: миграция RTFM 2.5 – настройка NAT на Bastion EC2 как замена NAT Gateway описана ручная настройка NAT на EC2 для замены AWS NAT Gateway.
Теперь — надо перенести эту настройку в CloudFormation шаблон блога и добавить Ansible роль для настройки NAT на самом Bastion EC2.
Далее:
- обновим шаблон CloudFormation для блога — уберём оттуда NAT Gateway, обновим маршруты подсетей, что бы они использовали Bastion EC2 вместо NAT GW
- создадим Ansible роль, которая будет выполнять настройку NAT на Bastion инстансе
UPD 27 Января 2018: вот, чем удобна такая автоматизация и такие посты — я начал писать этот его ещё 3 months ago (3 Nov @ 11:40), сейчас зашёл, копи-паст — и всё заводится за 5 минут:
Хотя всё-таки возвращаться к такому проекту после трёх месяцев перерыва — достаточно сложно, и без предыдущих постов этой серии — я потратил бы минимум день, что бы разобраться — как это всё сетапил (это к слову о важности ведения документации).
Ссылки на коммиты файлов, получившиеся в результате написания этого поста:
rtfm-blog-cf-template.json— шаблон CloudFormation для создания стекаrtfm-blog-ansible-provision.yml — файл провижена для Ansible, сейчас включает две роли — NGINX и NATroles/nat/tasks/main.yml — роль NAT для настройки роутинга на Bastion хостеroles/nat/files/iptables.service —systemd-файл для IPTABLES
Содержание
Подготовка
- запускаем Jenkins – Ansible: миграция RTFM 2.2 – RTFM Jenkins provision:
$ cd /home/setevoy/Work/RTFM/Github/rtfm-jenkins-ansible-provision/// скрипт в репозитории
$ ./rtfm-jenkins-provision.sh -b// бекапим EBS с workspaces Jenkins
$ ./rtfm-jenkins-provision.sh -c -s rtfm-jenkins -i 188.***.***.114// создаём CloudFormation стек с EC2 для Jenkins, подключаем EBS - Обновляем
IN Aдля ci.rtfm.co.ua, указываем адрес нового EC2 с Jenkins $ ./rtfm-jenkins-provision.sh -a
с помощью Ansible — запускаем docker-контейнер с Jenkins, подключаем EBS как volume- cоздаём стек rtfm-dev – Jenkins: миграция RTFM 2.4 – Jenkins Pipeline для CloudFormation RTFM стека
создаётся из Jenkins задачи - находим Public IP в
outputs, обновляем IN A субдомен dev.rtfm.co.ua
DNS пока у регистратора — Freehost.ua (не реклама, но вполне адекватный хостер и регистратор доменов, когда-то там работал)
CloudFormation
Для начала надо обновить шаблон CloudFormation для инфрастуктуры блога — убрать ресурсы NAT Gateway, лишний EIP, обновить RouteTables, которые тут есть с предыдущего сетапа, который включал в себя NAT Gateway (коммит с удалением)
В данный момент шаблон выглядит так — последний коммит с NAT Gateway.
AWS::EC2::EIP
Сейчас есть два ресурса Elastic IP — для Bastion EC2 и для NAT Gateway.
Оставляем один, удаляем ресурс NatGwIPAddress:
...
"NatGwIPAddress" : {
"Type" : "AWS::EC2::EIP",
"Properties" : {
"Domain" : "vpc"
}
},
...
AWS::EC2::NatGateway
Теперь — удаляем ресурс AWS::EC2::NatGateway:
...
"NAT" : {
"DependsOn" : "NatGwIPAddress",
"Type" : "AWS::EC2::NatGateway",
"Properties" : {
"AllocationId" : { "Fn::GetAtt" : ["NatGwIPAddress", "AllocationId"]},
"SubnetId" : { "Ref" : "PublicSubnet"},
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "natgw"] ] } },
{"Key" : "Env", "Value" : {"Ref" : "ENV"} }
]
}
},
...
AWS::EC2::Route
Следующим шагом — обновляем маршрут для приватной сети — ресурс PrivateRoute.
В Properties ресурса меняем параметр InstanceId — указание интанса, который будет работать как NAT: вместо NAT GW указываем тут ссылку на Bastion EC2.
Кроме того — добавляем явное указание DependsOn с указанием зависимости от Bastion EC2.
Было:
...
"PrivateRoute": {
"Type": "AWS::EC2::Route",
"Properties": {
"RouteTableId": {
"Ref": "PrivateRouteTable"
},
"DestinationCidrBlock": "0.0.0.0/0",
"NatGatewayId": {
"Ref": "NAT"
}
}
},
...
Стало:
...
"PrivateRoute": {
"Type": "AWS::EC2::Route",
"DependsOn" : "BastionEC2Instance",
"Properties": {
"RouteTableId": {
"Ref": "PrivateRouteTable"
},
"DestinationCidrBlock": "0.0.0.0/0",
"InstanceId": {
"Ref": "BastionEC2Instance"
}
}
...
AWS::EC2::Instance
Так же требуется отключить проверку Source/Destination на сетевом интерфейсе Bastion EC2, что бы он мог работать в качестве NAT-шлюза (ручная настройка описана тут>>>).
Обновляем ресурс BastionEC2Instance, добавляем параметр SourceDestCheck — "SourceDestCheck": "false":
...
"BastionEC2Instance" : {
"Type" : "AWS::EC2::Instance",
"Properties" : {
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "bastion"] ] } },
{"Key" : "Env", "Value" : {"Ref" : "ENV"} }
],
"InstanceType" : { "Ref" : "BastionInstanceType" },
"SecurityGroupIds" : [
{
"Ref": "BastionSecurityGroup"
}
],
"KeyName" : { "Ref" : "KeyName" },
"ImageId" : { "Ref" : "AMIID" },
"AvailabilityZone": {"Ref" : "AvalabilityZone"},
"SubnetId": { "Ref" : "PublicSubnet" },
"Volumes" : [
{
"VolumeId" : { "Ref" : "BastionEBSID" },
"Device" : "/dev/xvdb"
}
],
"SourceDestCheck": "false"
}
},
...
AWS::EC2::SecurityGroup
Что бы Bastion хост мог обслуживать трафик с Services и DB интансов — обновляем ресурс группы безопасности BastionSecurityGroup, и разрешаем весь трафик из Security Group для Services и DB:
...
"BastionSecurityGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
"Properties" : {
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "bastion-sg"] ] } },
{"Key" : "Env", "Value" : {"Ref" : "ENV"} }
],
"VpcId" : {"Ref" : "VPC"},
"GroupDescription" : "Enable SSH, HTTP/S, ICMP access from home",
"SecurityGroupIngress" : [
{
"IpProtocol" : "tcp",
"FromPort" : "22",
"ToPort" : "22",
"CidrIp" : { "Ref" : "HomeAllowLocation"}
},
{
"IpProtocol" : "tcp",
"FromPort" : "80",
"ToPort" : "80",
"CidrIp" : "0.0.0.0/0"
},
{
"IpProtocol" : "tcp",
"FromPort" : "443",
"ToPort" : "443",
"CidrIp" : "0.0.0.0/0"
},
{
"IpProtocol" : "icmp",
"FromPort" : "8",
"ToPort" : "-1",
"CidrIp" : { "Ref" : "HomeAllowLocation"}
},
{
"IpProtocol" : "tcp",
"FromPort" : "*",
"ToPort" : "(",
"SourceSecurityGroupId" : { "Ref" : "ServicesSecurityGroup"}
},
{
"IpProtocol" : "tcp",
"FromPort" : "*",
"ToPort" : "(",
"SourceSecurityGroupId" : { "Ref" : "DBSecurityGroup"}
}
]
}
},
...
Сохраняем, пушим — можно создавать стек.
Ссылка на текущий коммит шаблона — тут>>>.
Проверяем маршруты приватной таблицы:
Проверяем.
Интернет на Bastion:
[simterm]
$ ssh [email protected] -i ../../Bitbucket/aws-credentials/rtfm-dev.pem ... admin@ip-10-0-1-241:~$ ping ya.ru -c 1 PING ya.ru (87.250.250.242) 56(84) bytes of data. 64 bytes from ya.ru (87.250.250.242): icmp_seq=1 ttl=39 time=61.0 ms --- ya.ru ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 61.032/61.032/61.032/0.000 ms
[/simterm]
Копируем ключ, подключаемся на интанс в приватной сети, например — на DB:
[simterm]
admin@ip-10-0-1-241:~$ ssh [email protected] -i .ssh/rtfm-dev.pem ... admin@ip-10-0-129-5:~$ ping ya.ru -c 1 PING ya.ru (87.250.250.242) 56(84) bytes of data. --- ya.ru ping statistics --- 1 packets transmitted, 0 received, 100% packet loss, time 0ms
[/simterm]
ОК — интернет в приватной сети не работает, всё хорошо.
Ошибка Circular dependency
Однако тут возникает проблема: сейчас мы можем получить доступ к ServicesEC2Instance и DBEC2Instance с BastionEC2Instance, но вот с этих интансов к Bastion хосту — подключения нет, и NAT для них работать не будет, т.к. ресурсы AWS::EC2::SecurityGroup для DB и Services имеют правила, разрешающие подключение, например ServicesSecurityGroup:
...
"ServicesSecurityGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
"Properties" : {
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "services-sg"] ] } },
{"Key" : "Env", "Value" : {"Ref" : "ENV"} }
],
"VpcId" : {"Ref" : "VPC"},
"GroupDescription" : "Enable SSH access from Bastion",
"SecurityGroupIngress" : [
{
"IpProtocol" : "tcp",
"FromPort" : "22",
"ToPort" : "22",
"SourceSecurityGroupId" : { "Ref" : "BastionSecurityGroup" }
}
]
}
},
...
А группа для Bastion — имеет правила только для SSH и ICMP из HomeAllowLocation, и для портов 80/443 отовсюду:
...
"BastionSecurityGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
"Properties" : {
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "bastion-sg"] ] } },
{"Key" : "Env", "Value" : {"Ref" : "ENV"} }
],
"VpcId" : {"Ref" : "VPC"},
"GroupDescription" : "Enable SSH, HTTP/S, ICMP access from home",
"SecurityGroupIngress" : [
{
"IpProtocol" : "tcp",
"FromPort" : "22",
"ToPort" : "22",
"CidrIp" : { "Ref" : "HomeAllowLocation"}
},
{
"IpProtocol" : "tcp",
"FromPort" : "80",
"ToPort" : "80",
"CidrIp" : "0.0.0.0/0"
},
{
"IpProtocol" : "tcp",
"FromPort" : "443",
"ToPort" : "443",
"CidrIp" : "0.0.0.0/0"
},
{
"IpProtocol" : "icmp",
"FromPort" : "8",
"ToPort" : "-1",
"CidrIp" : { "Ref" : "HomeAllowLocation"}
}
]
}
},
...
Ведь раньше DB и Services «бегали» через NAT GW, и доступа к Bastion не требовалось (PHP-FPM на Services ещё не добавлен, роли для него будут в следующих постах).
Самым простым решением тут казалось бы добавить ещё два правила, в которые можно было включить имена групп для DB и Services (или их приватные IP), вроде такого:
...
{
"IpProtocol" : "tcp",
"FromPort" : "*",
"ToPort" : "(",
"SourceSecurityGroupId" : { "Ref" : "ServicesSecurityGroup"}
},
{
"IpProtocol" : "tcp",
"FromPort" : "*",
"ToPort" : "(",
"SourceSecurityGroupId" : { "Ref" : "DBSecurityGroup"}
}
...
Но в таком случае — мы получим ошибку Circular dependency error — группа BastionSecurityGroup ссылается на ресурсы ServicesSecurityGroup и DBSecurityGroup, которые, в свою очередь, ссылаются на группу BastionSecurityGroup.
Посмотрим ещё раз, уберу лишнее.
Группа ServicesSecurityGroup:
...
"ServicesSecurityGroup" : {
...
"GroupDescription" : "Enable SSH access from Bastion",
"SecurityGroupIngress" : [
{
"IpProtocol" : "tcp",
"FromPort" : "22",
"ToPort" : "22",
"SourceSecurityGroupId" : { "Ref" : "BastionSecurityGroup" }
}
...
Аналогично — группа DBSecurityGroup:
...
"DBSecurityGroup" : {
...
"GroupDescription" : "Enable SSH access from Bastion",
"SecurityGroupIngress" : [
{
"IpProtocol" : "tcp",
"FromPort" : "22",
"ToPort" : "22",
"SourceSecurityGroupId" : { "Ref" : "BastionSecurityGroup" }
},
...
И ещё раз группа BastionSecurityGroup:
...
"BastionSecurityGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
...
{
"IpProtocol" : "tcp",
"FromPort" : "*",
"ToPort" : "(",
"SourceSecurityGroupId" : { "Ref" : "ServicesSecurityGroup"}
},
{
"IpProtocol" : "tcp",
"FromPort" : "*",
"ToPort" : "(",
"SourceSecurityGroupId" : { "Ref" : "DBSecurityGroup"}
}
...
Проверяем:
[simterm]
$ aws cloudformation --region eu-west-1 validate-template --template-body file://rtfm-blog-cf-template.json An error occurred (ValidationError) when calling the ValidateTemplate operation: Circular dependency between resources: [DBAllowInboundRule, ServicesAllowInboundRule, ServicesSecurityGroup, DBSecurityGroup, DBEC2Instance, BastionEIPAssociation, ServicesEC2Instance, BastionEC2Instance, BastionSecurityGroup, PrivateRoute]
[/simterm]
Что бы избежать этой ошибки — вынесем правила для BastionSecurityGroup, разрешающие подключения с ServicesSecurityGroup и DBSecurityGroup в отдельные ресурсы — AWS::EC2::SecurityGroupIngress, добавим правило для доступа из групп DB и Services.
Используем IpProtocol -1, что бы использовать и TCP, и UDP, порты не указываем, что бы разрешить всё:
...
"DBAllowInboundRule": {
"Type": "AWS::EC2::SecurityGroupIngress",
"Properties":{
"IpProtocol": "-1",
"SourceSecurityGroupId": {
"Fn::GetAtt": [
"DBSecurityGroup",
"GroupId"
]
},
"GroupId": {
"Fn::GetAtt": [
"BastionSecurityGroup",
"GroupId"
]
}
}
},
...
И второй ресурс — разрешение для доступа из ServicesSecurityGroup:
...
"ServicesAllowInboundRule": {
"Type": "AWS::EC2::SecurityGroupIngress",
"Properties":{
"IpProtocol": "-1",
"SourceSecurityGroupId": {
"Fn::GetAtt": [
"ServicesSecurityGroup",
"GroupId"
]
},
"GroupId": {
"Fn::GetAtt": [
"BastionSecurityGroup",
"GroupId"
]
}
}
},
...
См. VPC Security Groups with Egress and Ingress Rules тут>>>.
С этим всё готово — можно настраивать NAT на Bastion EC2.
Ansible
Сначала выполним провижен NGINX — пока это единственная роль для Bastion, файл rtfm-blog-ansible-provision.yml:
- hosts: all
become:
true
roles:
- nginx
Сейчас имеется единственный хост в инвентори — Bastion, файл hosts:
[rtfm-dev] dev.rtfm.co.ua
Позже надо будет добавить хосты для DB и Services.
Запускаем провижен из Jenkins:
NAT role
Переходим в каталог репозитория:
[simterm]
$ cd /home/setevoy/Work/RTFM/Github/rtfm-blog-ansible-provision/
[/simterm]
Создаём роль NAT:
[simterm]
$ mkdir -p roles/nat/{tasks,files}
[/simterm]
sysctl.conf
Что бы включить NAT в ядре — необходимо обновить файл sysctl.conf.
Используем модуль lineinfile.
Создаём файл roles/nat/tasks/main.yml:
- name: Update sysctl.conf
lineinfile:
path: /etc/sysctl.conf
regexp: '^#net.ipv4.ip_forward=1$'
line: 'net.ipv4.ip_forward=1'
- name: Reload sysctl
command: sysctl -p
Тут с помощью lineinfile — обновляем значение для net.ipv4.ip_forward=1 (убираем # в начале строки), а затем с помощью модуля command — выполняем sysctl -p, что бы сразу применить изменения в ядре, без перезагрузки операционной системы EC2 инстанса.
Проверяем синтаксис:
[simterm]
$ ansible-playbook --syntax-check --limit=rtfm-dev rtfm-blog-ansible-provision.yml playbook: rtfm-blog-ansible-provision.yml
[/simterm]
Проверяем выполнение — пока можно вручную, без Jenkins:
[simterm]
$ ansible-playbook --limit=rtfm-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-provision.yml PLAY [all] **** TASK [Gathering Facts] **** ok: [dev.rtfm.co.ua] TASK [nginx : Install Nginx] **** ok: [dev.rtfm.co.ua] TASK [nginx : Replace NGINX config] **** ok: [dev.rtfm.co.ua] TASK [nginx : Add NGINX dev.rtfm.co.ua virtualhost config] **** ok: [dev.rtfm.co.ua] TASK [nginx : Service NGINX reload] **** changed: [dev.rtfm.co.ua] TASK [nat : Update sysctl.conf] **** changed: [dev.rtfm.co.ua] TASK [nat : Reload sysctl] **** changed: [dev.rtfm.co.ua] PLAY RECAP **** dev.rtfm.co.ua : ok=7 changed=3 unreachable=0 failed=0
[/simterm]
Проверяем на хосте:
[simterm]
$ ansible -i hosts rtfm-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem -a "/sbin/sysctl net.ipv4.ip_forward" dev.rtfm.co.ua | SUCCESS | rc=0 >> net.ipv4.ip_forward = 1
[/simterm]
IPTABLES
Далее — добавляем настройку IPTABLES.
Используем модуль iptables, в файл roles/nat/tasks/main.yml добавляем правило:
...
- name: Iptables POSTROUTING append
iptables:
table: nat
chain: POSTROUTING
source: 10.0.0.0/16
destination: 0.0.0.0/0
jump: MASQUERADE
Запускаем:
[simterm]
$ ansible-playbook --limit=rtfm-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-provision.yml PLAY [all] **** TASK [Gathering Facts] **** ok: [dev.rtfm.co.ua] TASK [nginx : Install Nginx] **** ok: [dev.rtfm.co.ua] TASK [nginx : Replace NGINX config] **** ok: [dev.rtfm.co.ua] TASK [nginx : Add NGINX dev.rtfm.co.ua virtualhost config] **** ok: [dev.rtfm.co.ua] TASK [nginx : Service NGINX reload] **** changed: [dev.rtfm.co.ua] TASK [nat : Update sysctl.conf] **** changed: [dev.rtfm.co.ua] TASK [nat : Reload sysctl] **** changed: [dev.rtfm.co.ua] TASK [nat : Iptables POSTROUTING append] **** changed: [dev.rtfm.co.ua] PLAY RECAP **** dev.rtfm.co.ua : ok=8 changed=4 unreachable=0 failed=0
[/simterm]
Проверяем:
[simterm]
$ ansible -i hosts rtfm-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem -a "sudo /sbin/iptables -t nat -v -L -n --line-number" [WARNING]: Consider using 'become', 'become_method', and 'become_user' rather than running sudo dev.rtfm.co.ua | SUCCESS | rc=0 >> Chain PREROUTING (policy ACCEPT 4 packets, 268 bytes) num pkts bytes target prot opt in out source destination Chain INPUT (policy ACCEPT 4 packets, 268 bytes) num pkts bytes target prot opt in out source destination Chain OUTPUT (policy ACCEPT 2 packets, 460 bytes) num pkts bytes target prot opt in out source destination Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes) num pkts bytes target prot opt in out source destination 1 2 460 MASQUERADE all -- * * 10.0.0.0/16 0.0.0.0/0
[/simterm]
ОК — правило есть, проверяем доступ в сеть с DB — с Bastion хоста подключаемся на DB:
[simterm]
$ admin@ip-10-0-1-109:~$ ssh [email protected] -i .ssh/rtfm-dev.pem
[/simterm]
Выполняем пинг:
[simterm]
admin@ip-10-0-129-110:~$ ping -c 1 ya.ru PING ya.ru (87.250.250.242) 56(84) bytes of data. 64 bytes from ya.ru (87.250.250.242): icmp_seq=1 ttl=40 time=47.0 ms --- ya.ru ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 47.056/47.056/47.056/0.000 ms
[/simterm]
Аналогично проверяем сеть на Services:
[simterm]
admin@ip-10-0-129-151:~$ ping ya.ru -c 1 PING ya.ru (87.250.250.242) 56(84) bytes of data. 64 bytes from ya.ru (87.250.250.242): icmp_seq=1 ttl=40 time=48.3 ms --- ya.ru ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 48.330/48.330/48.330/0.000 ms
[/simterm]
NAT on startup
Последний шаг — восстанавливать правила IPTABLES при рестарте сервера.
В каталоге roles/nat/files создаём файл сервиса iptables.service:
[Unit] Description=Packet Filtering Framework Before=network-pre.target Wants=network-pre.target [Service] Type=oneshot ExecStart=/sbin/iptables-restore /etc/iptables/iptables.rules ExecReload=/sbin/iptables-restore /etc/iptables/iptables.rules RemainAfterExit=yes [Install] WantedBy=multi-user.target
В roles/nat/tasks/main.yml добавляем создание каталога, сохранение правил IPTABLES, создание каталога для systemd-юнита IPTABLES, копирование файла и запуск IPTABLES с правилами из /etc/iptables/iptables.rules:
...
- name: Create IPTABLES config dir
file: path=/etc/iptables state=directory
- name: Save IPTABLES rules
shell: iptables-save > /etc/iptables/iptables.rules
- name: Create systemd-service iptables dir
file: path=/usr/lib/systemd/system state=directory
- name: Copy IPTABLES systemd-file
copy:
src: files/iptables.service
dest: /usr/lib/systemd/system/iptables.service
- name: IPTABLES enable
command: systemctl enable iptables.service
Готово:
Что далее?
- написать роли для Bastion — настройка NGINX, файлы конфигурации виртуалхостов, SSL, установка NGINX Amplify агента и т.д.
- роли для Services — создание каталога для постоянных данных и монтирование EBS, установка PHP-FPM, OpenVPN и т.д.
- роли для DB — создание каталога для постоянных данных и монтирование EBS, установка MariaDB
P.S. Как всё просто выглядит, когда оно уже готово…




