Прошёл почти год, как я начал миграцию 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
– и всё готово к миграции.