На пути к инфраструктуре у каждого наступает момент, когда вам нужно создавать собственные AMI AWS. Вместо того, чтобы настраивать свои экземпляры каждый раз при подготовке одного из них, вы создаете образ, который предварительно загружен:

  • Необходимые приложения / инструменты / скрипты
  • Пропатчен всеми последними пакетами
  • Повышение безопасности ОС
  • Информация о вашей среде, чтобы она могла подключиться к сети и немедленно начать работу.
  • так далее…

Я не думаю, что должен кому-то слишком сильно рассказывать, почему все вышеперечисленное приятно иметь. Это также экономит ваше время и усилия, поскольку вместо этого экземпляра PER он выполняется только один раз при создании самого изображения.

Еще одно преимущество пользовательских изображений - это концепция «домашние животные» и «овцы». Когда ваши изображения предварительно загружены со всем, что вам нужно, вы можете уничтожить и восстановить свои серверы по своему усмотрению. Ваши серверы становятся овцами, которых можно менять по мере необходимости. Это можно сравнить с наличием серверов, которые необходимо реконфигурировать каждый раз при их создании. Вместо того, чтобы уничтожать серверы / домашних животных по мере необходимости, вы должны позаботиться о них и обо всех связанных с этим трудностях (например, постоянное исправление, обновление, усиление защиты по соображениям безопасности и т. Д.)

Честно говоря, вы можете уменьшить боль ванильных изображений, используя что-то вроде Puppet, Nomad или Ansible. Проблема с этим подходом в том, что эти инструменты запускаются во время сборки. В результате при загрузке сервера этим инструментам потребуется 10–20 минут на его настройку. По сравнению с настраиваемым изображением, готовым мгновенно или через минуту или две, чтобы его можно было использовать.

Теперь, когда мы понимаем некоторые проблемы и преимущества, нам нужно спроектировать, как мы хотим создавать наши собственные изображения.

При создании пользовательских изображений, как и при любой автоматизации, мы хотим убедиться, что принимаем разумные решения. Нам нужно произвести что-то такое:

  • Легко использовать
  • Возможность автоматизации
  • Масштабируемый
  • Легко обновить
  • Безопасный
  • СУХОЙ - Уменьшение / удаление необходимости копировать и вставлять код

Вот схема того, как это выглядит для наших пользовательских изображений:

  • Мы создадим базовый образ, который будет иметь все общие зависимости, необходимые для всех типов серверов (например, исправления, инструменты мониторинга, усиление защиты ОС).
  • Все остальные образы будут использовать базовые образы в качестве отправной точки, а затем будут использовать любые дополнительные инструменты / приложения / конфигурации, необходимые для образа для конкретной функции (например, серверы-бастионы, узлы Kubernetes).
  • Все изображения / AMI будут размещены в центральной учетной записи AWS. Из центральной учетной записи изображения будут переданы другим нашим учетным записям AWS. Это позволяет нам управлять образами из одной учетной записи, сокращает время подготовки, помогает нам оставаться СУХИМИ и в целом упрощает использование / управление этой настройкой.
  • Упаковщик будет использован для создания образа
  • Ansible будет использоваться для настройки образа
  • Terraform загрузит экземпляры, созданные из образа, во время сборки.
  • Изображения не будут содержать секретов / API-ключей / паролей. Они будут предоставлены Terraform как часть процесса начальной загрузки. Это более безопасно, а также делает изображения более расширяемыми.

Packer, Ansible и Terraform будут описаны в отдельных разделах. Я также включу код или ссылки, которые можно использовать для начала работы с пользовательскими изображениями, в «Приступим к строительству!» раздел.

Упаковщик

Для тех, кто не знаком, Packer является частью семейства Hashicorp (так что вы знаете, что это будет потрясающе). Он автоматизирует создание образов машин с помощью:

  • создание временного экземпляра
  • настройка временного экземпляра в соответствии с предоставленными вами инструкциями
  • создание образа из временного экземпляра
  • завершение временного экземпляра

Примечание. Начиная с Packer v1.5.0, в Packer была добавлена ​​поддержка HCL. Все приведенные ниже примеры будут в HCL. Если у вас более старая версия Packer и вы не хотите обновляться, вам нужно будет преобразовать примеры в соответствующий JSON. Этот документ может помочь вам перейти с JSON на HCL.

Ansible

Ansible - это инструмент настройки сервера. Подумайте о таких инструментах, как Puppet, Puppet Bolt, Chef, Salt и т. Д., И вы будете в одном и том же состоянии. Он позволяет настраивать серверы с помощью кода. Одним из пунктов маркированного списка в разделе «Упаковщик» было:

настройка временного экземпляра в соответствии с предоставленными вами инструкциями

При настройке образов с помощью Packer вы используете так называемые средства обеспечения. Взглянув на документацию Packer, вы увидите, что есть много вариантов:

Мы предпочитаем использовать «Ansible», поскольку у нас уже есть кодовая база Ansible для настройки / обслуживания наших существующих «домашних животных». Если вы уже используете Chef, Puppet, Salt и т. Д., Я настоятельно рекомендую вам использовать соответствующий Provisioner. Это позволит вам повторно использовать существующий код и использовать уже имеющиеся у вас наборы навыков.

Единственное предостережение в том, что я бы рекомендовал избегать любых средств обеспечения, таких как «Shell» или «Windows Shell», для чего-либо обширного. Хотя может возникнуть соблазн написать быстрый сценарий bash или его аналог, он просто не масштабируется и в конечном итоге становится слишком сложным для управления.

Например, я смог сделать свои изображения более жесткими, используя эту роль Ansible. Представьте, сколько строк в bash потребовалось бы, чтобы управлять одним и тем же? Кроме того, даже если вы написали bash для соответствия этой роли, вам придется поддерживать все эти сценарии, чтобы убедиться, что они соответствуют передовым практикам. Использование роли Ansible заняло всего 5 минут и поддерживается поставщиком. Сценарий bash занял бы часы, а затем добавил бы дополнительные накладные расходы, так как мне пришлось бы их поддерживать.

Как вы увидите в примере кода ниже, мы также используем Inspec. Этот провайдер позволяет нам проверить, правильно ли защищены наши изображения. Добавление проверочного тестирования к вашей автоматизации - это всегда хорошо. В этом случае ваша команда Secops будет признательна.

Terraform

Как упоминалось ранее, мы не собираемся хранить в наших изображениях какие-либо секреты / пароли / API-ключи. Это желательно по нескольким причинам, но именно по соображениям безопасности и возможности настройки. Однако с этим требованием нам нужно «что-то» для предоставления секретов / конфигураций серверам после их предоставления. Поскольку Terraform отвечает за работу наших серверов, имеет смысл использовать его для предоставления необходимой информации во время сборки.

Хотя Terraform играет очень важную роль в этом процессе, его роль довольно мала и прямолинейна. Когда вы инициализируете экземпляр, AWS и другие поставщики предоставляют вам возможность указать «Данные пользователя».

Когда вы запускаете инстанс в Amazon EC2, у вас есть возможность передавать пользовательские данные в инстанс, которые можно использовать для выполнения общих задач автоматической настройки и даже запуска скриптов после запуска инстанса.

Используя этот механизм, мы планируем заставить Terraform делать две простые вещи:

  • Создайте сценарий bash, содержащий все конфигурации / секреты, необходимые серверу.
  • Выполните сценарий установки, который загрузит секреты и настроит сервер.

Теперь, когда мы рассмотрели дизайн и все основные компоненты, можно приступить к работе!

Приступаем к строительству!

Для начала мы собираемся создать каталог «packer» и два подкаталога с именами «base-image» и «assets».

mkdir -p packer/{assets,base-image}

В будущем, когда мы будем создавать дополнительные изображения, они будут просто добавляться сюда, в свои собственные папки. Каталог «assets» будет использоваться для хранения зависимостей, таких как наш код Ansible. Эта структура упростит организацию наших файлов, а также упростит автоматизацию с помощью CI / CD.

В каталоге base-image мы создадим два файла:

  • variables.pkr.hcl - Как вы можете догадаться, здесь будут храниться наши переменные
  • base.pkr.hcl - это файл, в котором мы определяем, как мы хотим, чтобы наше изображение было построено.

Примечание. Имя этих файлов не имеет значения для Packer, но они упрощают организацию содержимого.

переменные.pkr.hcl

Использовать переменные в Packer очень просто. Наш файл переменных будет выглядеть примерно так:

variable "ami-description" {
  type = string
  default = "My custom Ubuntu Image"
}
variable "aws_access_key" {
  type = string
  default = ""
}
variable "aws_secret_key" {
  type = string
  default = ""
}
variable "aws_profile" {
  type = string
  default = "myAWSProfile"
}
variable "aws_acct_list" {
  type = list(string)
  default = [
    #acctA
    "000000000000",
    #acctB
    "111111111111"
}
variable "destination_regions" {
  type = list(string)
  default = [
    "us-west-1",
    "us-west-2"]
}
variable "fmttime" {
  type = string
  default = "{{isotime \"2006-01-02-150405\"}}"
}
variable "source_image_name" {
  type = string
  default = "ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-"
}
variable "ssh_user" {
  type = string
  default = "ubuntu"
}

После определения на переменные можно легко ссылаться в Packer, используя формат ${varnamehere}. В этом конкретном примере я хочу указать на несколько вещей:

  • Мы определяем aws_profile. Секретный ключ и ключ доступа намеренно определены как пустые. Эта конфигурация будет использовать ваш профиль AWS для аутентификации. Если вместо этого вы хотите использовать ключи доступа / секретные ключи, опустите переменную профиля.
  • Переменная «aws_acct_list» будет использоваться для того, чтобы сообщить Пакеру обо всех учетных записях, с которыми должен быть предоставлен доступ к нашему изображению.
  • Список «destination_regions» сообщит Packer, в каких регионах должно быть доступно мое изображение.

Как упоминалось ранее, все вышеперечисленное позволит нам управлять нашими изображениями из централизованной учетной записи AWS, обеспечивая при этом доступ из наших дополнительных учетных записей.

base.pkr.hcl - исходный раздел

Файл base.pkr.hcl определит образ, который мы хотим создать, и то, как мы хотим его настроить.

Мы начинаем наш файл с указания раздела source. Этот раздел будет содержать всю информацию, необходимую для создания временного экземпляра. Вторая и последняя часть этого файла - это раздел build. Эта часть кода позволит Packer узнать, как следует настроить временный образ.

Вот исходный раздел целиком. Будет легче пройти через то, к чему мы стремимся, если вы сможете увидеть всю картину целиком.

source "amazon-ebs" "example" {
  ami_name = "custom/ubuntu-${var.fmttime}"
  ami_description = "${var.ami-description}"
  ami_users = "${var.aws_acct_list}"
  access_key = "${var.aws_access_key}"
  secret_key = "${var.aws_secret_key}"
  profile = "${var.aws_profile}"
  region = "us-west-1"
  instance_type = "t3.small"
  ami_regions = "${var.destination_regions}"
  associate_public_ip_address = true
  communicator = "ssh"
  ssh_username = "${var.ssh_user}"
vpc_filter {
    filters = {
      "tag:Name": "myVPC",
      "isDefault": "false"
    }
  }
  subnet_filter {
    filters = {
      "state": "available",
      "tag:Name": "*public*"
    }
    random = true
  }
source_ami_filter {
    filters = {
      name = "${var.source_image_name}*"
      virtualization-type = "hvm"
      root-device-type = "ebs"
    }
    owners = [
      "099720109477"]
    most_recent = true
  }
run_tags = {
    OS_Version = "Ubuntu"
  }
tags = {
    OS_Version = "Ubuntu"
    Name = "custom/ubuntu-${var.fmttime}"
  }
}

Большинство этих настроек говорят сами за себя. Мы делаем такие вещи, как присвоение имени изображению, предоставление описания, определение тегов и т. Д. Имя изображения будет содержать временную метку и выглядеть примерно как custom/ubuntu-2020-01-20-185613. Такое соглашение об именах гарантирует, что все изображения уникальны и их можно будет сортировать.

Несколько настроек, которые мы должны расширить:

ami_users используется, чтобы сообщить Packer, какие учетные записи AWS должны иметь доступ к вашему пользовательскому образу. Это связано с переменной aws_acct_list, которую мы определили ранее.

variable "aws_acct_list" {
  type = list(string)
  default = [
    #acctA
    "000000000000",
    #acctB
    "111111111111"
}

Если у вас только одна учетная запись AWS, вы можете опустить этот параметр.

aws_regions определяет, в какие регионы следует скопировать полученный AMI. Если вы работаете только в одном регионе, вы можете опустить эту переменную. Даже если вы находитесь в одном регионе, я все равно рекомендую оставить это на месте, чтобы в сценарии аварийного восстановления ваши образы были готовы и ожидали.

Фильтры VPC и Subnet используются для определения VPC и подсети, в которой будет создан временный экземпляр. Эти значения можно либо жестко запрограммировать, либо вы можете создать фильтр для их динамического поиска. Вы также можете пропустить этот раздел, если у вас есть VPC по умолчанию в AWS. Мы решили использовать фильтр на случай, если когда-либо повторно подготовили наши VPC:

vpc_filter {
  filters = {
    "tag:Name": "myVPC",
    "isDefault": "false"
  }
}
subnet_filter {
  filters = {
    "state": "available",
    "tag:Name": "*public*"
  }
  random = true
}

Мы собираемся сопоставить тег AWS «Name» как для VPC, так и для подсети. Примечание. Вам необходим SSH-доступ к временному экземпляру, поэтому убедитесь, что вы правильно определили свой VPC / подсеть.

Исходный AMI - это образ, с которым будет запущен временный экземпляр. Мы хотим убедиться, что у нас есть последний образ Ubuntu, и использование source_ami_filter позволяет нам это сделать.

source_ami_filter {
  filters = {
    name = "${var.source_image_name}*"
    virtualization-type = "hvm"
    root-device-type = "ebs"
  }
  owners = [
    "099720109477"]
  most_recent = true
}

Нам нужен образ, который начинается с ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-, нам нужен последний образ, и этот образ должен быть получен из учетной записи Ubuntu AWS (номер учетной записи: 099720109477).

При таком подходе каждый раз, когда Ubuntu выпускает новый образ, наш процесс сборки будет автоматически его использовать.

base.pkr.hcl - раздел сборки

После создания временного экземпляра с использованием информации из раздела source Packer настроит экземпляр, используя шаги, указанные в разделе build.

build {
  sources = [
    "source.amazon-ebs.example"
  ]
provisioner "ansible" {
    user = "${var.ssh_user}"
    playbook_file = "../assets/ansible/provision-base-server.yml"
    extra_arguments = [ "--extra-vars", "os_ignore_users: [\"${var.ssh_user}\"] os_filesystem_whitelist: [\"squashfs\"]" ]
  }
//  provisioner "inspec" {
//    inspec_env_vars = [ "CHEF_LICENSE=accept"]
//    profile = "https://github.com/dev-sec/linux-baseline"
//  }
}

Поскольку большая часть нашей конфигурации исходит от Ansible, наш раздел сборки довольно прост. Мы говорим Пакеру запустить наш сборник сценариев Ansible. Я также включил код для Inspec Provider для справки, но оставил его закомментированным. Напоминаем, что Inspec проверит, что ваше изображение было усилено. Это работает в тандеме с ansible-role, которую мы используем для усиления защиты наших серверов. Мы закомментировали это, так как наши пользовательские настройки приводят к сбою ванильной проверки, поэтому в будущих итерациях он будет изменен.

Это все, что вам нужно со стороны упаковщика.

Ansible

Как упоминалось ранее, Ansible позволяет настраивать серверы в автоматическом режиме. Я не буду углубляться в этот раздел, поскольку пользовательские изображения по самому своему определению должны быть адаптированы к вашему варианту использования. Я хочу выделить несколько важных шаблонов, а также дать вам представление о моем подходе, который вы можете использовать для начала. Плейбук, названный Пакером, выглядит примерно так:

- hosts: all
  become: yes
  become_user: root
  become_method: sudo
roles:
    - { role: update-pkgs, tags: ["updatepkgs"] }
    - { role: install-monitoring, tags: ["monitoring"] }
    - { role: aws-inspector, tags: ["inspector"] }
    - { role: install-secops-tool, tags: ["secops"] }
    - { role: dev-sec.os-hardening, tags: ["os-hardening"]}
    - { role: haveged, tags: ["haveged"] }
    - { role: post-provisioner, tags: ["post"] }

Мы обновляем пакеты на сервере, устанавливаем инструменты мониторинга / безопасности, укрепляем образ и устанавливаем «hasged» для генерации энтропии на серверах (что также является вопросом безопасности).

Если вы не знакомы с Ansible или хотите получить представление о том, что я делаю в этих ролях, вот пример одного из них.

---
- name: Ubuntu | Upgrade all current packages
  apt:
    update_cache: true
    upgrade: dist
    allow_unauthenticated: true
  when: ansible_os_family in ['Debian', 'Ubuntu']
- name: Ubuntu | Install Unattended-upgrades
  apt:
    update_cache: true
    name: unattended-upgrades
    state: present
  when: ansible_os_family in ['Debian', 'Ubuntu']
- name: Ubuntu | Configure Unattended-upgrades
  copy:
    src: 50unattended-upgrades.conf
    dest: /etc/apt/apt.conf.d/50unattended-upgrades
    owner: root
    group: root
    mode: 0644
  when: ansible_os_family in ['Debian', 'Ubuntu']
- name: Ubuntu | Configure Unattended-upgrades Timing
  copy:
    src: 20auto-upgrades.conf
    dest: /etc/apt/apt.conf.d/20auto-upgrades
    owner: root
    group: root
    mode: 0644
  when: ansible_os_family in ['Debian', 'Ubuntu']

Этот пример взят из роли пакетов обновлений. По сути, он использует apt-get, чтобы убедиться, что все пакеты обновлены. Затем мы также устанавливаем и настраиваем «автоматические обновления», чтобы сервер периодически обновлялся автоматически.

Главное, на чем я хочу сосредоточиться в Ansible, - это роль «пост-провайдера». Как упоминалось ранее, мы не хотим хранить конфиденциальную информацию на этих изображениях. Пост-инициатор - это механизм, который позволяет Terraform вводить секреты во время сборки и загружать наши серверы.

При создании этой роли мы не хотели тесно связывать Terraform с нашими изображениями. Это означает, что Terraform не должен знать 20 скриптов, необходимых для настройки образа Y, или 10 скриптов, необходимых для настройки образа X. Вместо этого Terraform должен просто запустить один скрипт, а все остальное произойдет автоматически.

Этот единственный сценарий - наш install.sh:

#!/usr/bin/env bash
for f in /opt/custom/postbuild-scripts/*
do
  if [ ! -d "$f" ]; then
   echo "Executing - $f"
    ${f}
  fi
done

Как видите, сценарий довольно прост, он перебирает все сценарии в каталоге /opt/custom/postbuild-scripts/* и выполняет их. Если бы мы хотели, мы также могли бы добавить в этот скрипт дополнительные проверки / логику / инструменты (что является еще одним преимуществом централизованного скрипта).

Поставщик сообщений отвечает за копирование сценариев установки и публикации в образ. Прелесть этого подхода в том, что когда вы создаете новые изображения из базового образа, вы можете добавлять в этот каталог дополнительные скрипты. Возможно, в вашем базовом образе 5 скриптов, а затем в образ-бастион добавятся еще 5. Terraform выполняет только один скрипт, и его не волнует, есть ли там 10, 20 или 1000 скриптов.

Сценарии в каталоге postbuild-scripts также довольно просты, поскольку Ansible уже проделал большую работу.

#!/usr/bin/env bash
set -euo pipefail
source /opt/custom/vars.sh
sed -i "s/ENTERLICENSEKEY/${monitoring_license_key}/" /etc/monitoring.yml
systemctl restart monitoring-agent

Все наши сценарии исходят из vars.sh сценария, который содержит всю информацию, необходимую для настройки сервера. Этот сценарий создается Terraform перед запуском install.sh. Мы скоро расскажем, как создается этот файл.

Создание имиджа

Установив Ansible и Packer, вы можете официально создать свой первый образ! Эта часть довольно проста. Из каталога базового образа вы можете запустить packer build .. Процесс сборки займет некоторое время, чтобы сделать свою работу. По крайней мере, вы можете рассчитывать на 10–15 минут, это может быть больше, в зависимости от того, что вы делаете на своем сервере.

У вас официально есть собственное изображение!

Terraform

Мы готовы на 99%, и теперь нам просто нужно запустить и загрузить экземпляр с нашим новым образом.

К настоящему времени вы должны были понять, что я не люблю жестко кодировать что-то, если я могу этого избежать. Первым шагом в Terraform является создание ресурса данных, который будет искать для меня AMI ID.

data "aws_ami" "base_ami" {
  most_recent      = true
  owners           = ["111111111111"]
filter {
    name   = "name"
    values = [var.base_image_name]
  }
}

В поле owners следует указать номер учетной записи AWS, в которой размещены ваши изображения. Затем мы создаем пользовательский шаблон, который передает необходимые нам секреты.

data "template_file" "base_user_data" {
  template = file("${path.module}/templates/base.userdata.sh.tpl")
  vars = {
    tool1_base_dn          = var.tool1_base_dn
    tool1_bind_user        = var.tool1_bind_user
    tool1_bind_passwd      = var.tool1_bind_passwd
    tool1_api_key          = var.tool1_api_key
    hostname               = "${var.cluster_name}-bastion"
    monitoring_license_key = var.monitoring_key
  }
}

В указанном выше шаблоне мы создаем vars.sh и запускаем install.sh.

#!/bin/bash
set -euo pipefail
# pull all the tf passed in vars and put into a file for other scripts to source
cat > /opt/custom/vars.sh <<'EOF'
#!/bin/bash
export tool1_base_dn="${tool1_base_dn}"
export tool1_bind_user="${tool1_bind_user}"
export tool1_bind_passwd="${tool1_bind_passwd}"
export tool1_api_key="${tool1_api_key}"
export hostname="${hostname}"
export monitoring_license_key="${monitoring_license_key}"
EOF
/opt/custom/install.sh

Наконец, когда вы определяете свой экземпляр, передайте user_data, и все готово!

resource "aws_instance" "example" {
  ami           =  data.aws_ami.base_ami.id
  instance_type = "t3.small"
  key_name      = var.base_ssh_key
  subnet_id     = aws_subnet.public_a.id
  vpc_security_group_ids = [
  aws_security_group.example.id]
user_data =  base64encode(data.template_file.base_user_data.rendered)
}

Заключение

Как уже упоминалось ранее, использование настраиваемых изображений дает множество преимуществ, но только в том случае, если вы можете автоматизировать их и в полной мере использовать их. Если вы прошли весь этот процесс, у вас должна быть прочная отправная точка для начала вашего путешествия. Следующим шагом будет добавление этого к вашему CI / CD и начало создания дополнительных образов, которые используют базовый образ в качестве источника. Вещи улучшаются, и небо - это предел!