Packer -> (Ansible) —> Terraform -> (Ansible) или деплой без гемора и смс

Автор: Admin | 01.11.2018

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *