Создание базового образа AMI при помощью Packer, настройка его окружения через Ansible и безболезненная передача параметра ami_id в Terraform для деплоя инстанса
Дано: три классических окружения dev, stg и prd.
Воркфлоу: Terraform для деплоя инстансов, сетей и прочих лоад балансеров на AWS. Затем плейбуками Ansible разворачивается обвязка внутри инстансов для обеспечения работы и мониторинга приложения. И, наконец, деплой самого приложения тем же анзиблом.
Задача: создать AMI, чтобы деплоить уже готовый образ и не конфигурировать само приложение после создания инстанса, исключив второй и третий шаг из текущего воркфлоу.
Трудность: существует два вида Ansible тасков. Те, которые зависят от имени/IP адреса/etc текущего хоста при применении плейбука и те, которые не зависят. Назову первые ИмяЗависимыми. Идея в том, что надо найти способ применять эти таски не меняя логику работы плейбуков.
Новый воркфлоу: Packer создает AMI, Ansible конфигурирует окружение внутри образа, Terraform разворачивает инстансы используя новый образ и далее функцией cloud-init вызывает ansible-playbook с применением только для ИмяЗависимых тасков, которые определяются при помощи тега.
Ansible
Структура каталогов Ansible/Packer репы:
. ├── ansible │ ├── group_vars │ │ ├── all │ │ ├── dev │ │ ├── nonprd │ │ ├── prd │ │ └── stg │ ├── inventory │ │ ├── dev │ │ ├── prd │ │ └── stg │ ├── roles │ │ ├── role_1 │ │ │ ├── tasks │ │ │ │ └── main.yml │ │ │ └── templates │ │ │ ├── name_dependent_template.j2 │ │ │ └── name_not_dependent_template.j2 │ │ ├── role_2 │ │ │ └── ... │ │ └── role_n │ │ └── ... │ ├── playbook_1.yml │ ├── playbook_2.yml │ └── playbook_n.yml └── packer └── ami.json
Плейбук выглядит обычно, за исключением переменной {{ target }}
, которая создана для возможности определения хоста/группы при помощи аргумента --extra-vars
, файл ansible/playbook_1.yml
:
--- - name: Run my playbook hosts: "{{ target }}" gather_facts: True user: "ubuntu" become: true roles: - role: role_1 - role: role_2
ubuntu
- дефолтный существующий пользователь для базовых AMI с Ubuntu.
Роль тоже вполне обычная, но только для ИмяЗависимых тасков определен тег name_dependent_config
, по которому будет запускаться анзибл на шаге деплоя инстанса из кастомного AMI, файл ansible/roles/role_1/tasks/main.yml
:
--- - name: name dependent config template: src=name_dependent_template.j2 dest=/etc/app/name_dependent_template.conf tags: - name_dependent_config - name: name NOT dependent config template: src=name_not_dependent_template.j2 dest=/etc/app/name_not_dependent_template.conf
Внутри темплейта могут быть любые ИмяЗависящие переменные, такие как {{ ansible_default_ipv4.address }}
, {{ inventory_hostname }}
и любые другие из фактов.
Packer
Конфиг для создания образа, файл ami.json
:
{ "variables": { "user": "ubuntu" }, "builders": [{ "type": "amazon-ebs", "access_key": "{{ user `aws_access_key` }}", "secret_key": "{{ user `aws_secret_key` }}", "region": "us-east-1", "source_ami": "ami-a4dc46db", "instance_type": "t2.micro", "ssh_username": "{{ user `user` }}", "ami_name": "{{ user `env` }}-ami", "force_deregister": true, "force_delete_snapshot": true, "tags": { "Name": "{{ user `env` }}" } }], "provisioners": [{ "type": "shell", "inline": [ "sudo apt-get update", "sudo apt-get install -y software-properties-common", "sudo apt-add-repository ppa:ansible/ansible", "sudo apt-get update", "sudo apt-get install -y ansible" }, { "type": "ansible-local", "staging_directory": "/home/{{ user `user` }}/ansible", "playbook_dir": "../ansible", "group_vars": "../ansible/group_vars", "playbook_files": [ "../ansible/playbook_1.yml", "../ansible/playbook_2.yml" ], "extra_arguments": [ "--extra-vars 'target=127.0.0.1'" ], "inventory_groups": "{{ user `env` }},{{ user `ansible_groups` }}" } ] }
Опишу только важные для задачи параметры, остальные легко гуглятся в рунете.
force_deregister
и force_delete_snapshot
- удалять старые AMI и снапшоты после очередного запуска packer build
, действуй на свое усмотрение. В любом случае, в терраформе есть параметр most_recent
, который берет последний из созданных AMI, если фильтру соответствуют несколько.
"Name": "{{ user `env` }}"
- присваиваемые теги вновь созданному AMI для идентификации его в Terraform'е
Provisioners: shell
- запускает произвольные команды на хосте. В данном случае устанавливается ansible
staging_directory
- путь, куда при сборке образа копируется весь каталог ансибла и там же остается при поднятии инстанса. Необходим, чтобы после создания инстанса терраформом, запустить cloud-init скрипт для ИмяЗависимых тасков. Если не указать, то по дефолту будет /tmp каталог, который, как известно, очищается при перезагрузке ОС.
playbook_dir
- путь до каталога с плейбуками ansible, который копируется в staging_directory
playbook_files
- путь до плейбуков, который Packer передаст для выполнения в Ansible
extra_arguments
- параметры для Ansible, которые Packer передаст при запуске каждого плейбука
inventory_groups
- указать группы, если в Ansible существуют group_vars
зависимые переменные. Packer внесет хост в эти группы для корректного определения group_vars.
Создать образ можно командой, выполняется для каждого окружения:
packer build -var 'env=dev' -var 'ansible_groups=nonprd' -var 'aws_access_key=dev_aws_access_key' -var 'aws_secret_key=dev_aws_secret_key' ami.json packer build -var 'env=stg' -var 'ansible_groups=nonprd' -var 'aws_access_key=stg_aws_access_key' -var 'aws_secret_key=stg_aws_secret_key' ami.json packer build -var 'env=prd' -var 'ansible_groups=prd' -var 'aws_access_key=prd_aws_access_key' -var 'aws_secret_key=prd_aws_secret_key' ami.json
После выполнения создадутся новые AMI с именами dev-ami, stg-ami, prd-ami и тегами dev, stg, prd, соответственно.
На данном этапе, при создании образа, Ansible запускает оба вида тасков.
Terraform
Определение AMI ID
Самый простой способ узнать ID нового образа для регистрации в качестве переменной Terraform'а - создать data ресурс с фильтрацией по заранее созданным тегам в Packer'е:
variable "env" { default = "dev" } data "aws_ami" "custom_ami" { filter { name = "state" values = ["available"] } filter { name = "tag:Name" values = ["${var.env}"] } most_recent = true }
И указать переменную custom_ami
при создании инстанса:
resource "aws_instance" "my_instance" { ami = "${data.aws_ami.custom_ami.id}" ... }
Запуск ИмяЗависимых тасков
Для автоматического запуска анзибля будет использоваться инструмент cloud-init. Для создания и передачи скрипта, выполняющегося после поднятия инстанса, используется провайдер data.template_file
и указывается при создании инстанса как user_data
:
variable "count" { default = "4" } data "template_file" "my_instance" { template = "${file("init.tpl")}" count = "${var.count}" vars { hostname = "${format("${var.env}-%02d", count.index)}" env = "${var.env}" } } resource "aws_instance" "my_instance" { ami = "${data.aws_ami.custom_ami.id}" count = "${var.count}" ... user_data = "${element(data.template_file.my_instance.*.rendered, count.index)}" }
Переменные hostname
и env
необходимо указывать именно так, для корректной интерполяции в значения из темплейта.
Установка хостнейма и запуск плейбуков только с определенными тегами (таски без этого тега выполняться не будут) описаны в файле init.tpl
:
#!/bin/bash echo ${hostname} > /etc/hostname echo '127.0.1.1 ${hostname}' >> /etc/hosts hostname ${hostname} sudo su ubuntu ansible-playbook /home/ubuntu/ansible/playbook_1.yml --extra-vars 'target=${hostname}' -c local -i /home/ubuntu/ansible/inventory/${env} --tags "name_dependent_config" ansible-playbook /home/ubuntu/ansible/playbook_2.yml --extra-vars 'target=${hostname}' -c local -i /home/ubuntu/ansible/inventory/${env} --tags "name_dependent_config" sudo apt purge ansible sudo rm -R /etc/ansible/ rm -R /home/ubuntu/ansible/ sudo reboot
Опционально, создание пользователя с необходимыми правами для сборки образа.
# Create User packer resource "aws_iam_user" "packer" { name = "packer" path = "/system/" } resource "aws_iam_access_key" "packer" { user = "${aws_iam_user.packer.name}" } # Packer user access for creave AMI resource "aws_iam_user_policy" "packer" { name = "Packer_access_for_creave_AMI" user = "${aws_iam_user.packer.name}" policy = <<EOF { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action" : [ "ec2:AttachVolume", "ec2:AuthorizeSecurityGroupIngress", "ec2:CopyImage", "ec2:CreateImage", "ec2:CreateKeypair", "ec2:CreateSecurityGroup", "ec2:CreateSnapshot", "ec2:CreateTags", "ec2:CreateVolume", "ec2:DeleteKeypair", "ec2:DeleteSecurityGroup", "ec2:DeleteSnapshot", "ec2:DeleteVolume", "ec2:DeregisterImage", "ec2:DescribeImageAttribute", "ec2:DescribeImages", "ec2:DescribeInstances", "ec2:DescribeRegions", "ec2:DescribeSecurityGroups", "ec2:DescribeSnapshots", "ec2:DescribeSubnets", "ec2:DescribeTags", "ec2:DescribeVolumes", "ec2:DetachVolume", "ec2:GetPasswordData", "ec2:ModifyImageAttribute", "ec2:ModifyInstanceAttribute", "ec2:ModifySnapshotAttribute", "ec2:RegisterImage", "ec2:RunInstances", "ec2:StopInstances", "ec2:TerminateInstances" ], "Resource" : "*" }] } EOF }