Продолжаем миграцию. Предыдущие посты серии:
- 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 и NAT
roles/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. Как всё просто выглядит, когда оно уже готово…