Всем привет! В рамках курсовой работы я занимался исследованиями возможностей такой отечественной облачной платформы, как Яндекс.Облако. Платформа предлагает различные сервисы для решения практических задач. Однако иногда бывает нужно на основе этих сервисов настроить своё облачное приложение с достаточно развесистой инфраструктурой. В этой статье я хочу поделиться опытом развёртки такого приложения.
Что хочется получить?
Grafana — мощный инструмент для решения аналитических задач или задач мониторинга каких-либо систем. В своей базовой комплектации это виртуальная машина с веб-сервером Графаны, а также база данных (ClickHouse, InfluxDB, etc.) с датасетом, по которому будет строиться аналитика.
После запуска виртуальной машины с веб-сервером можно будет зайти на его хост и получить красивый UI, указать базы данных в качестве источников для дальнейшей работы, создать дашборды и графики.
У базовой версии есть один существенный недостаток — она совершенно не отказоустойчива. То есть вся работоспособность приложения зависит от жизнеспособности одной виртуальной машины. Если она откажет или же 10 человек одновременно откроют UI, то возникнут проблемы.
Решаются они просто: нужно всего лишь… развернуть много одинаковых виртуальных машин с веб-сервером и поместить их под L3-балансер. Но не здесь не все так однозначно. Графана хранит пользовательские настройки (пути к базам данных, дашборды, графики и т.д.) прямо на диске своей виртуальной машины. Таким образом, если изменить какие-то настройки в UI, то эти изменения отобразятся лишь на той виртуальной машине, куда отправил нас балансер. Это приведет к неконсистентным настройкам нашего приложения, возникнут проблемы с запуском и использованием.
Здесь на помощь придет еще одна база данных, например, MySQL или ее аналог. Говорим Графане, что она должна хранить пользовательские настройки именно в этой "запасной" базе. После достаточно будет один раз указать на каждой машине путь к этой БД, а все остальные пользовательские настройки редактировать на любой из виртуальных машин, они будут прорастать на остальные.
Вот схема итоговой инфраструктуры приложения:
Научимся поднимать руками
MySQL и ClickHouse
Прежде чем разворачивать такое приложение нажатием одной кнопки, нужно было научиться поднимать ручками каждый его компонент и интегрировать их друг с другом.
Здесь нам поможет Яндекс.Облако, которое предоставляет L3-балансеры, ClickHouse и MySQL в качестве managed-сервисов. Пользователю необходимо только указать параметры и подождать, пока платформа приведет все в работоспособное состояние.
Я зарегистрировался, создал себе облако и платежный аккаунт. После этого зашел в облако и поднял кластеры MySQL и ClickHouse с минимальными настройками. Дождался, пока они станут активны.
Также надо не забыть создать в каждом кластере базу данных и настроить доступ к ней по логину и паролю. Вдаваться здесь в детали не буду — в интерфейсе все достаточно очевидно.
Неочевидная деталь была в том, что у этих БД множество хостов, которые обеспечивают их отказоустойчивость. Однако Графана требует ровно один хост для каждой БД, с которой она работает. Длительное чтение документации Облака привело меня к решению. Оказывается, хост вида c-<cluster_id>.rw.mdb.yandexcloud.net
маппится в текущий активный мастер-хост кластера с соответствующим айдишником. Именно его мы и отдадим Графане.
Веб-сервер
Теперь дело за веб-сервером. Поднимем обычную виртуальную машину с Linux и руками настроим на ней Графану.
Подлючимся по ssh и установим необходимые пакеты.
sudo apt-get install -y apt-transport-https software-properties-common wget
wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
sudo add-apt-repository "deb https://packages.grafana.com/enterprise/deb stable main"
sudo apt-get update
sudo apt-get install -y grafana-enterprise
После этого заведем Графану под systemctl и установим плагин для работы с ClickHouse (да, в базовой комплектации он не поставляется).
sudo systemctl start grafana-server
sudo systemctl enable grafana-server
sudo grafana-cli plugins install vertamedia-clickhouse-datasource
Все, после этого простой командой
sudo service grafana-server start
мы запустим веб-сервер. Теперь можно будет в браузере вбить внешний айпишник виртуальной машины, указать порт 3000 и увидеть красивый UI графаны.
Но не стоит спешить, прежде чем настраивать Графану, надо не забыть указать ей путь к MySQL, чтобы хранить настройки там.
Вся конфигурация веб-сервера Графаны лежит в файле /etc/grafana/grafana.ini
. Нужная строка выглядит так:
;url =
Выставляем хост к кластеру MySQL. В этом же файле находятся логин и пароль для доступа к Графане на картинке выше, которые по умолчанию оба равны admin
.
Можно воспользоваться sed-командами:
sudo sed -i "s#.*;url =.*#url = mysql://${MYSQL_USERNAME}:${MYSQL_PASSWORD}@${MYSQL_CLUSTER_URI}#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_user =.*#admin_user = ${GRAFANA_USERNAME}#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_password =.*#admin_password = ${GRAFANA_PASSWORD}#" /etc/grafana/grafana.ini
Самое время перезапустить веб-сервер!
sudo service grafana-server restart
Теперь в UI Графаны укажем ClickHouse в качестве DataSource.
Добиться работающей конфигурации у меня получилось при следующих настройках:
В качестве URL я указал https://c-<cluster_id>.rw.mdb.yandexcloud.net:8443
Всё! У нас есть одна работоспособная виртуальная машинка с веб-сервером, подключенным к CH и MySQL. Уже можно загружать датасет в ClickHouse и строить дашборды. Однако мы еще не достигли нашей цели и не развернули полноценную инфраструктуру.
Packer
Яндекс.Облако позволяет создать образ диска существующей виртуальной машины, а на его основе — сколько угодно идентичных друг другу машин. Именно этим мы и воспользуемся. Чтобы удобно собрать образ, возьмем инструмент Packer от HashiCorp. Он принимает на вход json-файл с инструкцией по сборке образа.
Наш json-файл будет состоять из двух блоков: builders и provisioners. Первый блок описывает параметры самого образа как сущности, а второй — инструкцию по наполнению нужным содержимым.
Builders
{
"builders": [
{
"type": "yandex",
"endpoint": "{{user `endpoint`}}",
"folder_id": "<folder_id>",
"subnet_id": "{{user `subnet_id`}}",
"zone": "{{user `zone`}}",
"labels": {},
"use_ipv4_nat": true,
"use_internal_ip": false,
"service_account_key_file": "<service_account_key_file>",
"image_name": "grafana-{{timestamp}}",
"image_family": "grafana",
"image_labels": {},
"image_description": "GRAFANA",
"source_image_family": "ubuntu-1804-lts",
"disk_size_gb": 3,
"disk_type": "network-hdd",
"ssh_username": "ubuntu"
}
],
...
}
В этом шаблоне нужно выставить идентификатор раздела в облаке, где вы хотите создать образ, а также путь к файлу с ключами от сервисного аккаунта, предварительно заведенного в этом разделе. Подробнее про создание сервисных аккаунтов и ключей в виде файла можно почитать в соответствующем разделе документации.
Такая конфигурация говорит, что образ диска будет собран на основе платформы ubuntu-1804-lts
, помещен в соответствующем разделе пользователя в семействе образов GRAFANA
под именем grafana-{{timestamp}}
.
Provisioners
Теперь более интересная часть конфигурации. В ней будет описана последовательность действий, которые надо будет совершить на виртуальной машине, прежде чем заморозить ее состояние в образ диска.
{
...,
"provisioners": [
{
"type": "shell",
"pause_before": "5s",
"scripts": [
"prepare-ctg.sh"
]
},
{
"type": "file",
"source": "setup.sh",
"destination": "/opt/grafana/setup.sh"
},
{
"type": "shell",
"execute_command": "sudo {{ .Vars }} bash '{{ .Path }}'",
"pause_before": "5s",
"scripts": [
"install-packages.sh",
"grafana-setup.sh",
"run-setup-at-reboot.sh"
]
}
]
}
Здесь все действия разделены на 3 этапа. На первом этапе выполняется простенький скрипт, который создает вспомогательную директорию.
prepare-ctg.sh:
#!/bin/bash
sudo mkdir -p /opt/grafana
sudo chown -R ubuntu:ubuntu /opt/grafana
На следующем этапе в эту директорию помещаем скрипт, который надо будет запустить сразу после запуска виртуальной машины. Этот скрипт положит пользовательские переменные, которые надо прописать, в конфиг Графаны и перезапустит веб-сервер.
setup.sh:
#!/bin/bash
CLUSTER_ID="<cluster_id>"
USERNAME="<username>"
PASSWORD="<password>"
sudo sed -i "s#.*;url =.*#url = mysql://${USERNAME}:${PASSWORD}@c-${CLUSTER_ID}.rw.mdb.yandexcloud.net#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_user =.*#admin_user = ${USERNAME}#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_password =.*#admin_password = ${PASSWORD}#" /etc/grafana/grafana.ini
sudo service grafana-server restart
После этого осталось сделать 3 вещи:
1) установить пакеты
2) завести Графану под systemctl и установить плагин ClickHouse
3) положить скрипт setup.sh в очереди на запуск сразу после включения виртуальной машины.
install-packages.sh:
#!/bin/bash
sudo systemd-run --property='After=apt-daily.service apt-daily-upgrade.service' --wait /bin/true
sudo apt-get install -y apt-transport-https
sudo apt-get install -y software-properties-common wget
wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
sudo add-apt-repository "deb https://packages.grafana.com/enterprise/deb stable main"
sudo apt-get update
sudo apt-get install -y grafana-enterprise
grafana-setup.sh:
#!/bin/bash
sudo systemctl start grafana-server
sudo systemctl enable grafana-server
sudo grafana-cli plugins install vertamedia-clickhouse-datasource
run-setup-at-reboot.sh:
#!/bin/bash
chmod +x /opt/grafana/setup.sh
cat > /etc/cron.d/first-boot <<EOF
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
@reboot root /bin/bash /opt/grafana/setup.sh > /var/log/yc-setup.log 2>&1
EOF
chmod +x /etc/cron.d/first-boot;
Теперь остается запустить Packer и на выходе получить образ, помещенный в указанном разделе. При создании виртуальной машины можно выбрать его в качестве загрузочного диска и после запуска получить готовый веб-сервер Графаны.
Инстанс-группа и балансер
После того, как появился образ диска, который позволяет создавать множество одинаковых веб-серверов Графаны, мы можем создать инстанс-группу. На платформе Яндекс.Облако этим термином называется объединение виртуальных машин, имеющих одинаковые характеристики. При создании инстанс-группы конфигурируется прототип всех машин в этой группе, а потом и характеристики самой группы (например, минимальное и максимальное количество активных машин). Если текущее количество будет не соответствовать этим критерием, то инстанс-группа сама удалит ненужные машины или создаст новые по образу и подобию.
В рамках нашей задачи мы создадим инстанс-группу веб-серверов, которые будут порождаться из созданного ранее образа диска.
По-настоящему примечательна последняя настройка инстанс-группы. Целевая группа в интеграции с Load Balancer поможет нажатием пары кнопок настроить L3-балансер поверх виртуальных машин этой группы.
При настройке балансера я реализовал два важных момента:
- Сделал так, чтобы балансер принимал пользовательский трафик на 80 порте, а перенаправлял его на 3000 порт виртуальных машин, как раз туда, где живет Графана.
- Настроил проверку жизнеспособности машин, пингуя их в 3000 порт.
Мини-итог
Наконец мы смогли руками развернуть желаемую инфраструктуру приложения, и теперь у нас есть высокоустойчивый сервис Grafana. Необходимо лишь знать IP-адрес балансера как точку входа в прииложение и хост кластера ClickHouse, чтобы загрузить в него датасет.
Казалось бы, победа? Да, победа. Но что-то все-таки смущает. Весь процесс выше требует очень много ручных действий и совершенно не масштабируется, хочется его по возможности автоматизировать. Этому и будет посвящен следующий раздел.
Интеграция с Terraform
Мы снова воспользуемся инструментом от компании HashiCorp по имени Terraform. Он поможет нажатием кнопки разворачивать всю инфраструктуру приложения, основываясь на нескольких переменных, переданных пользователем. Давайте напишем рецепт, который можно будет запускать многократно в разных разделах разных пользователей.
Вся работа с Терраформом сводится к написанию конфигурационного файла (*.tf
) и созданию инфраструктуры на его основе.
Переменные
В самое начала файла вынесем переменные, от которых зависит, где и как будет развернута будущая инфраструктура.
variable "oauth_token" {
type = string
default = "<oauth-token>"
}
variable "cloud_id" {
type = string
default = "<cloud-id>"
}
variable "folder_id" {
type = string
default = "<folder_id>"
}
variable "service_account_id" {
type = string
default = "<service_account_id>"
}
variable "image_id" {
type = string
default = "<image_id>"
}
variable "username" {
type = string
default = "<username>"
}
variable "password" {
type = string
default = "<password>"
}
variable "dbname" {
type = string
default = "<dbname>"
}
variable "public_key_path" {
type = string
default = "<path to ssh public key>"
}
Весь процесс развёртки приложения сведется к сборке образа диска и выставлению этих переменных. Поясню, за что они отвечают:
oauth_token — токен для доступа к облаку. Можно получить по ссылке.
cloud_id — идентификатор облака, где будем разворачивать приложение
folder_id — идентификатор раздела, где будем разворачивать приложение
service_account_id — идентификатор сервисного аккаунта в соотвествующем разделе облака.
image_id — идентификатор образа диска, полученного с помощью Packer
username и password — имя пользователя и пароль для доступа к обеим базам данных и веб-серверу Графаны
dbname — имя базы данных внутри кластеров CH и MySQL
public_key_path — путь к файлу с вашим публичным ssh-ключом, по которому можно будет подключаться под именем ubuntu
к виртуальным машинам с веб-серверами
Настройка провайдера
Теперь нужно настроить провайдер Терраформа — в нашем случае Яндекс:
provider "yandex" {
token = var.oauth_token
cloud_id = var.cloud_id
folder_id = var.folder_id
zone = "ru-central1-a"
}
Можно заметить, что здесь мы используем переменные, заданные выше.
Сеть и кластеры
Теперь создадим сеть, в которой будут общаться элементы нашей инфраструктуры, три подсети (по одной в каждом регионе) и поднимем кластеры CH и MySQL.
resource "yandex_vpc_network" "grafana_network" {}
resource "yandex_vpc_subnet" "subnet_a" {
zone = "ru-central1-a"
network_id = yandex_vpc_network.grafana_network.id
v4_cidr_blocks = ["10.1.0.0/24"]
}
resource "yandex_vpc_subnet" "subnet_b" {
zone = "ru-central1-b"
network_id = yandex_vpc_network.grafana_network.id
v4_cidr_blocks = ["10.2.0.0/24"]
}
resource "yandex_vpc_subnet" "subnet_c" {
zone = "ru-central1-c"
network_id = yandex_vpc_network.grafana_network.id
v4_cidr_blocks = ["10.3.0.0/24"]
}
resource "yandex_mdb_clickhouse_cluster" "ch_cluster" {
name = "grafana-clickhouse"
environment = "PRODUCTION"
network_id = yandex_vpc_network.grafana_network.id
clickhouse {
resources {
resource_preset_id = "s2.micro"
disk_type_id = "network-ssd"
disk_size = 16
}
}
zookeeper {
resources {
resource_preset_id = "s2.micro"
disk_type_id = "network-ssd"
disk_size = 10
}
}
database {
name = var.dbname
}
user {
name = var.username
password = var.password
permission {
database_name = var.dbname
}
}
host {
type = "CLICKHOUSE"
zone = "ru-central1-a"
subnet_id = yandex_vpc_subnet.subnet_a.id
}
host {
type = "CLICKHOUSE"
zone = "ru-central1-b"
subnet_id = yandex_vpc_subnet.subnet_b.id
}
host {
type = "CLICKHOUSE"
zone = "ru-central1-c"
subnet_id = yandex_vpc_subnet.subnet_c.id
}
host {
type = "ZOOKEEPER"
zone = "ru-central1-a"
subnet_id = yandex_vpc_subnet.subnet_a.id
}
host {
type = "ZOOKEEPER"
zone = "ru-central1-b"
subnet_id = yandex_vpc_subnet.subnet_b.id
}
host {
type = "ZOOKEEPER"
zone = "ru-central1-c"
subnet_id = yandex_vpc_subnet.subnet_c.id
}
}
resource "yandex_mdb_mysql_cluster" "mysql_cluster" {
name = "grafana_mysql"
environment = "PRODUCTION"
network_id = yandex_vpc_network.grafana_network.id
version = "8.0"
resources {
resource_preset_id = "s2.micro"
disk_type_id = "network-ssd"
disk_size = 16
}
database {
name = var.dbname
}
user {
name = var.username
password = var.password
permission {
database_name = var.dbname
roles = ["ALL"]
}
}
host {
zone = "ru-central1-a"
subnet_id = yandex_vpc_subnet.subnet_a.id
}
host {
zone = "ru-central1-b"
subnet_id = yandex_vpc_subnet.subnet_b.id
}
host {
zone = "ru-central1-c"
subnet_id = yandex_vpc_subnet.subnet_c.id
}
}
Как можно заметить, каждый из двух кластеров создан достаточно отказоустойчивым за счет размещения в трех зонах доступности.
Веб-серверы
Казалось бы, можно продолжать в том же духе, но я столкнулся со сложностью. До этого я сначала поднимал MySQL кластер и только после этого, зная его ID, собирал образ диска с нужной конфигурацией, где указывал хост к кластеру. Но теперь мы не знаем ID кластера до запуска Терраформа, в том числе и на момент сборки образа. Поэтому пришлось прибегнуть к следующему трюку.
Используя сервис метаданных от Amazon, мы передадим в виртуальную машину некоторые параметры, которые она примет и обработает. Нам необходимо, чтобы после запуска машина сходила в метаданные за хостом MySQL кластера и за username-password, которые пользователь указывал в файле Terraform. Незначительно изменим содержимое файла setup.sh
, который запускается при включении виртуальной машины.
setup.sh:
#!/bin/bash
CLUSTER_URI="$(curl -H 'Metadata-Flavor:Google' http://169.254.169.254/computeMetadata/v1/instance/attributes/mysql_cluster_uri)"
USERNAME="$(curl -H 'Metadata-Flavor:Google' http://169.254.169.254/computeMetadata/v1/instance/attributes/username)"
PASSWORD="$(curl -H 'Metadata-Flavor:Google' http://169.254.169.254/computeMetadata/v1/instance/attributes/password)"
sudo sed -i "s#.*;url =.*#url = mysql://${USERNAME}:${PASSWORD}@${CLUSTER_URI}#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_user =.*#admin_user = ${USERNAME}#" /etc/grafana/grafana.ini
sudo sed -i "s#.*;admin_password =.*#admin_password = ${PASSWORD}#" /etc/grafana/grafana.ini
sudo service grafana-server restart
Интанс-группа и балансер
Пересобрав новый образ диска, мы можем наконец дописать наш файл для Терраформа.
Укажем, что мы хотим использовать существующий образ диска:
data "yandex_compute_image" "grafana_image" {
image_id = var.image_id
}
Теперь создадим инстанс-группу:
resource "yandex_compute_instance_group" "grafana_group" {
name = "grafana-group"
folder_id = var.folder_id
service_account_id = var.service_account_id
instance_template {
platform_id = "standard-v1"
resources {
memory = 1
cores = 1
}
boot_disk {
mode = "READ_WRITE"
initialize_params {
image_id = data.yandex_compute_image.grafana_image.id
size = 4
}
}
network_interface {
network_id = yandex_vpc_network.grafana_network.id
subnet_ids = [yandex_vpc_subnet.subnet_a.id, yandex_vpc_subnet.subnet_b.id, yandex_vpc_subnet.subnet_c.id]
nat = "true"
}
metadata = {
mysql_cluster_uri = "c-${yandex_mdb_mysql_cluster.mysql_cluster.id}.rw.mdb.yandexcloud.net:3306/${var.dbname}"
username = var.username
password = var.password
ssh-keys = "ubuntu:${file("${var.public_key_path}")}"
}
network_settings {
type = "STANDARD"
}
}
scale_policy {
fixed_scale {
size = 6
}
}
allocation_policy {
zones = ["ru-central1-a", "ru-central1-b", "ru-central1-c"]
}
deploy_policy {
max_unavailable = 2
max_creating = 2
max_expansion = 2
max_deleting = 2
}
load_balancer {
target_group_name = "grafana-target-group"
}
}
Стоит обратить внимание, как мы передали в метадату cluster_uri
, username
и password
. Именно их виртуальная машина при запуске достанет и положит в конфиг Графаны.
Дело за балансером.
resource "yandex_lb_network_load_balancer" "grafana_balancer" {
name = "grafana-balancer"
listener {
name = "grafana-listener"
port = 80
target_port = 3000
external_address_spec {
ip_version = "ipv4"
}
}
attached_target_group {
target_group_id = yandex_compute_instance_group.grafana_group.load_balancer.0.target_group_id
healthcheck {
name = "healthcheck"
tcp_options {
port = 3000
}
}
}
}
Немного сахара
Осталась самая малость. После того, как инфраструктура развернется, придется сходить в UI Графаны и руками добавить кластер CH (ID которого нужно еще добыть) как Data Source. Но ведь ID кластера знает Терраформ. Поручим ему довести дело до ума.
Добавим нового провайдера — Графану, а в качестве хоста подсунем ей айпишник балансера. Все изменения, которые сделает Терраформ на машине, куда определит его балансер, прорастут в MySQL, а значит и на все остальные машины.
provider "grafana" {
url = "http://${[for s in yandex_lb_network_load_balancer.grafana_balancer.listener: s.external_address_spec.0.address].0}"
auth = "${var.username}:${var.password}"
}
resource "grafana_data_source" "ch_data_source" {
type = "vertamedia-clickhouse-datasource"
name = "grafana"
url = "https://c-${yandex_mdb_clickhouse_cluster.ch_cluster.id}.rw.mdb.yandexcloud.net:8443"
basic_auth_enabled = "true"
basic_auth_username = var.username
basic_auth_password = var.password
is_default = "true"
access_mode = "proxy"
}
Причешем
Выведем айпишник балансера и хост кластера ClickHouse
output "grafana_balancer_ip_address" {
value = [for s in yandex_lb_network_load_balancer.grafana_balancer.listener: s.external_address_spec.0.address].0
}
output "clickhouse_cluster_host" {
value = "https://c-${yandex_mdb_clickhouse_cluster.ch_cluster.id}.rw.mdb.yandexcloud.net:8443"
}
Можно запускать
Всё! Наш конфигурационный файл готов и можно, выставив переменные, сказать Терраформу поднять все, что мы описали выше. Весь процесс у меня занял около 15 минут.
В конце можно увидеть красивое сообщение:
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
Outputs:
clickhouse_cluster_host = https://c-c9q14ipa2ngadqsbp2iq.rw.mdb.yandexcloud.net:8443
grafana_balancer_ip_address = 130.193.50.25
А в облаке будут видны элементы поднятой инфраструктуры:
Подведем итоги
Теперь на примере Графаны каждый из вас умеет разворачивать приложения с развесистой облачной архитектурой на платформе Яндекс.Облако. В этом вам могут помочь такие полезные инструменты от HashiCorp, как Packer и Terraform. Надеюсь, кому-нибудь эта статья окажется полезной :)
P.S. Ниже приложу ссылочку на репозиторий, в котором можно найти готовые рецепты для Пакера и Терраформа, фрагменты которых я приводил в этой статье.