[Terraform + SaltStack] Готовим PrestoDB кластер в скороварке (Часть #1)
Что здесь интересного?
Рецепт приготовления вкусного и полезного PrestoDB кластера используя скороварку на базе Terraform и SaltStack в публичном облаке AWS. Рассмотрим подробно нюансы подготовки к работе самой скороварки, необходимые шаги для правильного приготовления самого блюда и, естественно, немножко расскажем о потреблении готового блюда. Эту часть можно использовать как учебный материал по Terraform.
итак, приступим:
Ингредиенты для рецепта
- Terraform — 1 шт.
- SaltStack — 1 мастер и 1+ миньен
- PrestoDB — 1 координатор и 1+ воркер
- AWS аккаунт — 1 шт.
- Смекалка и напильник — по вкусу
Рассмотрим ингредиенты подробнее: (без правил их приготовления)
1. Terraform — Замечательный инструмент от парней из Hashicorp (они же сделали такие, весьма полезные, штуки как Vagrant, Consul, Packer, Vault и др.) используемый для создания и модификации инфраструктур в различных облачных и не только окружениях.
2. SaltStack — Инструмент для автоматизированного конфигурирования и настройки серверов. Ваш покорный слуга уже писал об этом здесь и тут.
3. PrestoDB — Надстройка над Big Data провайдерами для возможности делать запросы к ним на родном и понятном SQL. Разработана ребятами из Facebook, которые перевели ее в статус OSS за что им огромное спасибо.
4. AWS (или любое другое публичное/приватное облако, к примеру: GCE или OpenStack) из списка поддерживаемых Terraform в котором будет в последствии работать наш PrestoDB кластер. Мы будем использовать AWS т.к. он наиболее распространен (среди public cloud платформ) и понятен многим без массы дополнительных пояснений.
5. В статье будут описаны лишь базовые принципы работы связки указанных продуктов, и некоторые хитрости для облегчения процесса, но я не буду подробно останавливаться на нюансах работы того или иного компонента — по каждому из них в принципе можно написать книгу. Потому адаптировать указанные приемы используя голову очень приветствуется. И еще — не пишите в комментариях, что что-то настроено не оптимально (в частности PrestoDB) — это не та цель которую я преследую.
Готовим скороварку!
В любом кулинарном рецепте есть умолчание о том, что сковородки и кастрюли уже готовы к приготовлению, но в нашем случае правильная подготовка скороварки (Terraform+SaltStack) является чуть-ли не на 80% залогом успешного приготовления блюда.
Итак — начнем с Terraform. Ну есть же CloudFormation для AWS или SaltCloud от создателей SaltStack так почему же выбран именно Terraform? Основная фишка Terraform в его простоте и понятном DSL — для создания инстанса (или 10ти) необходимо и достаточно такого описания (подразумеваем Terraform скачан и находится в пределах $PATH):
provider "aws" {
access_key = "XXXXXXXXXXXXXXXXXXXXX" # AWS IAM key
secret_key = "******************************************" # AWS IAM secret
region = "us-east-1" # region used to create resources
}
resource "aws_instance" "example_inst" {
ami = "ami-6d1c2007" # CentOS 7 AMI located in US-East-1
instance_type = "t2.medium"
count = "1" # or "10" can be used for parallel creation
vpc_security_group_ids = [ "default-sg" ] # some security group with at least 22 port opened
key_name = "secure_key" # pre created AWS E2 key pair
subnet_id = "sub-abcdef123" # AWS VPC subnet
}
и простой последовательности команд:
terraform plan
terraform apply
описательная часть вполне понятна и, мне кажется, не требует пояснений для тех, кто знаком с AWS. Подробнее про доступные AWS ресурсы тут. Конечно же подразумеваем, что AWS аккаунт, ключи которого указаны в Terraform конфигурации, имеет привилегии на создание необходимых ресурсов.
Собственно самое интересное кроется в вызовах самого Terraform — terraform plan — делает «прикидку» того, что необходимо сделать с последнего состояния (в нашем примере — надо создать новый инстанс) и показывает какие ресурсы будут созданы, удалены или модифицированы, apply — собственно запустит процесс создания запланированных ресурсов. В случае если Terraform уже запускался и Вы изменили конфигурацию (скажем, добавили инстансов) на этапе планирования будет показано какие недостающие ресурсы будут созданы и apply может создать недостающие.
terraform destroy
поможет полностью убрать все ресурсы созданные при помощи Terraform (учитываются находящиеся в текущем каталоге .tfstate файлы, хранящие описание состояние созданной инфраструктуры).
Важный момент, о котором не стоит забывать — terraform в большинстве случаев не будет модифицировать уже имеющиеся ресурсы — он просто удалит старые и пересоздаст заново. Это значит, к примеру, что если вы создали инстанс типа t2.medium и потом поменяли конфигурацию указав новый тип для инстанса, скажем m4.xlarge, то при запуске apply Terraform сначала уничтожит ранее созданный, а потом создаст новый. Это может показаться странным для пользователей AWS (можно было остановить инстанс, поменять его тип и запустить заново не потеряв при этом данных на диске), но это сделано для предоставления одинаково прогнозируемого поведения на всех платформах. И еще одно: Terraform не умеет (да и не должен уметь по своей природе) контролировать ресурсы во время их жизненного цикла — это значит, что Terraform не предоставляет команды типа stop или reboot для созданных с его помощью инстансов — Вы должны использовать другие средства для управления созданной инфраструктурой.
Terraform предоставляет прекрасный набор функционала доступный в своем DSL — это переменные (https://www.terraform.io/docs/configuration/variables.html), интерполяторы (необходимы для итерирования, модификации переменных), модули и т.д. Вот один из примеров использования всего этого:
# Cluster shortname
variable cluster_name { default = "example-presto" }
# Count of nodes in cluster
variable cluster_size { default = 3 }
# Default owner for all nodes
variable cluster_owner { default = "user@example.com" }
# Default AWS AMI to use for cluster provisioning
variable cluster_node_ami { default = "ami-6d1c2007" }
# Default AWS type to use for cluster provisioning
variable cluster_node_type { default = "t2.large" }
# Defualt VPC subnet
variable cluster_vpc_subnet { default = "subnet-da628fad" }
# Default Security group to apply to instances
variable cluster_sg { default = "sg-xxxxxxx" }
# Default KeyPair to use for provisioning
variable cluster_keyname { default = "secure_key" }
# Clurter worker nodes
resource "aws_instance" "worker_nodes" {
ami = "${var.cluster_node_ami}"
instance_type = "${var.cluster_node_type}"
count = "${var.cluster_size - 1}" # one node will be used for coordinator
vpc_security_group_ids = [ "${var.cluster_sg}" ]
key_name = "${var.cluster_keyname}"
subnet_id = "${var.cluster_vpc_subnet}"
disable_api_termination = true
tags {
Name = "${var.cluster_name}-cluster-worker-${format("%02d", count.index+1)}"
Owner = "${var.cluster_owner}"
Purpose = "PrestoDB cluster '${var.cluster_name}' node ${format("%02d", count.index+1)}"
}
}
тут пример использования переменных, арифметических операций над ними, интерполяция с помощью format, использование индекса текущего элемента (если создается несколько однотипных инстансов), а также тегирование ресурсов.
Но только лишь создания/уничтожения инстансов не достаточно — необходимо их еще как-то инициализировать (скопировать файлы, установить и настроить специфичный софт, обновить систему, провести конфигурацию кластера и т.д.) для этого Terraform вводит понятие Provisioners. К основным из них относятся file, remote-exec, chef и null-resource. Типичными операциями являются копирование файлов и запуск скриптов на удаленном инстансе.
Вот предыдущий пример с включенными операциями провиженинга:
# Localy stored SSH private key filename
variable cluster_keyfile { default = "~/.ssh/secure_key.pem" }
# Clurter worker nodes
resource "aws_instance" "worker_nodes" {
ami = "${var.cluster_node_ami}"
instance_type = "${var.cluster_node_type}"
count = "${var.cluster_size - 1}" # one node will be used for coordinator
vpc_security_group_ids = [ "${var.cluster_sg}" ]
key_name = "${var.cluster_keyname}"
subnet_id = "${var.cluster_vpc_subnet}"
disable_api_termination = true
tags {
Name = "${var.cluster_name}-cluster-worker-${format("%02d", count.index+1)}"
Owner = "${var.cluster_owner}"
Purpose = "PrestoDB cluster '${var.cluster_name}' node ${format("%02d", count.index+1)}"
}
# Copy bootstrap script
provisioner "file" {
source = "bootstrap-script.sh"
destination = "/tmp/bootstrap-script.sh"
connection {
type = "ssh"
user = "centos"
private_key = "${file("${var.cluster_keyfile}")}"
}
}
# Running provisioning commands
provisioner "remote-exec" {
inline = [
"yum -y update",
"sudo sh /tmp/bootstrap-script.sh"
]
connection {
type = "ssh"
user = "centos"
private_key = "${file("${var.cluster_keyfile}")}"
}
}
}
Основное замечание — указание информации о соединении к удаленному хосту — для AWS это чаще всего доступ по ключу — потому Вы должны указать где именно этот ключ лежит (для удобства была введена переменная). Обратите внимание, что атрибут private_key в секции connection не может принимать путь к файлу (только ключ текстом) — вместо этого используется интерполятор $file{} который открывает файл на диске и возвращает его содержимое.
Мы добрались до создания простого кластера состоящего из нескольких инстансов (не будем вдаваться в подробности содержимого файла bootstrap-script.sh — положим, что там прописана установка необходимого софта). Давайте рассмотрим, как в нашей скороварке делать кластера с выделенным мастером. В общем будем полагать, что worker ноды кластера должны знать где находится master нода для того, чтобы в ней зарегистрироваться и в дальнейшем получать задачи (давайте оставим всякие вкусности типа Raft и Gossip протоколы для установления мастера и распространения информации в кластере для других статей) — для простоты — положим worker должен знать ip адрес master-а. Как это реализовать в Terraform? Для начала надо создать отдельный инстанс для master-а:
resource "aws_instance" "master_node" {
ami = "${var.cluster_node_ami}"
instance_type = "${var.cluster_node_type}"
count = "1"
<...skipped...>
provisioners {
<...skipped...>
}
}
затем, добавим зависимость в worker ноды:
# Clurter worker nodes
resource "aws_instance" "worker_nodes" {
depends_on = ["aws_instance.master_node"] # dependency from master node introduced
ami = "${var.cluster_node_ami}"
instance_type = "${var.cluster_node_type}"
count = "${var.cluster_size - 1}" # one node will be used for coordinator
<...skipped...>
}
модификатор ресурса depends_on можно использовать для задания порядка выполнения задач по созданию инфрастуктур — Terraform не будет создавать worker ноды до тех пор пока не будет полностью создана master нода. Как видно из примера в качестве зависимости (тей) можно указывать список конструируемый из типа ресурса с указанием через точку его имени. В AWS Вы можете создавать не только инстансы, но и VPC, сети и т.д. — их нужно будет указывать как зависимости для использующих VPC ресурсов — это будет гарантировать правильный порядок создания.
Но, продолжим с передачей адреса master ноды всем worker нодам. Для этого Terraform предоставляет механизм ссылок на раннее созданные ресурсы — т.е. вы можете просто извлечь информацию о ip адресе master ноды в описании worker-а:
# Clurter worker nodes
resource "aws_instance" "worker_nodes" {
depends_on = ["aws_instance.master_node"] # dependency from master node introduced
ami = "${var.cluster_node_ami}"
instance_type = "${var.cluster_node_type}"
count = "${var.cluster_size - 1}" # one node will be used for coordinator
<...skipped...>
# Running provisioning commands
provisioner "remote-exec" {
inline = [
"yum -y update",
"sudo sh /tmp/bootstrap-script.sh ${aws_instance.master_node.private_ip}" # master-ip passed to script
]
connection {
type = "ssh"
user = "centos"
private_key = "${file("${var.cluster_keyfile}")}"
}
}
}
т.е. при помощи переменных вида ${aws_instance.master_node.private_ip} можно получить доступ к почти любой информации о ресурсе. В данном примере подразумеваем, что bootstrap-script.sh может принимать в качестве параметра адрес master ноды и использовать его в последствии для внутреннего конфигурирования.
Иногда не достаточно и таких связей, — к примеру, необходимо вызвать какие-то скрипты на стороне master ноды после подключения worker нод (принять ключи, запустить init задачи на worker нодах и т.п.) для этого есть механизм который в Terraform называется null-resource — это fake ресурс который с помощью механизма зависимостей (см. выше) может быть создан после того, как будут созданы все master и worker ноды. Вот пример такого ресурса:
resource "null_resource" "cluster_provision" {
depends_on = [
"aws_instance.master_node",
"aws_instance.worker_nodes"
]
# Changes to any instance of the workers cluster nodes or master node requires re-provisioning
triggers {
cluster_instance_ids = "${aws_instance.master_node.id},${join(",", aws_instance.worker_nodes.*.id)}"
}
# Bootstrap script can run only on master node
connection {
host = "${aws_instance.coordinator_node.private_ip}"
type = "ssh"
user = "centos"
private_key = "${file("${var.cluster_keyfile}")}"
}
provisioner "remote-exec" {
inline = [
<... some after-provision scripts calls on master node...>
]
}
}
небольшое пояснение:
1. depends_on — мы указываем список тех ресурсов, которые должны быть готовы заранее.
2. triggers — формируем стоку (id всех инстансов через запятую, в нашем случае) изменение которой вызовет выполнение всех указанных в этом ресурсе провизионеров.
3. указываем на каком инстансе нужно выполнить скрипты провизионинга указанные в этом ресурсе в секции connection.
Если Вам нужно совершить несколько шагов на разных серверах — создавайте несколько null-resource с указанием необходимых зависимостей.
В целом, описанного будет достаточно для создания достаточно сложных инфрастурктур с помощью Terraform.
Вот еще несколько важных советов для тех, кто любит учиться на чужих ошибках:
1. Не забывайте бережно хранить .tfstate файлы в которых Terraform хранит последнее состояние созданной инфрастуркутуры (ко всему — это json файл, который можно использовать как исчерпывающий источник информации о созданных ресурсах)
2. Не меняйте созданные при помощи Terraform ресурсы вручную (используя консоли управления самим сервисами и другие внешние фреймворки) — при следующем запуске plan & apply вы получите пересоздание не соответствующего текущему описанию ресурса, что будет весьма неожиданно и часто плачевно.
3. Старайтесь сначала оттестировать свои конфигурации на небольших по размеру инстансах / небольшом их количестве, — много ошибок очень тяжело отловить в ходе создания конфигураций, а встроенный в Terraform валидатор покажет только синтаксические ошибки (и то не все).
Во второй части рассмотрим продолжение приготовления к работе скороварки — опишем как положить на верх созданной инфраструктуры SaltStack master + minions чтобы поставить PrestoDB.