Создание базового образа 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
}
