Прошёл почти год, как я начал миграцию v2, закончил на посте Ansible: миграция RTFM 2.11 – хост Services – установка PHP, PHP-FPM.
Основной её идеей на тот момент было максимально использовать возможности AWS и Ansible — чисто из интереса и для практики, т.к. на предыдущем месте работы её было совсем мало (там в основном был Azure с его Resource Manager и скрипты на bash).
Три месяца тому я сменил работу, и теперь у меня валом и Jenkins, и AWS, и CloudFormation, и Ansible. А потому — решил максимально упростить инфрастуктуру для RTFM, и быстренько сделать новый вариант миграции (и наконец-то её таки закончить и перенести блог, а то в самом деле — «Сапожник без сапог»).
В отличии от старого варианта, где планировалось 3 сервера (один под Jenkins, второй под Web в публичной сети, один под PHP-FPM+OpenVPN+почта+прочее в приватной сети, третий — чисто под MariaDB, тоже в приватной сети), в новом варианте CI не будет вообще — достаточно bash-скрипта, а все сервисы будут работать на одном сервере в публичной сети.
Помимо просто упрощения создания такой инфрастуктуры — это поможет сэкономить, т.к. даже сейчас общий счёт в месяц на Амазоне за ЕС2 t2.micro под блог + отдельный t2.nano для MariaDB и t2.nano для OpenVPN, плюс трафик и диски тянет на 60+ баксов, например — счёт за Июль 2018 выглядит так:
Собственно, дальнейший план таков:
- CloudFormation в роли orchestration manager для инфрастуктуры
- Ansible — в роли configuration manager, он же будет запускать CloudFormation
- мониторинг — CloudWatch и NGINX Amplify
В CloudFormation стеке будут:
- VPC
- 1 публичная подсеть
- Internet Gateway
- Elastic IP
- один EC2, к которому будут монтироваться два созданных вручную EBS — один под
/backups, второй под/data— в/dataбудут находиться базы MariaDB и файлы сайтов,/backups— для файлов бекапов, созданных утилиткойsimple-backup - Security Group
Кроме этого, в AWS будут использоваться S3 корзины для бекапов, DLM для автоматизации создания снапшотов EBS-разделов (ещё одни бекапы, их много не бывает), CloudWatch для алертов.
Позже, возможно, добавлю CloudWatch агент на сервер для сбора логов и алерты на кол-во ошибок типа 401, 404 и 5хх.
Ansible будет выполнять установку и настройку:
- создание самого CloudFormation стека
- NGINX + файлы конфигурации виртуалхостов
- PHP-FPM + файлы конфигурации FPM-пулов
- MariaDB
- Let’s Encrypt
- NGINX Amplify агента
- logrotate
- unattended-upgrades
- (позже) OpenVPN
- (позже) memcached
- (позже) Dovecot+Postfix+Postfixadmin
Для хранения шаблонов, ролей и прочих файлов — будут использоваться два репозитория: публичный в Github для Ansible/CloudFormation, и приватный в Bitbucket — для PEM-ключей, файлов конфигурации виртуалхостов NGINX и конфигов пулов PHP-FPM.
Создание всего этого безобразия начнём с написания (вообще-то — просто копирования примера) шаблона для CloudFormation стека. Потом добавим Ansible роль с модулем cloudformation для создания стека, потом — bash-скрипт для запуска Ansible, и потом уже остальные роли для настройки сервисов.
Получившиеся в результате написания этого поста шаблон CloudFormation и Ansible роли доступны в Github (будут обновляться, поэтому примеры из поста могут не совпадать с реальными данными в репозитории).
Содержание
CloudFormation
Шаблон стандартный, давно мною написанный, ничего в нём особого нет, кроме, разве что, подключения EBS по ID к EC2 и подключения IAM-роли для CloudWatch.
Создаём диск для /data:
Аналогично для /backups.
В Parameters шаблона добавляем параметры для них:
...
"DataEBSID": {
"Description": "Data disk EBS ID",
"Type": "String",
"Default": "vol-09ad53faabe393c98"
},
"BackupsEBSID": {
"Description": "Data disk EBS ID",
"Type": "String",
"Default": "vol-0c5f083506a98b985"
}
...
Добавляем Mappings.
Тут будут задаваться CIDR VPC и подсетей по имени стека — rtfm-dev и rtfm-production (хотя можно было вынести в параметры, и потом передавать их из переменных Ansible, что бы не усложнять):
...
"Mappings": {
"VPCCIDRs" : {
"rtfm-dev" : {
"VPCCIDRBlock" : "10.0.2.0/24"
},
"rtfm-production" : {
"VPCCIDRBlock" : "10.0.1.0/24"
}
},
"SubnetsCIDRs" : {
"rtfm-dev" : {
"EC2SubnetCIDR" : "10.0.2.0/28"
},
"rtfm-production" : {
"EC2SubnetCIDR" : "10.0.1.0/28"
}
}
},
...
Добавляем ресурсы.
VPC и подсеть, через Fn:FindInMap получаем значение из Mappings:
...
"Resources" : {
"VPC" : {
"Type" : "AWS::EC2::VPC",
"Properties" : {
"CidrBlock" : { "Fn::FindInMap" : [ "VPCCIDRs", {"Ref" : "ENV"}, "VPCCIDRBlock" ] },
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "vpc"] ] } },
{"Key" : "Env", "Value" : {"Ref" : "ENV"} }
]
}
},
"PublicSubnet" : {
"Type" : "AWS::EC2::Subnet",
"Properties" : {
"VpcId" : { "Ref" : "VPC" },
"CidrBlock" : { "Fn::FindInMap" : [ "SubnetsCIDRs", {"Ref" : "ENV"}, "EC2SubnetCIDR" ] },
"AvailabilityZone" : {"Ref" : "AvailabilityZone"},
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "public-net"] ] } },
{"Key" : "Env", "Value" : {"Ref" : "ENV"} }
]
}
},
...
И EC2 с двумя EBS:
...
"EC2Instance" : {
"Type" : "AWS::EC2::Instance",
"Properties" : {
"Tags" : [
{"Key" : "Name", "Value" : { "Fn::Join" : [ "-", [ {"Ref" : "AWS::StackName"}, "ec2"] ] } },
{"Key" : "Env", "Value" : {"Ref" : "ENV"} }
],
"InstanceType" : { "Ref" : "EC2InstanceType" },
"SecurityGroupIds" : [
{
"Ref": "EC2SecurityGroup"
}
],
"KeyName" : { "Ref" : "KeyName" },
"IamInstanceProfile" : {
"Ref" : "CloudWatchAccessProfile"
},
"ImageId" : { "Ref" : "AMIID" },
"AvailabilityZone": {"Ref" : "AvailabilityZone"},
"SubnetId": { "Ref": "PublicSubnet" },
"Volumes" : [
{
"VolumeId" : { "Ref" : "DataEBSID" },
"Device" : "/dev/xvdb"
},
{
"VolumeId" : { "Ref" : "BackupsEBSID" },
"Device" : "/dev/xvdc"
}
]
}
}
...
Отдельно описываем создание и подключение IAM роли для CloudWatch-агента:
...
"CloudWatchAccessProfile" : {
"Type" : "AWS::IAM::InstanceProfile",
"Properties" : {
"Path" : "/",
"Roles" : [
{ "Ref": "CloudWatchAgentAccessRole" }
]
}
},
"CloudWatchAgentAccessRole" : {
"Type" : "AWS::IAM::Role",
"Properties" : {
"RoleName": { "Fn::Join" : [ "-", [ "CloudWatchAgentAccessRole", {"Ref" : "AWS::StackName"} ] ] },
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement" : [
{
"Effect" : "Allow",
"Principal" : {
"Service" : ["ec2.amazonaws.com"]
},
"Action" : [
"sts:AssumeRole"
]
}
]
},
"ManagedPolicyArns": [
"arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy"
],
"Path" : "/"
}
},
...
Подключается к EC2 в его Properties:
...
"IamInstanceProfile" : {
"Ref" : "CloudWatchAccessProfile"
},
...
Вроде всё ОК, можно проверять:
[simterm]
$ aws --profile rtfm cloudformation validate-template --template-body file://rtfm-cf-stack.json
[/simterm]
И создаём Dev стек:
[simterm]
$ aws --profile rtfm cloudformation create-stack --stack-name rtfm-dev --template-body file://rtfm-cf-stack.json --disable-rollback --capabilities CAPABILITY_NAMED_IAM
[/simterm]
Готово:
Шаблон в репозитории — тут>>>.
Ansible
Роль cloudformation
Работу с Ansible начнём с роли для CloudFormation, используя модуль cloudformation.
Создаём hosts.ini — inventory-файл:
[rtfm-dev] dev.rtfm.co.ua [rtfm-dev:vars] env=dev aws_env=rtfm-dev set_hostname=rtfm-dev
Создаём каталоги для роли cloudformation:
[simterm]
$ mkdir -p roles/cloudformation/{vars,files,tasks}
[/simterm]
Перемещаем файл шаблона в roles/cloudforamtion/files:
[simterm]
$ mv rtfm-cf-stack.json roles/cloudformation/files/
[/simterm]
Переменные и шифрование значений
Создаём файл roles/cloudformation/vars/main.yml — тут перечислим переменные, общие для всех окружений:
region: "eu-west-1" availability_zone: "eu-west-1a" ami_id: "ami-05cdaf7e7b6c76277"
Глобальные переменные
Кроме переменных в файле roles/cloudformation/vars/main.yml — требуется добавить ещё три переменных с блоками IP, с которых будет разрешён доступ по SSH в SecurityGroup, и которые будут использоваться в конфигах NGINX для ограничения доступа к некоторым хостам.
Зададим их в глоабльном файле параметров, создаём каталоги:
[simterm]
$ mkdir -p group_vars/all
[/simterm]
В этом каталоге поместим файл group_vars/all/main.yml. Т.к. этот файл будет лежать в публичном репозитории Github, а светить IP не очень хочется — то их зашифруем с помощью ansible-vault (хотя можно просто вынести в файлы переменных в приватном репозитории, о них дальше).
Создаём файл с паролем:
[simterm]
$ pwgen 12 -n 1 > ~/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm_ansible_vault_pass
[/simterm]
Шифруем значение для переменной office_allow_location:
[simterm]
$ ansible-vault --vault-password-file ~/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm_ansible_vault_pass encrypt_string
Reading plaintext input from stdin. (ctrl-d to end input)
194.***.***.24/29!vault |
$ANSIBLE_VAULT;1.1;AES256
35353463646362653633613238323634326135653265613934303532333939663437343164363931
6633613936633233336463353235613566623933333865300a363635383433623334373736303938
63363138663865393637316638623037666635333361626465653136623037343139623437633332
6331323632646238350a623261633464343161353130383134363764313233613363303634626137
38393461353237396265353539366666633333306161363233383032386362343532
Encryption successful
[/simterm]
Вписываем значение в group_vars/all/main.yml, повторяем для home_allow_location_provider{1,2}:
office_allow_location: !vault |
$ANSIBLE_VAULT;1.1;AES256
35353463646362653633613238323634326135653265613934303532333939663437343164363931
...
38393461353237396265353539366666633333306161363233383032386362343532
home_allow_location_provider1: !vault |
$ANSIBLE_VAULT;1.1;AES256
32613064363936363339313331336362643234636235386430313564393537646131653835313335
...
39303736643434396261383838643364663963663864386338303663636462333936
home_allow_location_provider2: !vault |
$ANSIBLE_VAULT;1.1;AES256
62636466336334626632383662303866316166633232336465366137396134323835646231376133
...
65373866303862336534386531363439353563343531653362323662303061373830
Туда же добавляем переменную, в котором будет локальный путь к приватному репозиторию:
... local_private_repo_location: "/home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure"
Dev/Production переменные
Возвращемся к роли cloudformation.
Создаём второй файл переменных — roles/cloudformation/vars/dev_vars.yml, сюда занесём переменные, специфичные для Dev окружения (для Production — создадим файл roles/cloudformation/vars/production_vars.yml):
termination_protection: no key_name: "rtfm-dev-eu-west-1a" ec2_instance_type: "t2.nano" data_ebs_id: "vol-09ad53faabe393c98" backups_ebs_id: "vol-0c5f083506a98b985"
Убираем все добавленные во время написания и тестирования значения из полей Defaults в шаблоне CloudFormation, т.е. вместо, например:
...
"BackupsEBSID": {
"Description": "Backups disk EBS ID",
"Type": "String",
"Default": "vol-0c5f083506a98b985"
}
...
Оставляем:
...
"BackupsEBSID": {
"Description": "Backups disk EBS ID",
"Type": "String"
}
...
В приватном репозитории Bitbucket создаём файл aws-credenatials.yml, в котором создаём и шифруем переменные для модуля cloudformation:
[simterm]
$ ansible-vault --vault-password-file ~/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm_ansible_vault_pass encrypt_string
Reading plaintext input from stdin. (ctrl-d to end input)
AKI***FFQ!vault |
$ANSIBLE_VAULT;1.1;AES256
39323435303837636563393438633036623533626331386661666634326266326239623762373037
...
38303433323531356333663663663565316139383361623035303732323638663632
[/simterm]
Добавляем значение в /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/aws-credenatials.yml:
aws_access_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
39323435303837636563393438633036623533626331386661666634326266326239623762373037
...
38303433323531356333663663663565316139383361623035303732323638663632
Повторяем для aws_secret_key.
Создаём файл с задачами, в котором будем вызывать модуль cloudformation — roles/cloudformation/tasks/main.yml, где используем include_vars: file: "{{ env }}_vars.yml", который определяет файл загрузки переменных для Dev/Prod:
- name: "Read ENV specific variables {{ env }}_vars.yml"
include_vars:
file: "{{ env }}_vars.yml"
- name: "Read AWS credenatials vault {{ env }}_vars.yml"
include_vars:
file: "/home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/aws-credenatials.yml"
- name: "Create {{ env }} stack"
cloudformation:
# /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/aws-credenatials.yml
aws_access_key: "{{ aws_access_key }}"
aws_secret_key: "{{ aws_secret_key }}"
# aws_env - from hosts.ini
stack_name: "{{ aws_env }}"
state: "present"
disable_rollback: true
template: "roles/cloudformation/files/rtfm-cf-stack.json"
# roles/cloudformation/vars/main.yml
region: "{{ region }}"
template_parameters:
# roles/cloudformation/vars/main.yml
AMIID: "{{ ami_id }}"
AvailabilityZone: "{{ availability_zone }}"
OfficeAllowLocation: "{{ office_allow_location }}"
HomeAllowLocationProvider1: "{{ home_allow_location_provider1 }}"
HomeAllowLocationProvider2: "{{ home_allow_location_provider2 }}"
# roles/cloudformation/vars/dev_vars.yml
ENV: "{{ aws_env }}"
KeyName: "{{ key_name }}"
EC2InstanceType: "{{ ec2_instance_type }}"
DataEBSID: "{{ data_ebs_id }}"
BackupsEBSID: "{{ backups_ebs_id }}"
tags:
Stack: "{{ aws_env }}"
Создаём сам файл плейбука — rtfm.yml:
- hosts: all
user: admin
roles:
- role: cloudformation
tags: infra
Создаём файл настроек Ansible — ansible.cfg, переопределим некоторые дефолтные параметры:
[defaults] host_key_checking=False inventory = hosts.ini retry_files_enabled = False
Всё готово, можно проверять.
Запускаем syntax-check:
[simterm]
$ ansible-playbook --tags "infra" --limit=rtfm-dev rtfm.yml --vault-password-file ~/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm_ansible_vault_pass --syntax-check playbook: rtfm.yml
[/simterm]
Для cloudformation модуля требуется boto3, устанавливаем:
[simterm]
$ sudo pacman -S python-boto3
[/simterm]
Запускаем создание стека:
[simterm]
# ansible-playbook --tags "infra" --limit=rtfm-dev rtfm.yml --vault-password-file /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm_ansible_vault_pass --extra-vars "ansible_connection=local" PLAY [all] **** TASK [Gathering Facts] **** ok: [dev.rtfm.co.ua] TASK [cloudformation : Read ENV specific variables dev_vars.yml] **** ok: [dev.rtfm.co.ua] TASK [cloudformation : Read AWS credenatials vault dev_vars.yml] **** ok: [dev.rtfm.co.ua] TASK [cloudformation : Create dev stack] **** changed: [dev.rtfm.co.ua] PLAY RECAP **** dev.rtfm.co.ua : ok=4 changed=1 unreachable=0 failed=0
[/simterm]
Стек готов:
Скрипт запуска Ansible
Для упрощения работы, что бы не вводить команды каждый раз — можно использовать простой скрипт на bash, который будет вызывать Ansible с необходимыми параметрами:
#!/usr/bin/env bash
HELP="\n\t-c: run CloudFormation role (--tags infra) to create a stack
\n\t-a: run all App roles excluding CF (--tags app), to provision an EC2 instance
\n\t-t: tags to apply, enclosed with double quotes and separated by commas, i.e. \"common, web\"
\n\t-S: skip dry-run
\n\t-e: environment to be used in --limit, defaults to \"rtfm-dev\"
\n\t-v: vault-password file, if none - default ~/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm_ansible_vault_pass
\n\t-r: RSA key to be used, defaults to ~/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm-dev-eu-west-1a.pem
\n"
[[ "$#" -lt 1 ]] && { echo -e $HELP; exit 1 ; }
### set defaults
# -c == infra, -a == app
TAGS=
# ansible_connection default to ssh
CONN="ansible_connection=ssh"
# -S
SKIP=0
# -e
ENV="rtfm-dev"
# -v
VAULT="/home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm_ansible_vault_pass"
# -r
RSA_KEY="/home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm-dev-eu-west-1a.pem"
while getopts "caSe:v:r:ht:" opt; do
case $opt in
c)
TAGS=infra
CONN="ansible_connection=local"
;;
a)
TAGS=app
;;
t)
TAGS=$OPTARG
;;
S)
SKIP=1
;;
e)
ENV=$OPTARG
;;
v)
VAULT=$OPTARG
;;
r)
RSA_KEY=$OPTARG
;;
h)
echo -e $HELP
exit 0
;;
?)
echo -e $HELP && exit 1
;;
esac
done
echo -e "\nTags: $TAGS\nEnv: $ENV\nVault: $VAULT\nRSA: $RSA_KEY\n"
read -p "Are you sure to proceed? [y/n] " -r
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
dependencies_install () {
ansible-galaxy install --role-file requirements.yml
}
syntax_check () {
local tags=$1
local env=$2
local vault=$3
local rsa=$4
ansible-playbook --private-key $rsa --tags "$1" --limit=$env rtfm.yml --vault-password-file $vault --syntax-check
}
ansible_check () {
local tags=$1
local env=$2
local vault=$3
local rsa=$4
local connection=$5
local application=$6
ansible-playbook --private-key $rsa --tags "$1" --limit=$env rtfm.yml --vault-password-file $vault --check --extra-vars "$connection $application"
}
ansible_exec () {
local tags=$1
local env=$2
local vault=$3
local rsa=$4
local connection=$5
local application=$6
ansible-playbook --private-key $rsa --tags "$1" --limit=$env rtfm.yml --vault-password-file $vault --extra-vars "$connection $application"
}
# asserts
[[ $TAGS ]] || { echo -e "\nERROR: no TAGS found. Exit."; exit 1; }
[[ $ENV ]] || { echo -e "\nERROR: no ENV found. Exit."; exit 1; }
[[ $VAULT ]] || { echo -e "\nERROR: no VAULT found. Exit."; exit 1; }
echo -e "\nInstalling dependencies...\n"
if dependencies_install; then
echo -e "\nDone."
else
echo -e "Something went wrong. Exit."
exit 1
fi
echo -e "\nExecuting syntax-check..."
if syntax_check "$TAGS" $ENV $VAULT $RSA_KEY; then
echo -e "Syntax check passed.\n"
else
echo -e "Something went wrong. Exit."
exit 1
fi
if [[ $SKIP == 0 ]]; then
echo -e "Running dry-run..."
if ansible_check "$TAGS" $ENV $VAULT $RSA_KEY $CONN $APP;then
echo -e "Dry-run check passed.\n"
else
echo -e "Something went wrong. Exit."
exit 1
fi
else
echo -e "Skipping dry-run.\n"
fi
read -p "Are you sure to proceed? [y/n] " -r
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
echo -e "Applying roles..."
if ansible_exec "$TAGS" $ENV $VAULT $RSA_KEY $CONN $APP;then
echo -e "Provisioning done.\n"
else
echo -e "Something went wrong. Exit."
exit 1
fi
Скрипт в репозитории — тут>>>.
Помощь:
[simterm]
# ./ansible_exec.sh -h
-c: run CloudFormation role (--tags infra) to create a stack
-a: run all App roles excluding CF (--tags app), to provision an EC2 instance
-t: tags to apply, enclosed with double quotes and separated by commas, i.e. "common, web"
-S: skip dry-run
-e: environment to be used in --limit, defaults to "rtfm-dev"
-v: vault-password file, if none - default ~/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm_ansible_vault_pass
-r: RSA key to be used, defaults to ~/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm-dev-eu-west-1a.pem
[/simterm]
Скрипт задаёт дефолтные значения, если не указаны в аргументах — окружение (rtfm-dev), путь к файлу с паролем для ansible-vault и RSA-ключ для хоста.
С помощью опций -c, -a и -t можно задать теги для запускаемых ролей.
Например, для роли cloudformation это тег infra:
- hosts: all
user: admin
roles:
- role: cloudformation
tags: infra
В функции ansible_check() скрипт выполняет проверку ролей с помощью ansible-playbook --check, которую можно пропустить с помощью опции -S.
Запускаем его:
[simterm]
# ./ansible_exec.sh -c Tags: infra Env: rtfm-dev Vault: /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm_ansible_vault_pass RSA: /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm-dev-eu-west-1a.pem Are you sure to proceed? [y/n] y Installing dependencies... [WARNING]: - SimpliField.logrotate (master) is already installed - use --force to change version to unspecified [WARNING]: - jnv.unattended-upgrades (v1.6.0) is already installed - use --force to change version to unspecified [WARNING]: - geerlingguy.docker (2.5.1) is already installed - use --force to change version to unspecified Done. Executing syntax-check... playbook: rtfm.yml Syntax check passed. Running dry-run... PLAY [all] **** TASK [Gathering Facts] **** ok: [dev.rtfm.co.ua] TASK [cloudformation : Read ENV specific variables dev_vars.yml] **** ok: [dev.rtfm.co.ua] TASK [cloudformation : Read AWS credenatials vault dev_vars.yml] **** ok: [dev.rtfm.co.ua] TASK [cloudformation : Create dev stack] **** ok: [dev.rtfm.co.ua] PLAY RECAP **** dev.rtfm.co.ua : ok=4 changed=0 unreachable=0 failed=0 Dry-run check passed. Are you sure to proceed? [y/n] y Applying roles... PLAY [all] **** TASK [Gathering Facts] **** ok: [dev.rtfm.co.ua] TASK [cloudformation : Read ENV specific variables dev_vars.yml] **** ok: [dev.rtfm.co.ua] TASK [cloudformation : Read AWS credenatials vault dev_vars.yml] **** ok: [dev.rtfm.co.ua] TASK [cloudformation : Create dev stack] **** ok: [dev.rtfm.co.ua] PLAY RECAP **** dev.rtfm.co.ua : ok=4 changed=0 unreachable=0 failed=0 Provisioning done.
[/simterm]
Стек уже создан, поэтому Ansible ничего не изменил.
Ansilbe роли
Осталось добавить роли для установки и настройки сервисов.
Не буду тут останавливаться на всех, приведу пример добавления ролей common, nginx, mysql и letsncrypt.
common роль
Роль common выполняет установку базовых пакетов, настройку окружения, пользователей, имени хоста и т.д.
Создаём каталоги:
[simterm]
$ mkdir -p roles/common/{tasks,vars,files}
[/simterm]
Создаём файл roles/common/tasks/main.yml:
- name: "Upgrade all packages to the latest version"
apt:
update_cache: yes
upgrade: yes
- name: "Install common packages"
apt: name={{item}} state=present
with_items:
- python-apt
- mailutils
- curl
- dnsutils
- telnet
- unzip
- parted
- mc
- bc
- golang
- python-docker
- python-pip
- htop
- mysql-client
- tree
- name: "Create partition on the {{ data_volume_device }}"
parted:
device: "{{ data_volume_device }}"
number: 1
state: present
- name: "Create {{ data_volume_mount_path }} directory"
file:
path: "{{ data_volume_mount_path }}"
state: directory
mode: 0755
recurse: yes
- name: "Create a ext4 filesystem on the {{ data_volume_partition }}"
filesystem:
fstype: ext4
dev: "{{ data_volume_partition }}"
- name: "Mount data-volume {{ data_volume_partition }} to the {{ data_volume_mount_path }}"
mount:
path: "{{ data_volume_mount_path }}"
src: "{{ data_volume_partition }}"
state: mounted
fstype: ext4
...
- name: "Set hostname"
hostname:
name: "{{ set_hostname }}"
- name: "Add hostname to /etc/hosts"
lineinfile:
dest: /etc/hosts
regexp: '^127\.0\.0\.1[ \t]+localhost'
line: "127.0.0.1 localhost {{ set_hostname }} {{ inventory_hostname }}"
state: present
- name: "Update /etc/cloud/cloud.cfg.d/01_debian_cloud.cfg"
lineinfile:
dest: /etc/cloud/cloud.cfg.d/01_debian_cloud.cfg
regexp: '^manage_etc_hosts: true'
line: "manage_etc_hosts: false"
state: present
- name: "Set timezone to Europe/Kiev"
timezone:
name: Europe/Kiev
- name: "Change root mailbox"
lineinfile:
dest: /etc/aliases
regexp: '^root: '
line: "root: {{ notify_email }}"
state: present
- name: "Update mail aliases"
shell:
newaliases
- name: "Add the 'setevoy' system user"
user:
name: "setevoy"
shell: "/bin/bash"
append: yes
groups: "sudo"
- name: "Copy .vimrc to the 'root' user"
copy:
src: "files/vimrc"
dest: "/root/.vimrc"
- name: "Copy .vimrc to the 'setevoy' user"
copy:
src: "files/vimrc"
dest: "/home/setevoy/.vimrc"
Тут устанавливаем пакеты, создаём разделы на дисках /dev/xvdb для /data и /dev/xvdc для /backups, монтируем их, создаём файловые системы, задаём имя хоста из переменной set_hostname, заданной в файле hosts.ini, настраиваем почту root, добавляем пользователя setevoy и копируем .vimrc.
Проверяем подключенные к инстансу диски:
[simterm]
root@ip-10-0-2-12:/home/admin# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT xvda 202:0 0 8G 0 disk └─xvda1 202:1 0 8G 0 part / xvdb 202:16 0 8G 0 disk xvdc 202:32 0 8G 0 disk
[/simterm]
xvdb будет /data, xvdc будет /backups.
В roles/common/vars/main.yml задаём переменные:
notify_email: !vault | $ANSIBLE_VAULT;1.1;AES256 62376361343165653932663834366434626366646230623964623531356363313835623566383236 3732323331646437643161383134356231316564316665360a656666306239343866306431616639 61373739623637623766386162343036303031393332373537306435363433313762313864333261 6265643131393939320a343938626264323635363534353334303163353566653866366161393930 35313036616363393636656163623231346265353032396235363134383831346461 data_volume_device: "/dev/xvdb" data_volume_partition: "/dev/xvdb1" data_volume_mount_path: "/data" backups_volume_device: "/dev/xvdc" backups_volume_partition: "/dev/xvdc1" backups_volume_mount_path: "/backups"
notify_email шифруем аналогично тому, как шифровали IP, что бы не светить лишний раз почтовый ящик в сети.
Создаём файл roles/common/files/vimrc:
set tabstop=4 set shiftwidth=4 set softtabstop=4 set expandtab set paste set smartindent syntax on
Добавляем become: true и вызов роли в плейбук rtfm.yml с указанием тегов common и apps, что бы иметь возможность запускать его со всеми другими ролями указав скрипту указав -a, или отдельно только эту роль — указав-t "common":
- hosts: all
become:
true
roles:
- role: cloudformation
tags: infra
- role: common
tags: common, app
Переносим в ansible.cfg пользователя:
[defaults] remote_user=admin host_key_checking=False inventory = hosts.ini retry_files_enabled = False
Вроде ОК? Запускаем скрипт, первый раз с опцией -S, что бы пропустить syntax-check (или руками устанавливаем python-apt на хосте):
[simterm]
# ./ansible_exec.sh -a -S Tags: app Env: rtfm-dev Vault: /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm_ansible_vault_pass RSA: /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm-dev-eu-west-1a.pem Are you sure to proceed? [y/n] y Installing dependencies... ... Done. Executing syntax-check... playbook: rtfm.yml Syntax check passed. Skipping dry-run. Are you sure to proceed? [y/n] y Applying roles... PLAY [all] **** TASK [Gathering Facts] **** ok: [dev.rtfm.co.ua] TASK [common : Upgrade all packages to the latest version] **** [WARNING]: Could not find aptitude. Using apt-get instead changed: [dev.rtfm.co.ua] TASK [common : Install common packages] **** changed: [dev.rtfm.co.ua] => (item=[u'python-apt', u'mailutils', u'curl', u'dnsutils', u'telnet', u'unzip', u'parted', u'mc', u'bc', u'golang', u'python-docker', u'python-pip', u'htop', u'mysql-client', u'tree']) TASK [common : Create partition on the /dev/xvdb] **** changed: [dev.rtfm.co.ua] TASK [common : Create /data directory] **** changed: [dev.rtfm.co.ua] TASK [common : Create a ext4 filesystem on the /dev/xvdb1] **** changed: [dev.rtfm.co.ua] TASK [common : Mount data-volume /dev/xvdb1 to the /data] **** changed: [dev.rtfm.co.ua] TASK [common : Create partition on the /dev/xvdc] **** changed: [dev.rtfm.co.ua] TASK [common : Create /backups directory] **** changed: [dev.rtfm.co.ua] TASK [common : Create a ext4 filesystem on the /dev/xvdc1] **** changed: [dev.rtfm.co.ua] TASK [common : Mount backups-volume /dev/xvdc1 to the /backups] **** changed: [dev.rtfm.co.ua] TASK [common : Set hostname] **** changed: [dev.rtfm.co.ua] TASK [common : Add hostname to /etc/hosts] **** changed: [dev.rtfm.co.ua] TASK [common : Update /etc/cloud/cloud.cfg.d/01_debian_cloud.cfg] **** changed: [dev.rtfm.co.ua] TASK [common : Set timezone to Europe/Kiev] **** changed: [dev.rtfm.co.ua] ... Provisioning done.
[/simterm]
Проверяем разделы:
[simterm]
root@ip-10-0-2-12:/home/admin# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT xvda 202:0 0 8G 0 disk └─xvda1 202:1 0 8G 0 part / xvdb 202:16 0 8G 0 disk └─xvdb1 202:17 0 8G 0 part /data xvdc 202:32 0 8G 0 disk └─xvdc1 202:33 0 8G 0 part /backups
[/simterm]
Всё на месте, всё готово.
nginx роль
Здесь пришлось немного повозиться из-за желания разделить данные между приватным и публичным репозиторием.
Создаём каталоги для роли в публичном репозитории:
[simterm]
$ mkdir -p roles/nginx/{tasks,vars,files,templates}
[/simterm]
Для создания пользователей и каталогов — используем список web_projects с вложенными списками domains, а затем плагин nested.
В приватном репозитории создаём каталог vars и файл ~/Work/RTFM/Bitbucket/rtfm-infrastructure/vars/dev_web_projects.yml, в котором перечисляем пользователей и их домены:
web_projects:
- name: rtfm
domains:
- dev.rtfm.co.ua
- test.rtfm.co.ua
- name: setevoy
domains:
- dev.test.setevoy.kiev.ua
...
В публичном репозии создаём файл roles/nginx/vars/main.yml, в котором задаём переменную web_data_root_prefix:
web_data_root_prefix: "/data/www"
Создаём файл roles/nginx/tasks/main.yml, пока добавим только установку nginx, создание пользователей и каталогов.
При этом при создании пользователей — пропускаем пользователя setevoy, т.к. он используется не только для NGINX, и уже создан в роли common (но требуется в списке web_users для создания каталогов).
При создании пользователей в «Add web-users» — используем with_items, где в цикле передаём item.name из списка web_pojects:
...
- name: "Add web-users"
user:
name: "{{ item.name }}"
shell: "/usr/sbin/nologin"
with_items: "{{ web_projects }}"
when: '"setevoy" not in item'
...
А следующим шагом, в «Check {{ web_data_root_prefix }} directories for virtual hosts» — используем список «{{ web_projects }}» и его элемент item.name для создания каталогов вида /data/www/rtfm/, и вложенный список directories, из которого используем имена хостов для создания директорий вида /data/www/rtfm/dev.rtfm.co.ua/ и /data/www/rtfm/test.rtfm.co.ua/:
...
- name: "Check {{ web_data_root_prefix }} directories for virtual hosts"
file:
path: "{{ web_data_root_prefix }}/{{ item.0.name }}/{{ item.1 }}"
state: directory
recurse: yes
with_subelements:
- "{{ web_projects }}"
- domains
...
Всё вместе сейчас выглядит так:
- name: "Read private env-specific variables from the {{ local_private_repo_location}}/vars/{{ env }}_web_projects.yml"
include_vars:
file: "{{ local_private_repo_location}}/vars/{{ env }}_web_projects.yml"
- name: "Add web-users"
user:
name: "{{ item.name }}"
shell: "/usr/sbin/nologin"
with_items: "{{ web_projects }}"
when: '"setevoy" not in item'
- name: "Check {{ web_data_root_prefix }} directories for virtual hosts"
file:
path: "{{ web_data_root_prefix }}/{{ item.0.name }}/{{ item.1 }}"
state: directory
owner: "{{ item.0.name }}"
group: "{{ item.0.name }}"
recurse: yes
with_subelements:
- "{{ web_projects }}"
- domains
- name: "Install NGINX"
apt:
name: nginx
Добавляем роль nginx в плейбук с тегами web, nginx, app:
- hosts: all
become:
true
roles:
- role: test
tags: test
- role: cloudformation
tags: infra
- role: common
tags: common, app
- role: nginx
tags: web, nginx, app
Используя теги — будет возможность запустить только роль nginx, указав тег nginx, роли nginx и php-fpm — указав тег web, или все роли — указав тег app.
Проверяем:
[simterm]
# ./ansible_exec.sh -t "web"
Tags: web
Env: rtfm-dev
Vault: /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm_ansible_vault_pass
RSA: /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/aws-credenatials/rtfm-dev-eu-west-1a.pem
Are you sure to proceed? [y/n] y
Installing dependencies...
[WARNING]: - SimpliField.logrotate (master) is already installed - use --force to change version to unspecified
...
Done.
Executing syntax-check...
playbook: rtfm.yml
Syntax check passed.
Running dry-run...
...
TASK [nginx : Read private env-specific variables from the /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/vars/dev_web_projects.yml] **************************************************************************************************
ok: [dev.rtfm.co.ua]
TASK [nginx : Add web-users] ****
changed: [dev.rtfm.co.ua] => (item={u'domains': [u'dev.rtfm.co.ua', u'test.rtfm.co.ua'], u'name': u'rtfm'})
skipping: [dev.rtfm.co.ua] => (item={u'domains': [u'dev.test.setevoy.kiev.ua'], u'name': u'setevoy'})
...
TASK [nginx : Check /data/www directories for virtual hosts] ****
changed: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'dev.rtfm.co.ua'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'test.rtfm.co.ua'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'setevoy'}, u'dev.test.setevoy.kiev.ua'))
...
TASK [nginx : Install NGINX] ****
ok: [dev.rtfm.co.ua]
PLAY RECAP ****
dev.rtfm.co.ua : ok=5 changed=2 unreachable=0 failed=0
Dry-run check passed.
Are you sure to proceed? [y/n] y
Applying roles...
PLAY [all] ****
TASK [Gathering Facts] ****
ok: [dev.rtfm.co.ua]
TASK [nginx : Read private env-specific variables from the /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/vars/dev_web_projects.yml] **************************************************************************************************
ok: [dev.rtfm.co.ua]
TASK [nginx : Add web-users] ****
changed: [dev.rtfm.co.ua] => (item={u'domains': [u'dev.rtfm.co.ua', u'test.rtfm.co.ua'], u'name': u'rtfm'})
skipping: [dev.rtfm.co.ua] => (item={u'domains': [u'dev.test.setevoy.kiev.ua'], u'name': u'setevoy'})
...
TASK [nginx : Check /data/www directories for virtual hosts] ****
changed: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'dev.rtfm.co.ua'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'test.rtfm.co.ua'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'setevoy'}, u'dev.test.setevoy.kiev.ua'))
...
TASK [nginx : Install NGINX] ****
ok: [dev.rtfm.co.ua]
PLAY RECAP ****
dev.rtfm.co.ua : ok=5 changed=2 unreachable=0 failed=0
Provisioning done.
[/simterm]
Проверяем пользователей:
[simterm]
root@rtfm-dev:/home/admin# cat /etc/shadow | grep 'rtfm\|setevoy' setevoy:!:17766:0:99999:7::: rtfm:!:17766:0:99999:7:::
[/simterm]
Проверяем логин:
[simterm]
root@rtfm-dev:/home/admin# su -l rtfm This account is currently not available. root@rtfm-dev:/home/admin# su -l setevoy setevoy@rtfm-dev:~$
[/simterm]
ОК — все, кроме пользователя setevoy логиниться не могут (см. /bin/false vs /sbin/nologin).
И каталоги:
[simterm]
root@rtfm-dev:/home/admin# ls -l /data/www/ | grep 'rtfm\|setevoy' drwxr-xr-x 2 rtfm rtfm 4096 Aug 23 12:46 rtfm drwxr-xr-x 2 setevoy setevoy 4096 Aug 23 12:46 setevoy
[/simterm]
Вложенные каталоги для виртуалхостов:
[simterm]
root@rtfm-dev:/home/admin# ls -l /data/www/rtfm/ total 8 drwxr-xr-x 2 rtfm rtfm 4096 Aug 24 10:03 dev.rtfm.co.ua drwxr-xr-x 2 rtfm rtfm 4096 Aug 24 10:03 test.rtfm.co.ua
[/simterm]
Хорошо, всё работает.
Далее надо добавить копирование файлов настроек самого NGINX и виртуалхостов.
Создаём файл roles/nginx/templates/nginx.conf.j2:
user www-data;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
client_max_body_size 240M;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
log_format main_ext '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'"$host" sn="$server_name" '
'rt=$request_time '
'ua="$upstream_addr" us="$upstream_status" '
'ut="$upstream_response_time" ul="$upstream_response_length" '
'cs=$upstream_cache_status' ;
#access_log /var/log/nginx/access.log main;
access_log /var/log/nginx/access.log main_ext;
error_log /var/log/nginx/error.log warn;
sendfile on;
tcp_nopush on;
# enable gzip
gzip on;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_min_length 1100;
gzip_buffers 4 8k;
gzip_proxied any;
gzip_types
# text/html is always compressed by HttpGzipModule
text/css
text/javascript
text/xml
text/plain
text/x-component
application/javascript
application/json
application/xml
application/rss+xml
font/truetype
font/opentype
application/vnd.ms-fontobject
image/svg+xml;
gzip_static on;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
}
Создаём каталоги в приватном репозитории:
[simterm]
$ mkdir -p ~/Work/RTFM/Bitbucket/rtfm-infrastructure/nginx/templates/{dev,production}
[/simterm]
Создаём конфиг для Dev — ~/Work/RTFM/Bitbucket/rtfm-infrastructure/nginx/templates/dev/dev.rtfm.co.ua.conf.j2:
server {
listen 80;
server_name {{ item.1 }};
root {{ web_data_root_prefix }}/rtfm/{{ item.1 }};
access_log /var/log/nginx/{{ item.1 }}-access.log main_ext;
error_log /var/log/nginx/{{ item.1 }}-error.log warn;
allow {{ office_allow_location }};
allow {{ home_allow_location_provider1 }};
allow {{ home_allow_location_provider2 }};
deny all;
error_page 404 /404.html;
location = /404.html {
root {{ web_data_root_prefix }}/{{ item.0.name }}/{{ item.1 }};
}
...
location ~ \.php$ {
proxy_read_timeout 3000;
include /etc/nginx/fastcgi_params;
fastcgi_pass unix:/var/run/{{ item.1 }}-php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
В шаблоне используем {{ item.0.name }} из списка web_projects для подстановки пользователя, и {{ item.1 }} из вложенного списка domains для подстановки домена.
Пока без SSL, позже добавим Lets Encrypt роль и обновим шаблон.
Повторяем для всех доменов из web_projects:
[simterm]
$ ls -l ~/Work/RTFM/Bitbucket/rtfm-infrastructure/nginx/templates/dev/ | grep 'rtfm\|setevoy' -rw-r--r-- 1 setevoy setevoy 3465 Aug 23 13:38 dev.rtfm.co.ua.conf.j2 -rw-r--r-- 1 setevoy setevoy 3465 Aug 24 10:15 dev.test.setevoy.kiev.ua.conf.j2 -rw-r--r-- 1 setevoy setevoy 3465 Aug 24 10:14 test.rtfm.co.ua.conf.j2
[/simterm]
В приватном репозитории создаём каталог для файлов .htaccess:
[simterm]
$ mkdir ~/Work/RTFM/Bitbucket/rtfm-infrastructure/nginx/files
[/simterm]
Создаём файл с паролем для HTTP-авторизации:
[simterm]
$ htpasswd -c /home/setevoy/Work/RTFM/Bitbucket/rtfm-infrastructure/nginx/files/htpasswd_rtfm setevoy New password: Re-type new password: Adding password for user setevoy
[/simterm]
Повторяем для всех проектов (rtfm, setevoy etc).
Обновляем roles/nginx/tasks/main.yml — добавляем копирование файлов, в «Add NGINX virtualhosts configs» снова используем вложенный список domains:
...
- name: "Replace NGINX config"
template:
src: "templates/nginx.conf.j2"
dest: "/etc/nginx/nginx.conf"
owner: "root"
group: "root"
mode: 0644
- name: "Copy Status HTTP auth file"
copy:
src: "{{ local_private_repo_location }}/nginx/files/htpasswd_{{ item.name }}"
dest: "{{ web_data_root_prefix }}/{{ item.name }}/.htpasswd_{{ item.name }}"
owner: "www-data"
group: "www-data"
mode: 0600
with_items: "{{ web_projects }}"
ignore_errors: True
- name: "Add NGINX virtualhosts configs"
template:
src: "{{ local_private_repo_location }}/nginx/templates/{{ env }}/{{ item.1 }}.conf.j2"
dest: "/etc/nginx/conf.d/{{ item.1 }}.conf"
owner: "root"
group: "root"
mode: 0644
with_subelements:
- "{{ web_projects }}"
- domains
Запускаем Ansible:
[simterm]
...
TASK [nginx : Replace NGINX config] ****
ok: [dev.rtfm.co.ua]
TASK [nginx : Copy Status HTTP auth file] ****
changed: [dev.rtfm.co.ua] => (item={u'domains': [u'dev.rtfm.co.ua', u'test.rtfm.co.ua'], u'name': u'rtfm'})
changed: [dev.rtfm.co.ua] => (item={u'domains': [u'dev.test.setevoy.kiev.ua'], u'name': u'setevoy'})
...
TASK [nginx : Add NGINX virtualhosts configs] ****
ok: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'dev.rtfm.co.ua'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'test.rtfm.co.ua'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'setevoy'}, u'dev.test.setevoy.kiev.ua'))
...
PLAY RECAP ****
dev.rtfm.co.ua : ok=8 changed=2 unreachable=0 failed=0
Provisioning done.
[/simterm]
Проверяем файлы на хосте:
[simterm]
root@rtfm-dev:/home/admin# ls -l /etc/nginx/conf.d/ | grep 'rtfm\|setevoy' -rw-r--r-- 1 root root 3316 Aug 24 09:11 dev.rtfm.co.ua.conf -rw-r--r-- 1 root root 3316 Aug 24 10:20 dev.test.setevoy.kiev.ua.conf -rw-r--r-- 1 root root 3316 Aug 24 10:20 test.rtfm.co.ua.conf
[/simterm]
Содержимое конфигов:
[simterm]
root@rtfm-dev:/home/admin# head /etc/nginx/conf.d/dev.rtfm.co.ua.conf
server {
listen 80;
server_name dev.rtfm.co.ua;
root /data/www/rtfm/dev.rtfm.co.ua;
access_log /var/log/nginx/dev.rtfm.co.ua-access.log main_ext;
error_log /var/log/nginx/dev.rtfm.co.ua-error.log warn;
root@rtfm-dev:/home/admin# head /etc/nginx/conf.d/dev.rtfm.co.ua.conf /etc/nginx/conf.d/dev.test.setevoy.kiev.ua.conf
==> /etc/nginx/conf.d/dev.rtfm.co.ua.conf <==
server {
listen 80;
server_name dev.rtfm.co.ua;
root /data/www/rtfm/dev.rtfm.co.ua;
access_log /var/log/nginx/dev.rtfm.co.ua-access.log main_ext;
error_log /var/log/nginx/dev.rtfm.co.ua-error.log warn;
==> /etc/nginx/conf.d/dev.test.setevoy.kiev.ua.conf <==
server {
listen 80;
server_name dev.test.setevoy.kiev.ua;
root /data/www/rtfm/dev.test.setevoy.kiev.ua;
access_log /var/log/nginx/dev.test.setevoy.kiev.ua-access.log main_ext;
error_log /var/log/nginx/dev.test.setevoy.kiev.ua-error.log warn;
[/simterm]
Коректность конфигов (потом добавим это в Ansible):
[simterm]
root@rtfm-dev:/home/admin# nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
[/simterm]
Перечитываем их:
[simterm]
root@rtfm-dev:/home/admin# service nginx reload
[/simterm]
Создаём каталог и добавляем тестовый файлик:
[simterm]
root@rtfm-dev:/home/admin# mkdir /data/www/rtfm/dev.rtfm.co.ua root@rtfm-dev:/home/admin# echo "It works!" > /data/www/rtfm/dev.rtfm.co.ua/test.html
[/simterm]
Проверяем доступ:
В roles/nginx/tasks/main.yml добавляем проверку корректности конфигов и перезагрузку NGINX:
...
- name: "Check NGINX configs"
shell: "/usr/sbin/nginx -t"
register: nginx_config_status
- name: "NGINX test status"
debug:
msg: "{{ nginx_config_status }}"
- name: "NGINX test return code"
debug:
msg: "{{ nginx_config_status.rc }}"
- name: "Service NGINX restart and enable on boot"
systemd:
name: nginx
state: restarted
enabled: yes
daemon_reload: yes
when: nginx_config_status.rc == 0
Роль php-fpm аналогична, её описывать не буду, есть>>> в репозитории.
mysql роль
Создаём каталог роли:
[simterm]
$ mkdir -p roles/mysql/{tasks,vars}
[/simterm]
Создаём файл с задачами.
Начнём с установки MariaDB и смены пути к каталогу с данными — вместо дефолтного /var/lib/mysql создадим каталог /data/mysql, что бы базы лежали на отдельном EBS (хотя, возможно, спорное решение — но пока попробую держать отдельно).
В файле roles/mysql/tasks/main.yml добавляем задачи:
- name: "Install MariaDB server"
package:
name: "{{ item }}"
state: installed
with_items:
- mariadb-server
- python-mysqldb
- name: "Create {{ db_datadir }}"
file:
path: "{{ db_datadir }}"
state: directory
owner: "mysql"
group: "mysql"
- name: "Check if {{ db_datadir }}/mysql present"
stat:
path: "{{ db_datadir }}/mysql"
register: db_data_dir
- name: "Synchronize /var/lib/mysql with {{ db_datadir }}"
synchronize:
src: "/var/lib/mysql/"
dest: "{{ db_datadir }}"
delegate_to: "{{ inventory_hostname }}"
when: not db_data_dir.stat.exists and not db_data_dir.stat.exists
- name: "Change 'datadir' to the {{ db_datadir }}"
lineinfile:
dest: "/etc/mysql/mariadb.conf.d/50-server.cnf"
regexp: '^datadir'
line: "datadir = {{ db_datadir }}"
state: present
- name: "MariaDB restart and enable on boot"
systemd:
name: mariadb
state: restarted
enabled: yes
daemon_reload: yes
- name: "Delete default /var/lib/mysql"
file:
state: absent
path: "/var/lib/mysql/"
when: db_default_dir.stat.exists and db_default_dir.stat.exists
Тут:
- устанавливаем MariaDB
- создаём
/data/mysql - проверяем существует ли каталог (база
mysql)/data/mysql/mysql: если установка новая, на новый EBS — то каталог будет не найден, и его надо будет синхронизировать из/var/lib/mysql, иначе MariaDB не запустится после обновления параметраdatadir, если это обновление уже настроенного хоста (/dataподключается из отдельного EBS, на котором уже могут быть данные) — то пропускаем синхронизацию - копируем базы, если
/data/mysql/mysqlне найден - обновляем
datadirв/etc/mysql/mariadb.conf.d/50-server.cnf - перезапускаем MariaDB
- удаляем
/var/lib/mysql
Создаём файл с переменными — roles/mysql/vars/main.yml, тут только одна:
db_datadir: "/data/mysql"
(правильнее было бы в roles/mysql/defaults/main.yml, потом перенесу)
Добавляем роль в плейбук rtfm.yml:
- hosts: all
become:
true
roles:
- role: test
tags: test
- role: cloudformation
tags: infra
- role: common
tags: common, app
- role: nginx
tags: web, app
- role: php-fpm
tags: web, app
- role: mysql
tags: db, app
Запускаем:
[simterm]
... TASK [mysql : Install MariaDB server] **** changed: [dev.rtfm.co.ua] => (item=mariadb-server) changed: [dev.rtfm.co.ua] => (item=python-mysqldb) TASK [mysql : Create /data/mysql] **** changed: [dev.rtfm.co.ua] TASK [mysql : Check if /data/mysql/mysql present] **** ok: [dev.rtfm.co.ua] TASK [mysql : Synchronize /var/lib/mysql with /data/mysql] **** changed: [dev.rtfm.co.ua -> dev.rtfm.co.ua] TASK [mysql : Change 'datadir' to the /data/mysql] **** changed: [dev.rtfm.co.ua] TASK [mysql : MariaDB restart and enable on boot] **** changed: [dev.rtfm.co.ua] TASK [mysql : Delete default /var/lib/mysql] **** changed: [dev.rtfm.co.ua] ...
[/simterm]
Проверяем:
[simterm]
root@rtfm-dev:/home/admin# mysql -u root -ptestpw --execute 'SHOW VARIABLES WHERE Variable_Name LIKE "datadir"' +---------------+--------------+ | Variable_name | Value | +---------------+--------------+ | datadir | /data/mysql/ | +---------------+--------------+
[/simterm]
Далее требуется создать базы и пользователей.
В приватном репозитории добавляем файл ~/Work/RTFM/Bitbucket/rtfm-infrastructure/vars/dev_db_projects.yml.
В нём задаём данные доступа для root, список db_users с пользователями и их паролями (потом их зашифруем), и список db_projects с вложенными списками databases:
db_root_user: root
db_root_pass: "testpw"
db_root_host: localhost
db_users:
rtfm:
password: "testpw"
host: localhost
setevoy:
password: "testpw"
host: localhost
...
db_projects:
- name: rtfm
databases:
- rtfm_dev
- rtfm_test
- name: setevoy
databases:
- money_dev
- test_dev
...
Возвращаемся к roles/mysql/tasks/main.yml и добавляем задачи:
- обновить пароль
root - удалить анонимного пользователя
- отключить
rootлогин сlocalhostбез пароля - создать базы данных из списков
databasesв спискеdb_projects - создать пользователей из списка
db_users - задать им права доступа к базам из списков
databasesспискаdb_projects, используяappend_privs, что бы дать привилегии на несколько баз
Получилось немного запутанно — но работает (хотя наверняка есть более «прямой» путь решения).
Задачи:
...
- name: "Update mysql root password"
mysql_user:
name: "{{ db_root_user }}"
host: "{{ db_root_host }}"
password: "{{ db_root_pass }}"
login_user: "{{ db_root_user }}"
login_password: "{{ db_root_pass }}"
check_implicit_admin: yes
priv: "*.*:ALL,GRANT"
- name: "Remove anonymous user"
mysql_user:
name: ''
host: localhost
state: absent
login_user: "{{ db_root_user }}"
login_password: "{{ db_root_pass }}"
- name: "Disable root@localhost NOPASSWD login"
command: "{{ item }}"
with_items:
- "mysql -u {{ db_root_user }} -p{{ db_root_pass }} --execute=\"UPDATE mysql.user SET plugin = '' WHERE user = 'root' AND host = 'localhost'\""
- "mysql -u {{ db_root_user }} -p{{ db_root_pass }} --execute=\"FLUSH PRIVILEGES\""
- name: "Create databases"
mysql_db:
name: "{{ item.1 }}"
state: present
login_user: "{{ db_root_user }}"
login_password: "{{ db_root_pass }}"
with_subelements:
- "{{ db_projects }}"
- databases
- name: "Create projects users"
mysql_user:
name: "{{ item.key }}"
password: "{{ item.value.password }}"
state: present
login_user: "{{ db_root_user }}"
login_password: "{{ db_root_pass }}"
with_dict: "{{ db_users }}"
- name: "Grant permissions"
mysql_user:
name: "{{ item.0.name }}"
priv: "{{ item.1 }}.*:ALL"
append_privs: "yes"
state: present
login_user: "{{ db_root_user }}"
login_password: "{{ db_root_pass }}"
with_subelements:
- "{{ db_projects }}"
- databases
Запускаем:
[simterm]
TASK [mysql : Update mysql root password] ****
changed: [dev.rtfm.co.ua]
TASK [mysql : Remove anonymous user] ****
changed: [dev.rtfm.co.ua]
TASK [mysql : Disable root@localhost NOPASSWD login] ****
changed: [dev.rtfm.co.ua] => (item=mysql -u root -ptestpw --execute="UPDATE mysql.user SET plugin = '' WHERE user = 'root' AND host = 'localhost'")
changed: [dev.rtfm.co.ua] => (item=mysql -u root -ptestpw --execute="FLUSH PRIVILEGES")
TASK [mysql : Create databases] ****
changed: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'rtfm_dev'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'rtfm_test'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'setevoy'}, u'money_dev'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'setevoy'}, u'test_dev'))
...
TASK [mysql : Create projects users] ****
changed: [dev.rtfm.co.ua] => (item={'value': {u'host': u'localhost', u'password': u'testpw'}, 'key': u'rtfm'})
changed: [dev.rtfm.co.ua] => (item={'value': {u'host': u'localhost', u'password': u'testpw'}, 'key': u'setevoy'})
...
TASK [mysql : Grant permissions] ****
changed: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'rtfm_dev'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'rtfm_test'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'setevoy'}, u'money_dev'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'setevoy'}, u'test_dev'))
...
PLAY RECAP ****
dev.rtfm.co.ua : ok=9 changed=6 unreachable=0 failed=0
Provisioning done.
[/simterm]
Проверяем:
[simterm]
root@rtfm-dev:/home/admin# mysql -u rtfm -ptestpw --execute 'show databases' +--------------------+ | Database | +--------------------+ | information_schema | | rtfm_dev | | rtfm_test | +--------------------+
[/simterm]
С этим закончили.
letsencrypt роль
Осталось установить Let’s Encrypt клиент и настроить SSL, после чего можно копировать блог и тестить всё получившееся.
У Ansible уже есть модуль letsencrypt — но я предпочту создать роль сам.
notify_email переносим из roles/common/vars/main.yml в group_vars/all/main.yml.
Создаём каталог:
[simterm]
$ mkdir -p roles/letsencrypt/tasks
[/simterm]
Создаём файл roles/letsencrypt/tasks/main.yml:
- name: "Read private env-specific variables from the {{ local_private_repo_location}}/vars/{{ env }}_web_projects.yml"
include_vars:
file: "{{ local_private_repo_location}}/vars/{{ env }}_web_projects.yml"
- name: "Install Let's Encrypt client"
apt:
name=letsencrypt
state=latest
- name: "Stop NGINX"
systemd:
name: nginx
state: stopped
ignore_errors: true
- name: "Check existing certificates"
command: "ls -1 /etc/letsencrypt/live/"
register: live_certs
ignore_errors: true
- name: "Certs found"
debug:
msg: "{{ live_certs.stdout }}"
- name: "Obtain certificates"
command: "letsencrypt certonly --standalone --agree-tos -m {{ notify_email }} -d {{ item.1 }}"
# debug:
# msg: "getting cert {{ item.1 }}"
with_subelements:
- "{{ web_projects }}"
- domains
when: "item.1 not in live_certs.stdout_lines"
- name: "Start NGINX"
systemd:
name: nginx
state: started
ignore_errors: true
- name: "Update renewal settings to web-root"
lineinfile:
dest: "/etc/letsencrypt/renewal/{{ item.1 }}.conf"
regexp: '^authenticator '
line: "authenticator = webroot"
state: present
with_subelements:
- "{{ web_projects }}"
- domains
- name: "Add Let's Encrypt cronjob for cert renewal"
cron:
name: letsencrypt_renewal
special_time: weekly
job: letsencrypt renew --webroot -w /var/www/html/ &> /var/log/letsencrypt/letsencrypt.log && service nginx reload
Тут есть один «костыль»: изначально сертификаты получаются через standalone аутентификатор (command: "letsencrypt certonly --standalone), т.к. если установка на новый хост — то NGINX там ещё не будет, а обновляться будут через webroot— т.к. NGINX уже будет запущен, а останавливать его для проверки апдейтов сертификатов — идея так себе. Следовательно — надо обновить конфигурацию в конфигах в каталоге /etc/letsencrypt/renewal/, что и делается в задаче «Update renewal settings to web-root». Должно работать, проверю по ходу дела.
Добавляем в плейбук rtfm.yml перед NGINX, т.к. его конфиги уже будут с настроенным SSL:
- hosts: all
become:
true
roles:
- role: test
tags: test
- role: cloudformation
tags: infra
- role: common
tags: common, app
- role: letsencrypt
tags: ssl, app
- role: nginx
tags: nginx, web, app
- role: php-fpm
tags: php, web, app
- role: mysql
tags: db, app
Запускаем Ansible:
[simterm]
...
TASK [letsencrypt : Install Let's Encrypt client] ****
changed: [dev.rtfm.co.ua]
TASK [letsencrypt : Stop NGINX] ****
changed: [dev.rtfm.co.ua]
TASK [letsencrypt : Obtain certificates] ****
changed: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'dev.rtfm.co.ua'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'rtfm'}, u'test.rtfm.co.ua'))
changed: [dev.rtfm.co.ua] => (item=({u'name': u'setevoy'}, u'dev.test.setevoy.kiev.ua'))
...
TASK [letsencrypt : Start NGINX] ****
changed: [dev.rtfm.co.ua]
TASK [letsencrypt : Add Let's Encrypt cronjob for cert renewal] ****
changed: [dev.rtfm.co.ua]
PLAY RECAP ****
dev.rtfm.co.ua : ok=7 changed=4 unreachable=0 failed=0
Provisioning done.
[/simterm]
Обновляем шаблоны NGINX.
Т.к. на Dev доступ ограничен с помощью ngx_http_access_module и директив allow/deny — то надо обновить шаблоны, и разрешить доступ к .well_known.
Редактируем nginx/templates/dev/dev.rtfm.co.ua.conf.j2:
server {
listen 80;
server_name {{ item.1 }};
server_tokens off;
location ~ /.well-known {
allow all;
}
location / {
allow {{ office_allow_location }};
allow {{ home_allow_location_provider1 }};
allow {{ home_allow_location_provider2 }};
deny all;
return 301 https://{{ item.1 }}$request_uri;
}
}
server {
listen 443 ssl;
server_name {{ item.1 }};
root {{ web_data_root_prefix }}/rtfm/{{ item.1 }};
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains" always;
server_tokens off;
access_log /var/log/nginx/{{ item.1 }}-access.log main_ext;
error_log /var/log/nginx/{{ item.1 }}-error.log warn;
ssl_certificate /etc/letsencrypt/live/{{ item.1 }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ item.1 }}/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/dhparams.pem;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
allow {{ office_allow_location }};
allow {{ home_allow_location_provider1 }};
allow {{ home_allow_location_provider2 }};
deny all;
...
Обновляем остальные конфиги, деплоим их на хост.
Копирование блога
Эту задачу можно тоже автоматизировать в Ansible, но лучше пока руками.
Копируем файлы:
[simterm]
root@rtfm-dev:/home/admin# scp -r [email protected]:/data/rtfm.co.ua/* /data/www/rtfm/dev.rtfm.co.ua/
[/simterm]
Создаём дамп базы данных:
[simterm]
14:30:48 [setevoy@rtfm-prod-current ~] $ mysqldump -h 172.31.64.60 -u setevoy -p rtfm_db1 > rtfm_db1.sql
[/simterm]
Копируем его на Dev:
[simterm]
root@rtfm-dev:/home/admin# scp [email protected]:/home/setevoy/rtfm_db1.sql .
[/simterm]
Заливаем в базу rtfm_dev:
[simterm]
root@rtfm-dev:/home/admin# mysql -u rtfm -p rtfm_dev < rtfm_db1.sql
[/simterm]
Обновляем домен в базе (тут префикс таблиц db1_ вместо дефолтного wp_):
[simterm]
MariaDB [rtfm_dev]> UPDATE db1_options SET option_value = replace(option_value, 'https://rtfm.co.ua', 'https://dev.rtfm.co.ua') WHERE option_name = 'home' OR option_name = 'siteurl'; Query OK, 2 rows affected (0.00 sec) MariaDB [rtfm_dev]> UPDATE db1_posts SET guid = replace(guid, 'https://rtfm.co.ua', 'https://dev.rtfm.co.ua'); Query OK, 6209 rows affected (0.20 sec) MariaDB [rtfm_dev]> UPDATE db1_posts SET post_content = replace(post_content, 'https://rtfm.co.ua', 'https://dev.rtfm.co.ua'); Query OK, 3312 rows affected (1.58 sec) MariaDB [rtfm_dev]> UPDATE db1_postmeta SET meta_value = replace(meta_value, 'https://rtfm.co.ua', 'https://dev.rtfm.co.ua'); Query OK, 1482 rows affected (0.26 sec)
[/simterm]
Обновляем параметры в /data/www/rtfm/dev.rtfm.co.ua/wp-config.php — DB_NAME, DB_USER, DB_PASSWORD, DB_HOST.
Проверяем:
Так… Ну — почти… Что-то с плагином sitemap-generator — он реально древний, и для PHP-7 не заточен. Да и виджет плагина Rating Widget тоже выдаёт варнинги, и тоже из-за переезда PHP 5 => 7 — надо пообщаться с их саппортом.
Но в целом — осталось добавить остальные роли, типа exim, amplify-agent, logrotate — и всё готово к миграции.










