[Из песочницы] Деплой распределенных сервисов в Яндекс.Облаке на примере Grafana
Всем привет! В рамках своей курсовой работы я занимался исследованиями возможностей такой отечественной облачной платформы, как Яндекс.Облако. Платформа предоставляет множество различных сервисов, помогающих решать многие практические задачи. Однако иногда бывает нужно на основе этих сервисов настроить свое облачное приложение, имеющее достаточно развесистую инфраструктуру. В этой статье я хочу поделиться своим опытом развертки такого приложения.
Что хочется получить?
Grafana — мощный инструмент для решения аналитических задач или задач мониторинга каких-либо систем. В базовой своей комплектации это виртуальная машина с веб-сервером Графаны, а так же база данных (ClickHouse, InfluxDB, etc.) с датасетом, по которому будет строиться аналитика.
После того, как получится поднять виртуалку с веб-сервером, можно будет зайти на хост веб-сервера и получить красивый UI, в котором можно указать базы данных в качестве источников данных, с которыми будет вестись дальнейшая работа. Там же можно будет создать дашборды и графики.
Однако у базовой версии есть один существенный недостаток — она совершенно не отказоустойчивая. То есть вся работоспособность приложения зависит от жизнеспособности одной виртуальной машины. Если она откажет или же 10 человек одновременно откроют UI, то возникнут проблемы.
Такая проблема обычно решается просто — нужно всего лишь… развернуть много одинаковых виртуальных машин с веб-сервером и поместить их под L3-балансер. Но не все так просто. Графана хранит пользовательские настройки (пути к базам данных, дашборды, графики и т.д.) прям на диске своей виртуальной машины. Таким образом, если изменить какие-то настройки в UI, то эти изменения отобразятся лишь на той виртуальной машине, в которую отправил нас балансер. Это приведет к неконсистентным настройкам нашего приложения. Мало того, что этим будет совершенно невозможно пользоваться, так еще и непонятно как изначально запустить приложение. Не заходить же нам руками на каждую машину и настраивать там Графану.
Здесь нам на помощь придет еще одна база данных. Например, MySQL или ее аналог. Можно сказать Графане, что она должна хранить пользовательские настройки именно в ней. Тогда все сведется к тому, чтобы единожды указать на каждой машине путь к этой БД, а все остальные пользовательские настройки можно будет редактировать на любой из виртуальных машин, и они будут прорастать на остальные.
У меня получилась вот такая схема итоговой инфраструктуры приложения.
Научимся поднимать руками
MySQL и ClickHouse
Прежде чем учиться разворачивать такое приложение нажатием одной кнопки, нужно было научиться поднимать ручками каждый его компонент и интегрировать их друг с другом.
Здесь нам значительную помощь окажет Яндекс.Облако, которое предоставляет L3-балансеры, ClickHouse и MySQL в качестве managed-сервисов. То есть пользователю необходимо только выставить параметры и подождать пока платформа сама приведет все в работоспособное состояние.
Я зарегистрировался на платформе, создал себе облако и платежный аккаунт. После этого зашел в облако и поднял себе кластера MySQL и ClickHouse с минимальными настройками. Дождался, пока они станут активны.
Также надо не забыть создать в каждом кластере базу данных и настроить доступ к ней по логину и паролю. Вдаваться здесь в детали не буду — в интерфейсе все достаточно очевидно.
Неочевидная деталь была в том, что у этих БД множество хостов, которые обеспечивают их отказоустойчивость. Однако Графана требует ровно один хост для каждой БД, с которой она работает. Длительное чтение документации Облака привело меня к решению. Оказывается, хост вида c-
маппится в текущий активный мастер-хост кластера с соответствующим айдишником. Именно его мы и отдадим Графане.
Веб-сервер
Теперь дело встало за веб-сервером. Поднимем обычную виртуальную машину с 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-
Всё! После этого у нас есть одна работоспособная виртуальная машинка с веб-сервером, подключенным к CH и MySQL. Теперь можно уже загружать датасет в ClickHouse и строить дашборды. Однако мы еще не достигли нашей цели и не развернули полноценную инфраструктуру нашего приложения.
Packer
Платформа Яндекс.Облака позволяет создать образ диска существующей виртуальной машины, а затем создать на основе этого образа сколько угодно идентичных друг другу виртуальных машин. Именно этим мы и воспользуемся. Чтобы удобно собирать образ, мы вопользуемся инструментов Packer от HashiCorp. Он принимает на вход json-файл с иинструкцией по сборке образа.
Наш Json-файл будет состоять из двух блоков: builders и provisioners. Первый блок описывает параметры самого образа как сущности, а второй — инструкцию по наполнению этого образа нужным содержимым.
Builders
{
"builders": [
{
"type": "yandex",
"endpoint": "{{user `endpoint`}}",
"folder_id": "",
"subnet_id": "{{user `subnet_id`}}",
"zone": "{{user `zone`}}",
"labels": {},
"use_ipv4_nat": true,
"use_internal_ip": false,
"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=""
USERNAME=""
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 < /var/log/yc-setup.log 2>&1
EOF
chmod +x /etc/cron.d/first-boot;
Теперь остается запустить создание образа Packer’ом и на выходе получить образ, помещенный в указанном разделе. Теперь при создании виртуальной машины можно выбрать его в качестве загрузочного диска и после запуска получить готовый веб-сервер Графаны.
Инстанс-группа и балансер
После того, как у нас появился образ диска, который позволяет создавать множество одинаковых веб-серверов Графаны, мы можем создать инстанс-группу. На платформе Яндекс.Облако этим термином называется объединение виртуальных машин, имеющих одинаковые характеристики. При создании инстанс-группы вы сначала конфигурируете прототип всех машин в этой группе, а потом и характеристики самой группы. Например, такие, как минимально и максимальное количество активных машин в группе. Если текущее количество будет не соответствовать этим критерием, то инстанс группа сама удалит ненужные машины или создаст новые по образу и подобию.
В рамках нашей задачи мы создадим инстанс-группу веб-серверов, которые будут порождаться из образа диска, созданного нами ранее.
По-настоящему примечательна последняя настройка инстанс-группы. Целевая группа в интеграции с Load Balancer поможет нам нажатием пары кнопок настроить L3-балансер поверх виртуальных машин этой группы.
При настройке балансера сделаем 2 вещи:
- Сделаем так, чтобы балансер принимал пользовательский трафик на 80 порту, а перенаправлял его на 3000 порт виртуальных машин. Как раз туда, где живет Графана.
- Настроим проверку жизнеспособности машин, пингуя их в 3000 порт.
Мини-итог
Наконец, мы смогли руками развернуть желаемую инфраструктуру приложения, и теперь у нас есть высокоустойчивый сервис Grafana. Нам необходимо лишь знать IP-адрес балансера, как точку входа в прииложение, и хост кластера ClickHouse, чтобы загрузить в него датасет.
Казалось бы победа? Да, победа. Но что-то все-таки смущает. Смущает, что весь процесс выше требует очень много ручных действий. И совершенно не масштабируется. Поднятие одного такого приложения требует внимательности и четкого выполнения инструкции. Очень хочется автоматизировать процесс разворачивания и настройки всей этой инфраструктуры, сведя к минимуму необходимые для этого действия. Этому и будем посвящен следующий раздел.
Интеграция с Terraform
Мы снова воспользуемся инструментом от компании HashiCorp по имени Terraform. Он поможет нам нажатием кнопки разворачивать всю инфраструктуру нашего приложения, основываясь на нескольких переменных, переданных пользователем. Мы сейчас напишем рецепт, который можно будет запускать многократно в разных разделах разных пользователей.
Вся работа с Терраформом сводится к написанию конфигурационного файлика (*.tf
) и затем запуском создания инфраструктуры на его основе.
Переменные
В самое начала файла вынесем переменные, от которых зависит где и как будет развернута будущая инфраструктура.
variable "oauth_token" {
type = string
default = ""
}
variable "cloud_id" {
type = string
default = ""
}
variable "folder_id" {
type = string
default = ""
}
variable "service_account_id" {
type = string
default = ""
}
variable "image_id" {
type = string
default = ""
}
variable "username" {
type = string
default = ""
}
variable "password" {
type = string
default = ""
}
variable "dbname" {
type = string
default = ""
}
variable "public_key_path" {
type = string
default = ""
}
Весь процесс развертки приложения сведется к сборке образа диска и выставлению этих переменных. Поясню, за что они отвечают:
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. Ниже приложу ссылочку на репозиторий, в котором можно найти готовые рецепты для Пакера и Терраформа, фрагменты которых я приводил в этой статье.
Репозиторий