AWS: миграция RTFM 2.7 – CloudFormation и Ansible – наcтройка NAT

Автор: | 27/01/2018
 

Продолжаем миграцию. Предыдущие посты серии:

  1. AWS: миграция RTFM 2.1 – CloudFormation для EC2 c Jenkins: создание CloudFormation шаблона для EC2 с Jenkins
  2. Ansible: миграция RTFM 2.2 – RTFM Jenkins provision: создание ролей Ansible для установки и запуска Jenkins
  3. AWS: миграция RTFM 2.3 – инфраструктура для RTFM и создание CloudFormation шаблона – VPC, subnets, EC2: создание CloudFormation шаблона для инстансов самого блога (bastion, services и db), настройка VPC
  4. Jenkins: миграция RTFM 2.4 – Jenkins Pipeline для CloudFormation RTFM стека: создание Jenkins pipeline для провижена стека блога
  5. AWS: миграция RTFM 2.5 – настройка NAT на Bastion EC2 как замена NAT Gateway: настройка NAT на Bastion EC2, настройка маршрутизации в подсетях VPC
  6. 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.

Далее:

  1. обновим шаблон CloudFormation для блога – уберём оттуда NAT Gateway, обновим маршруты подсетей, что бы они использовали Bastion EC2 вместо NAT GW
  2. создадим Ansible роль, которая будет выполнять настройку NAT на Bastion инстансе

UPD 27 Января 2018: вот, чем удобна такая автоматизация и такие посты – я начал писать этот его ещё 3 months ago (3 Nov @ 11:40), сейчас зашёл, копи-паст – и всё заводится за 5 минут:

Хотя всё-таки возвращаться к такому проекту после трёх месяцев перерыва – достаточно сложно, и без предыдущих  постов этой серии – я потратил бы минимум день, что бы разобраться – как это всё сетапил (это к слову о важности ведения документации).

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

Подготовка

  1. запускаем 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
  2. Обновляем IN A  для ci.rtfm.co.ua, указываем адрес нового EC2 с Jenkins
  3. $ ./rtfm-jenkins-provision.sh -a
    с помощью Ansible – запускаем docker-контейнер с Jenkins, подключаем EBS как volume
  4. cоздаём стек rtfm-dev – Jenkins: миграция RTFM 2.4 – Jenkins Pipeline для CloudFormation RTFM стека
    создаётся из Jenkins задачи
  5. находим 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"}
          }

        ]
      }
    },
...

Сохраняем, пушим – можно создавать стек.

Ссылка на текущий коммит шаблона – тут>>>.

Создаём стек из Jenkins:

Проверяем маршруты приватной таблицы:

Проверяем.

Интернет на 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

Готово:

Что далее?

  1. написать роли для Bastion – настройка NGINX, файлы конфигурации виртуалхостов, SSL, установка NGINX Amplify агента и т.д.
  2. роли для Services – создание каталога для постоянных данных и монтирование EBS, установка PHP-FPM, OpenVPN и т.д.
  3. роли для DB – создание каталога для постоянных данных и монтирование EBS, установка MariaDB

P.S. Как всё просто выглядит, когда оно уже готово…