
Sentry — это инструмент для отслеживания ошибок и производительности приложений в реальном времени.
-
Отслеживает баги и exceptions в бекенд, веб и мобильных приложениях.
-
Показывает стек вызовов, контекст, окружение, пользователя и другую полезную информацию.
-
Помогает разработчикам быстро находить и исправлять баги.
-
Поддерживает множество языков и фреймворков
Для кого этот пост
-
Этот пост для тех кто хочет перейти с Sentry в docker-compose
-
Для тех кто хочет перейти с Nodestore в PostgreSQL в S3
Отличия от предыдущего поста про Sentry
-
Используются Kafka, ClickHouse вне Kubernetes
-
Для Nodestore используется S3
-
Добавлен пример сборки кастомных image sentry, snuba, replay с сертификатом от yandex
-
Подключение Kafka, Redis, ClickHouse, Postgres через SSL (можно отключить).
-
Динамическое формирование values для helm чарта sentry
-
Используется чистый terraform чтобы вам было легче разобраться в коде
Быстрый старт в yandex cloud
-
Клонируем репозиторий https://github.com/patsevanton/sentry-external-kf-ch-pg-rd
-
Меняем dns зону и dns запись в файле ip-dns.tf
-
Меняем user_email и system_url в файле templatefile.tf
Запускаем инфраструктуру:
export YC_FOLDER_ID='ваш folder' terraform init terraform apply
Формируем kubeconfig для кластера k8s с указанным ID (идентификатор_кластера) в Yandex Cloud, используя внешний IP (—external)
yc managed-kubernetes cluster get-credentials --id идентификатор_кластера --external --force
Проверяем сгенерированный конфиг values_sentry.yaml из шаблона
Деплоим Sentry в кластер через Helm
kubectl create namespace test helm repo add sentry https://sentry-kubernetes.github.io/charts helm repo update helm upgrade --install sentry -n test sentry/sentry --version 26.15.1 -f values_sentry.yaml
В версии 26.15.1 sentry helm чарта используется 25.2.0 версия sentry
Пароли
Пароли генерируются динамически, но вы можете указать свои пароль в local.tf Их можно получить посмотрев values_sentry.yaml или используя terraform output
Простой пример отправки exception
-
Создаем проект в Sentry, выбираем python, копируем DSN
-
Заходим в директорию
example-python -
Меняем dsn в main.py (Сам DSN лучше хранить в секретах (либо брать из env))
-
Запускаем python код
cd example-python python3 -m venv venv source venv/bin/activate pip install --upgrade sentry-sdk python3 main.py
Почему важно выносить Kafka, Redis, ClickHouse, Postgres вне Kubernetes
Плюсы такого подхода:
-
Масштабируемость
-
Изоляция ресурсов
-
Более надежное хранилище
Минусы/предостережения:
-
Логирование и трассировка проблем становится чуть сложнее
-
Требует аккуратной настройки переменных и IAM-доступов (особенно к S3)
Подключение Kafka, Redis, ClickHouse, Postgres через SSL
В этом посте в отличие от предыдущего будет подключение Kafka, Redis, Postgres через SSL. Для подключения ClickHouse по SSL ждем вот этого PR. В terraform коде в комментариях указано как настраивать SSL и как отключать SSL
Структура Terraform проекта
-
Список и краткое описание ключевых файлов в репо:
-
example-python— демонстрация, как отправлять ошибки в Sentry из Python -
clickhouse.tf— managed ClickHouse (Yandex Cloud) -
ip-dns.tf– настраивает IP-адреса и записи DNS для ресурсов. -
k8s.tf— managed Kuberbetes (Yandex Cloud) для деплоя Sentry -
kafka.tf— managed Kafka (Yandex Cloud) -
locals.tf– определяет локальные переменные, используемые в других файлах Terraform. -
net.tf– описывает сетевые ресурсы, такие как VPC, подсети и маршруты. -
postgres.tf— managed Postgres (Yandex Cloud) -
redis.tf— для кэширования и очередей managed Redis (Yandex Cloud) -
s3_filestore.tfиs3_nodestore.tf— хранилище blob-данных managed S3 (Yandex Cloud) -
values_sentry.yamlиvalues_sentry.yaml.tpl— конфиг для Sentry, параметризуем через Terraformtemplatefile -
versions.tf– задаёт версии Terraform и провайдеров, необходимых для работы проекта.
-
Хранение основных данных (Nodestore) в S3
Отмечу отдельно что основные данные (Nodestore) хранятся в S3, так как хранение в PostgreSQL приводит со временем к проблемам и медленной работе Sentry. Файл s3_nodestore.tf — хранилище blob-данных managed S3 (Yandex Cloud). В файле values_sentry.yaml указание где хранить Nodestore указывается так
sentryConfPy: | SENTRY_NODESTORE = "sentry_s3_nodestore.backend.S3NodeStorage" SENTRY_NODESTORE_OPTIONS = { "bucket_name": "название-бакета", "region": "ru-central1", "endpoint": "https://storage.yandexcloud.net", "aws_access_key_id": "aws_access_key_id", "aws_secret_access_key": "aws_secret_access_key", }
Динамическое формирование файла values.yaml для helm чарта Sentry
-
Файл values.yaml (
values_sentry.yaml) формируется используя шаблонvalues_sentry.yaml.tplиtemplatefile.tf -
В финальный конфиг через terraform функцию
templatefile()превращается в values_sentry.yaml -
В файлах
values_sentry.yaml.tplиtemplatefile.tfсодержится разные настройки.
Собираем кастомные image
Вы можете использовать docker image по умолчанию или собрать image. В этих кастомных image происходит установка сертификатов и установка sentry-s3-nodestore модуля. Сертификаты устанавливаются в python модуль certifi. Код сборок находится либо в этих репозиториях:
Sentry Kubernetes Hook: как это работает
Параметр asHook в Sentry Helm chart указывает, что основные контейнеры и миграции должны запуститься перед остальными контейнерами. Это нужно для первого запуска Sentry. После его можно отключить.
Планы на следующие посты про Sentry
-
Использовать Elasticsearch для NodeStore
-
Масштабируемость
-
Архитектура
-
Feature flags в sentry — https://github.com/getsentry/sentry/blob/master/src/sentry/features/temporary.py
Исходный terraform код
Скрытый текст
# Создание кластера ClickHouse в Яндекс Облаке resource "yandex_mdb_clickhouse_cluster" "sentry" { folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) # ID folder в Yandex Cloud name = "sentry" # Название кластера environment = "PRODUCTION" # Окружение (может быть также PRESTABLE) network_id = yandex_vpc_network.sentry.id # ID VPC-сети version = 24.8 # Версия ClickHouse clickhouse { resources { resource_preset_id = "s3-c2-m8" # Пресет ресурсов для узлов ClickHouse disk_type_id = "network-ssd" # Тип диска disk_size = 70 # Размер диска в ГБ } } zookeeper { resources { resource_preset_id = "s3-c2-m8" # Пресет ресурсов для узлов ZooKeeper disk_type_id = "network-ssd" # Тип диска disk_size = 34 # Размер диска в ГБ } } database { name = "sentry" # Имя базы данных в ClickHouse } user { name = local.clickhouse_user # Имя пользователя для доступа password = local.clickhouse_password # Пароль пользователя permission { database_name = "sentry" # Назначение прав доступа к БД "sentry" } } # Добавление хостов ClickHouse и ZooKeeper с привязкой к подсетям host { type = "CLICKHOUSE" # Тип узла — ClickHouse zone = yandex_vpc_subnet.sentry-a.zone subnet_id = yandex_vpc_subnet.sentry-a.id # Подсеть в зоне A } # Три узла ZooKeeper в разных зонах для отказоустойчивости host { type = "ZOOKEEPER" zone = yandex_vpc_subnet.sentry-a.zone subnet_id = yandex_vpc_subnet.sentry-a.id } host { type = "ZOOKEEPER" zone = yandex_vpc_subnet.sentry-b.zone subnet_id = yandex_vpc_subnet.sentry-b.id } host { type = "ZOOKEEPER" zone = yandex_vpc_subnet.sentry-d.zone subnet_id = yandex_vpc_subnet.sentry-d.id } timeouts { create = "60m" update = "60m" delete = "60m" } } # Вывод конфиденциальной информации о ClickHouse-кластере output "externalClickhouse" { value = { host = yandex_mdb_clickhouse_cluster.sentry.host[0].fqdn # FQDN первого ClickHouse-хоста database = one(yandex_mdb_clickhouse_cluster.sentry.database[*].name) # Имя БД httpPort = 8123 # HTTP порт ClickHouse tcpPort = 9000 # TCP порт ClickHouse username = local.clickhouse_user # Имя пользователя password = local.clickhouse_password # Пароль пользователя } sensitive = true # Отметка, что output содержит чувствительные данные }
example-python
import sentry_sdk sentry_sdk.init( dsn="http://xxxxx@sentry.apatsev.org.ru/2", traces_sample_rate=1.0, ) try: 1 / 0 except ZeroDivisionError: sentry_sdk.capture_exception()
ip-dns.tf
# Создание внешнего IP-адреса в Yandex Cloud resource "yandex_vpc_address" "addr" { name = "sentry-pip" # Имя ресурса внешнего IP-адреса external_ipv4_address { zone_id = yandex_vpc_subnet.sentry-a.zone # Зона доступности, где будет выделен IP-адрес } } # Создание публичной DNS-зоны в Yandex Cloud DNS resource "yandex_dns_zone" "apatsev-org-ru" { name = "apatsev-org-ru-zone" # Имя ресурса DNS-зоны zone = "apatsev.org.ru." # Доменное имя зоны (с точкой в конце) public = true # Указание, что зона является публичной # Привязка зоны к VPC-сети, чтобы можно было использовать приватный DNS внутри сети private_networks = [yandex_vpc_network.sentry.id] } # Создание DNS-записи типа A, указывающей на внешний IP resource "yandex_dns_recordset" "rs1" { zone_id = yandex_dns_zone.apatsev-org-ru.id # ID зоны, к которой принадлежит запись name = "sentry.apatsev.org.ru." # Полное имя записи (поддомен) type = "A" # Тип записи — A (IPv4-адрес) ttl = 200 # Время жизни записи в секундах data = [yandex_vpc_address.addr.external_ipv4_address[0].address] # Значение — внешний IP-адрес, полученный ранее }
Скрытый текст
# Создание сервисного аккаунта для управления Kubernetes resource "yandex_iam_service_account" "sa-k8s-editor" { folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) # ID folder в Yandex Cloud name = "sa-k8s-editor" # Имя сервисного аккаунта } # Назначение роли "editor" сервисному аккаунту на уровне папки resource "yandex_resourcemanager_folder_iam_member" "sa-k8s-editor-permissions" { folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) role = "editor" # Роль, дающая полные права на ресурсы папки member = "serviceAccount:${yandex_iam_service_account.sa-k8s-editor.id}" # Назначаемый участник } # Пауза, чтобы изменения IAM успели примениться до создания кластера resource "time_sleep" "wait_sa" { create_duration = "20s" depends_on = [ yandex_iam_service_account.sa-k8s-editor, yandex_resourcemanager_folder_iam_member.sa-k8s-editor-permissions ] } # Создание Kubernetes-кластера в Yandex Cloud resource "yandex_kubernetes_cluster" "sentry" { name = "sentry" # Имя кластера folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) network_id = yandex_vpc_network.sentry.id # Сеть, к которой подключается кластер master { version = "1.30" # Версия Kubernetes мастера zonal { zone = yandex_vpc_subnet.sentry-a.zone # Зона размещения мастера subnet_id = yandex_vpc_subnet.sentry-a.id # Подсеть для мастера } public_ip = true # Включение публичного IP для доступа к мастеру } # Сервисный аккаунт для управления кластером и нодами service_account_id = yandex_iam_service_account.sa-k8s-editor.id node_service_account_id = yandex_iam_service_account.sa-k8s-editor.id release_channel = "STABLE" # Канал обновлений # Зависимость от ожидания применения IAM-ролей depends_on = [time_sleep.wait_sa] } # Группа узлов для Kubernetes-кластера resource "yandex_kubernetes_node_group" "k8s-node-group" { description = "Node group for the Managed Service for Kubernetes cluster" name = "k8s-node-group" cluster_id = yandex_kubernetes_cluster.sentry.id version = "1.30" # Версия Kubernetes на нодах scale_policy { fixed_scale { size = 3 # Фиксированное количество нод } } allocation_policy { # Распределение нод по зонам отказоустойчивости location { zone = yandex_vpc_subnet.sentry-a.zone } location { zone = yandex_vpc_subnet.sentry-b.zone } location { zone = yandex_vpc_subnet.sentry-d.zone } } instance_template { platform_id = "standard-v2" # Тип виртуальной машины network_interface { nat = true # Включение NAT для доступа в интернет subnet_ids = [ yandex_vpc_subnet.sentry-a.id, yandex_vpc_subnet.sentry-b.id, yandex_vpc_subnet.sentry-d.id ] } resources { memory = 20 # ОЗУ cores = 4 # Кол-во ядер CPU } boot_disk { type = "network-ssd" # Тип диска size = 128 # Размер диска } } } # Настройка провайдера Helm для установки чарта в Kubernetes provider "helm" { kubernetes { host = yandex_kubernetes_cluster.sentry.master[0].external_v4_endpoint # Адрес API Kubernetes cluster_ca_certificate = yandex_kubernetes_cluster.sentry.master[0].cluster_ca_certificate # CA-сертификат exec { api_version = "client.authentication.k8s.io/v1beta1" args = ["k8s", "create-token"] # Команда получения токена через CLI Yandex.Cloud command = "yc" } } } # Установка ingress-nginx через Helm resource "helm_release" "ingress_nginx" { name = "ingress-nginx" repository = "https://kubernetes.github.io/ingress-nginx" chart = "ingress-nginx" version = "4.10.6" namespace = "ingress-nginx" create_namespace = true depends_on = [yandex_kubernetes_cluster.sentry] set { name = "controller.service.loadBalancerIP" value = yandex_vpc_address.addr.external_ipv4_address[0].address # Присвоение внешнего IP ingress-контроллеру } } # Вывод команды для получения kubeconfig output "k8s_cluster_credentials_command" { value = "yc managed-kubernetes cluster get-credentials --id ${yandex_kubernetes_cluster.sentry.id} --external --force" }
Скрытый текст
# Создание Kafka-кластера в Yandex Cloud # Здесь определяется Kafka кластер с именем "sentry" в Yandex Cloud с необходимыми параметрами конфигурации. resource "yandex_mdb_kafka_cluster" "sentry" { folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) # ID folder в Yandex Cloud name = "sentry" # Имя кластера environment = "PRODUCTION" # Среда (может быть PRESTABLE/PRODUCTION) network_id = yandex_vpc_network.sentry.id # Сеть VPC, в которой будет размещён кластер subnet_ids = [ # Список подсетей в разных зонах доступности yandex_vpc_subnet.sentry-a.id, yandex_vpc_subnet.sentry-b.id, yandex_vpc_subnet.sentry-d.id ] config { version = "3.6" # Версия kafka brokers_count = 1 # Кол-во брокеров в каждой зоне zones = [ # Зоны размещения брокеров yandex_vpc_subnet.sentry-a.zone, yandex_vpc_subnet.sentry-b.zone, yandex_vpc_subnet.sentry-d.zone ] assign_public_ip = false # Не присваивать публичный IP schema_registry = false # Без поддержки Schema Registry kafka { resources { resource_preset_id = "s3-c2-m8" # Пресет ресурсов для узлов PostgreSQL disk_type_id = "network-ssd" # Тип диска disk_size = 200 # Размер диска в ГБ } kafka_config { # оставьте пустым чтобы terraform не выводил что постоянно что то меняет в kafka_config # описание доступных настроек: https://terraform-provider.yandexcloud.net/resources/mdb_kafka_cluster.html#nested-schema-for3 } } } } # Список топиков Kafka с параметрами # Переменная локальная, которая содержит все топики Kafka и их параметры. locals { kafka_topics = { # Каждый ключ — имя топика. Значение — map опций конфигурации (может быть пустой) "events" = {}, "event-replacements" = {}, "snuba-commit-log" = { cleanup_policy = "CLEANUP_POLICY_COMPACT_AND_DELETE" min_compaction_lag_ms = "3600000" }, "cdc" = {}, "transactions" = {}, "snuba-transactions-commit-log" = { cleanup_policy = "CLEANUP_POLICY_COMPACT_AND_DELETE" min_compaction_lag_ms = "3600000" }, "snuba-metrics" = {}, "outcomes" = {}, "outcomes-dlq" = {}, "outcomes-billing" = {}, "outcomes-billing-dlq" = {}, "ingest-sessions" = {}, "snuba-metrics-commit-log" = { cleanup_policy = "CLEANUP_POLICY_COMPACT_AND_DELETE" min_compaction_lag_ms = "3600000" }, "scheduled-subscriptions-events" = {}, "scheduled-subscriptions-transactions" = {}, "scheduled-subscriptions-metrics" = {}, "scheduled-subscriptions-generic-metrics-sets" = {}, "scheduled-subscriptions-generic-metrics-distributions" = {}, "scheduled-subscriptions-generic-metrics-counters" = {}, "scheduled-subscriptions-generic-metrics-gauges" = {}, "events-subscription-results" = {}, "transactions-subscription-results" = {}, "metrics-subscription-results" = {}, "generic-metrics-subscription-results" = {}, "snuba-queries" = {}, "processed-profiles" = {}, "profiles-call-tree" = {}, "snuba-profile-chunks" = {}, "ingest-replay-events" = { max_message_bytes = "15000000" }, "snuba-generic-metrics" = {}, "snuba-generic-metrics-sets-commit-log" = { cleanup_policy = "CLEANUP_POLICY_COMPACT_AND_DELETE" min_compaction_lag_ms = "3600000" }, "snuba-generic-metrics-distributions-commit-log" = { cleanup_policy = "CLEANUP_POLICY_COMPACT_AND_DELETE" min_compaction_lag_ms = "3600000" }, "snuba-generic-metrics-counters-commit-log" = { cleanup_policy = "CLEANUP_POLICY_COMPACT_AND_DELETE" min_compaction_lag_ms = "3600000" }, "snuba-generic-metrics-gauges-commit-log" = { cleanup_policy = "CLEANUP_POLICY_COMPACT_AND_DELETE" min_compaction_lag_ms = "3600000" }, "generic-events" = {}, "snuba-generic-events-commit-log" = { cleanup_policy = "CLEANUP_POLICY_COMPACT_AND_DELETE" min_compaction_lag_ms = "3600000" }, "group-attributes" = {}, "snuba-dead-letter-metrics" = {}, "snuba-dead-letter-generic-metrics" = {}, "snuba-dead-letter-replays" = {}, "snuba-dead-letter-generic-events" = {}, "snuba-dead-letter-querylog" = {}, "snuba-dead-letter-group-attributes" = {}, "ingest-attachments" = {}, "ingest-attachments-dlq" = {}, "ingest-transactions" = {}, "ingest-transactions-dlq" = {}, "ingest-transactions-backlog" = {}, "ingest-events" = {}, "ingest-events-dlq" = {}, "ingest-replay-recordings" = {}, "ingest-metrics" = {}, "ingest-metrics-dlq" = {}, "ingest-performance-metrics" = {}, "ingest-feedback-events" = {}, "ingest-feedback-events-dlq" = {}, "ingest-monitors" = {}, "monitors-clock-tasks" = {}, "monitors-clock-tick" = {}, "monitors-incident-occurrences" = {}, "profiles" = {}, "ingest-occurrences" = {}, "snuba-spans" = {}, "snuba-eap-spans-commit-log" = {}, "scheduled-subscriptions-eap-spans" = {}, "eap-spans-subscription-results" = {}, "snuba-eap-mutations" = {}, "snuba-lw-deletions-generic-events" = {}, "shared-resources-usage" = {}, "buffered-segments" = {}, "buffered-segments-dlq" = {}, "uptime-configs" = {}, "uptime-results" = {}, "snuba-uptime-results" = {}, "task-worker" = {}, "snuba-ourlogs" = {} } } # Создание Kafka-топиков на основе описания в locals.kafka_topics # Итерируем по списку топиков и создаём их в Kafka с конфигурациями. resource "yandex_mdb_kafka_topic" "topics" { for_each = local.kafka_topics # Итерируемся по каждому топику cluster_id = yandex_mdb_kafka_cluster.sentry.id name = each.key # Имя топика partitions = 1 # Кол-во партиций replication_factor = 1 # Фактор репликации (можно увеличить для отказоустойчивости) topic_config { cleanup_policy = lookup(each.value, "cleanup_policy", null) min_compaction_lag_ms = lookup(each.value, "min_compaction_lag_ms", null) } timeouts { create = "60m" update = "60m" delete = "60m" } } # Локальная переменная со списком имен всех топиков (используется для прав доступа) # Список всех имен топиков, используемых для назначения прав доступа. locals { kafka_permissions = keys(local.kafka_topics) } # Создание пользователя Kafka и назначение прав доступа к каждому топику # Создаём пользователя Kafka и настраиваем права доступа для консьюмера и продюсера. resource "yandex_mdb_kafka_user" "sentry" { cluster_id = yandex_mdb_kafka_cluster.sentry.id name = local.kafka_user # Имя пользователя password = local.kafka_password # Пароль пользователя # Назначение роли "консьюмер" для каждого топика dynamic "permission" { for_each = toset(local.kafka_permissions) content { topic_name = permission.value role = "ACCESS_ROLE_CONSUMER" } } # Назначение роли "продюсер" для каждого топика dynamic "permission" { for_each = toset(local.kafka_permissions) content { topic_name = permission.value role = "ACCESS_ROLE_PRODUCER" } } } # Вывод Kafka-подключения в виде структурированных данных (sensitive — чувствительные данные скрываются) # Данный вывод предоставляет информацию о подключении к Kafka с учётом безопасности. output "externalKafka" { description = "Kafka connection details in structured format" value = { cluster = [ for host in yandex_mdb_kafka_cluster.sentry.host : { host = host.name port = 9091 # 9091 — если используется SSL, иначе 9092 } if host.role == "KAFKA" ] sasl = { mechanism = "SCRAM-SHA-512" # Механизм аутентификации (например, PLAIN, SCRAM) username = local.kafka_user password = local.kafka_password } security = { protocol = "SASL_SSL" # Использовать SASL_SSL (или SASL_PLAINTEXT при отсутствии SSL) } } sensitive = true }
Скрытый текст
# Получаем информацию о конфигурации клиента Yandex data "yandex_client_config" "client" {} # Генерация случайного пароля для Kafka resource "random_password" "kafka" { length = 20 # Длина пароля 20 символов special = false # Без специальных символов min_numeric = 4 # Минимум 4 цифры в пароле min_upper = 4 # Минимум 4 заглавные буквы в пароле } # Генерация случайного пароля для ClickHouse resource "random_password" "clickhouse" { length = 20 # Длина пароля 20 символов special = false # Без специальных символов min_numeric = 4 # Минимум 4 цифры в пароле min_upper = 4 # Минимум 4 заглавные буквы в пароле } # Генерация случайного пароля для Redis resource "random_password" "redis" { length = 20 # Длина пароля 20 символов special = false # Без специальных символов min_numeric = 4 # Минимум 4 цифры в пароле min_upper = 4 # Минимум 4 заглавные буквы в пароле } # Генерация случайного пароля для PostgreSQL resource "random_password" "postgres" { length = 20 # Длина пароля 20 символов special = false # Без специальных символов min_numeric = 4 # Минимум 4 цифры в пароле min_upper = 4 # Минимум 4 заглавные буквы в пароле } # Генерация случайного пароля для администратора Sentry resource "random_password" "sentry_admin_password" { length = 20 # Длина пароля 20 символов special = false # Без специальных символов min_numeric = 4 # Минимум 4 цифры в пароле min_upper = 4 # Минимум 4 заглавные буквы в пароле } # Локальные переменные для настройки инфраструктуры locals { folder_id = data.yandex_client_config.client.folder_id # ID папки в Yandex Cloud sentry_admin_password = random_password.sentry_admin_password.result # Сгенерированный пароль администратора Sentry kafka_user = "sentry" # Имя пользователя для Kafka kafka_password = random_password.kafka.result # Сгенерированный пароль для Kafka clickhouse_user = "sentry" # Имя пользователя для ClickHouse clickhouse_password = random_password.clickhouse.result # Сгенерированный пароль для ClickHouse redis_password = random_password.redis.result # Сгенерированный пароль для Redis postgres_password = random_password.postgres.result # Сгенерированный пароль для PostgreSQL filestore_bucket = "sentry-bucket-apatsev-filestore-test" # Имя бакета для Filestore nodestore_bucket = "sentry-bucket-apatsev-nodestore-test" # Имя бакета для Nodestore } # Выводим сгенерированные пароли для сервисов output "generated_passwords" { description = "Map of generated passwords for services" # Описание вывода value = { kafka_password = random_password.kafka.result # Пароль для Kafka clickhouse_password = random_password.clickhouse.result # Пароль для ClickHouse redis_password = random_password.redis.result # Пароль для Redis postgres_password = random_password.postgres.result # Пароль для PostgreSQL } sensitive = true # Скрывает пароли в логах, но они доступны через `terraform output` }
Скрытый текст
# Ресурс для создания сети VPC в Yandex Cloud resource "yandex_vpc_network" "sentry" { name = "vpc" # Имя сети VPC folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) # ID папки, либо из локальной переменной, либо из конфигурации клиента Yandex Cloud } # Ресурс для создания подсети в зоне "ru-central1-a" resource "yandex_vpc_subnet" "sentry-a" { folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) # ID папки, либо из локальной переменной, либо из конфигурации клиента Yandex Cloud v4_cidr_blocks = ["10.0.1.0/24"] # CIDR блок для подсети (IP-диапазон) zone = "ru-central1-a" # Зона, где будет размещена подсеть network_id = yandex_vpc_network.sentry.id # ID сети, к которой будет привязана подсеть } # Ресурс для создания подсети в зоне "ru-central1-b" resource "yandex_vpc_subnet" "sentry-b" { folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) # ID папки, либо из локальной переменной, либо из конфигурации клиента Yandex Cloud v4_cidr_blocks = ["10.0.2.0/24"] # CIDR блок для подсети (IP-диапазон) zone = "ru-central1-b" # Зона, где будет размещена подсеть network_id = yandex_vpc_network.sentry.id # ID сети, к которой будет привязана подсеть } # Ресурс для создания подсети в зоне "ru-central1-d" resource "yandex_vpc_subnet" "sentry-d" { folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) # ID папки, либо из локальной переменной, либо из конфигурации клиента Yandex Cloud v4_cidr_blocks = ["10.0.3.0/24"] # CIDR блок для подсети (IP-диапазон) zone = "ru-central1-d" # Зона, где будет размещена подсеть network_id = yandex_vpc_network.sentry.id # ID сети, к которой будет привязана подсеть }
Скрытый текст
# Создание кластера PostgreSQL в Yandex Cloud resource "yandex_mdb_postgresql_cluster" "postgresql_cluster" { # Название кластера name = "sentry" # Среда, в которой развертывается кластер environment = "PRODUCTION" # Сеть, в которой будет размещен кластер network_id = yandex_vpc_network.sentry.id # Конфигурация кластера PostgreSQL config { # Версия PostgreSQL version = "16" # Версия PostgreSQL # Включение автофейловера (автоматический перевод на другой узел при сбое) autofailover = true # Период хранения резервных копий в днях backup_retain_period_days = 7 resources { # Размер диска в ГБ disk_size = 129 # Тип диска disk_type_id = "network-ssd" # Пресет ресурсов для узлов PostgreSQL resource_preset_id = "s3-c2-m8" } } # Хост в зоне "ru-central1-a" host { zone = "ru-central1-a" subnet_id = yandex_vpc_subnet.sentry-a.id } # Хост в зоне "ru-central1-b" host { zone = "ru-central1-b" subnet_id = yandex_vpc_subnet.sentry-b.id } # Хост в зоне "ru-central1-d" host { zone = "ru-central1-d" subnet_id = yandex_vpc_subnet.sentry-d.id } } # Создание базы данных в PostgreSQL resource "yandex_mdb_postgresql_database" "postgresql_database" { # Идентификатор кластера, к которому относится база данных cluster_id = yandex_mdb_postgresql_cluster.postgresql_cluster.id # Имя базы данных name = "sentry" # Владелец базы данных (пользователь) owner = yandex_mdb_postgresql_user.postgresql_user.name # Установка расширений для базы данных extension { # Расширение для работы с типом данных citext (регистр не учитывается при сравнении строк) name = "citext" } # Зависимость от ресурса пользователя depends_on = [yandex_mdb_postgresql_user.postgresql_user] } # Создание пользователя PostgreSQL resource "yandex_mdb_postgresql_user" "postgresql_user" { # Идентификатор кластера, к которому принадлежит пользователь cluster_id = yandex_mdb_postgresql_cluster.postgresql_cluster.id # Имя пользователя name = "sentry" # Пароль пользователя password = local.postgres_password # Ограничение по количеству соединений conn_limit = 300 # Разрешения для пользователя (пока пустой список) grants = [] } # Вывод внешних данных для подключения к базе данных PostgreSQL output "externalPostgresql" { value = { # Пароль для подключения (значение скрыто) password = local.postgres_password # Адрес хоста для подключения (с динамическим именем хоста на основе ID кластера) host = "c-${yandex_mdb_postgresql_cluster.postgresql_cluster.id}.rw.mdb.yandexcloud.net" # Порт для подключения к базе данных port = 6432 # Имя пользователя для подключения username = yandex_mdb_postgresql_user.postgresql_user.name # Имя базы данных для подключения database = yandex_mdb_postgresql_database.postgresql_database.name } # Помечаем значение как чувствительное (не выводить в логах) sensitive = true }
Скрытый текст
# Создание кластера Redis в Yandex Managed Service for Redis resource "yandex_mdb_redis_cluster" "sentry" { name = "sentry" # Название кластера folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) # ID folder в Yandex Cloud network_id = yandex_vpc_network.sentry.id # ID сети VPC environment = "PRODUCTION" # Среда (может быть PRODUCTION или PRESTABLE) tls_enabled = true # Включение TLS для защищённого подключения config { password = local.redis_password # Пароль для подключения к Redis maxmemory_policy = "ALLKEYS_LRU" # Политика очистки памяти: удаляются наименее используемые ключи version = "7.2" # Версия Redis } resources { resource_preset_id = "hm3-c2-m8" # Тип конфигурации по CPU и памяти disk_type_id = "network-ssd" # Тип диска disk_size = 65 # Размер диска в ГБ } host { zone = "ru-central1-a" # Зона доступности subnet_id = yandex_vpc_subnet.sentry-a.id # ID подсети } } # Вывод внешних параметров подключения к Redis output "externalRedis" { value = { # host = yandex_mdb_redis_cluster.sentry.host[0].fqdn # FQDN первого хоста Redis # Адрес хоста для подключения (с динамическим именем хоста на основе ID кластера) host = "c-${yandex_mdb_redis_cluster.sentry.id}.rw.mdb.yandexcloud.net" port = 6380 # Порт Redis SSL password = local.redis_password # Пароль подключения } sensitive = true # Значение помечено как чувствительное }
s3_filestore.tf
Скрытый текст
# Создание статического ключа доступа для учетной записи сервиса в Yandex IAM resource "yandex_iam_service_account_static_access_key" "filestore_bucket_key" { # ID учетной записи сервиса, для которой создается ключ доступа service_account_id = yandex_iam_service_account.sa-s3.id # Описание для ключа доступа description = "static access key for object storage" } # Создание бакета (хранилища) в Yandex Object Storage resource "yandex_storage_bucket" "filestore" { # Название бакета bucket = local.filestore_bucket # Важно: команда sentry cleanup не удаляет файлы, хранящиеся во внешнем хранилище, таком как GCS или S3. # https://develop.sentry.dev/self-hosted/experimental/external-storage/ # Правило жизненного цикла объектов в бакете lifecycle_rule { # Уникальный идентификатор правила id = "delete-after-30-days" # Флаг, указывающий, что правило активно enabled = true # Параметры истечения срока хранения объектов expiration { # Объекты будут автоматически удаляться через 30 дней после загрузки days = 30 } } # Доступ и секретный ключ, полученные от статического ключа доступа access_key = yandex_iam_service_account_static_access_key.filestore_bucket_key.access_key secret_key = yandex_iam_service_account_static_access_key.filestore_bucket_key.secret_key # ID папки, в которой будет размещен бакет folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) # ID folder в Yandex Cloud # Указываем зависимость от ресурса IAM-члена, который должен быть создан до бакета depends_on = [ yandex_resourcemanager_folder_iam_member.sa-admin-s3, ] } # Вывод ключа доступа для бакета (с чувствительным значением) output "access_key_for_filestore_bucket" { # Описание вывода description = "access_key filestore_bucket" # Значение для вывода (ключ доступа к бакету) value = yandex_storage_bucket.filestore.access_key # Указание, что выводимое значение чувствительно sensitive = true } # Вывод секретного ключа для бакета (с чувствительным значением) output "secret_key_for_filestore_bucket" { # Описание вывода description = "secret_key filestore_bucket" # Значение для вывода (секретный ключ для бакета) value = yandex_storage_bucket.filestore.secret_key # Указание, что выводимое значение чувствительно sensitive = true }
s3_nodestore.tf
Скрытый текст
# Создание статического ключа доступа для сервисного аккаунта resource "yandex_iam_service_account_static_access_key" "nodestore_bucket_key" { # Привязка к существующему сервисному аккаунту service_account_id = yandex_iam_service_account.sa-s3.id # Описание ключа доступа description = "static access key for object storage" } # Создание бакета для хранения объектов resource "yandex_storage_bucket" "nodestore" { # Имя бакета, которое определено в локальной переменной bucket = local.nodestore_bucket # Важно: команда sentry cleanup не удаляет файлы, хранящиеся во внешнем хранилище, таком как GCS или S3. # https://develop.sentry.dev/self-hosted/experimental/external-storage/ # Правило жизненного цикла объектов в бакете lifecycle_rule { # Уникальный идентификатор правила id = "delete-after-30-days" # Флаг, указывающий, что правило активно enabled = true # Параметры истечения срока хранения объектов expiration { # Объекты будут автоматически удаляться через 30 дней после загрузки days = 30 } } # Привязка статического ключа доступа (access_key) и секретного ключа (secret_key) access_key = yandex_iam_service_account_static_access_key.nodestore_bucket_key.access_key secret_key = yandex_iam_service_account_static_access_key.nodestore_bucket_key.secret_key # Идентификатор папки, в которой будет создан бакет folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) # ID folder в Yandex Cloud # Зависимость от другого ресурса, чтобы этот бакет был создан после предоставления прав сервисному аккаунту depends_on = [ yandex_resourcemanager_folder_iam_member.sa-admin-s3, ] } # Вывод ключа доступа для бакета, чтобы другие ресурсы могли его использовать output "access_key_for_nodestore_bucket" { # Описание, что это ключ доступа description = "access_key nodestore_bucket" # Значение — это ключ доступа, привязанный к бакету value = yandex_storage_bucket.nodestore.access_key # Указание, что это чувствительное значение, и его не следует показывать в логах sensitive = true } # Вывод секретного ключа для бакета output "secret_key_for_nodestore_bucket" { # Описание, что это секретный ключ description = "secret_key nodestore_bucket" # Значение — это секретный ключ, привязанный к бакету value = yandex_storage_bucket.nodestore.secret_key # Указание, что это чувствительное значение, и его не следует показывать в логах sensitive = true }
s3_service_account.tf
# Создание сервисного аккаунта в Yandex IAM resource "yandex_iam_service_account" "sa-s3" { # Имя сервисного аккаунта name = "sa-test-apatsev" } # Присваивание роли IAM для сервисного аккаунта resource "yandex_resourcemanager_folder_iam_member" "sa-admin-s3" { # Идентификатор папки, в которой будет назначена роль folder_id = coalesce(local.folder_id, data.yandex_client_config.client.folder_id) # ID folder в Yandex Cloud # Роль, которую мы назначаем сервисному аккаунту role = "storage.admin" # Сервисный аккаунт, которому будет назначена роль member = "serviceAccount:${yandex_iam_service_account.sa-s3.id}" }
Скрытый текст
# Ресурс null_resource используется для выполнения локальной команды, # генерирующей файл конфигурации Sentry на основе шаблона resource "null_resource" "write_sentry_config" { provisioner "local-exec" { # Команда записывает сгенерированную строку (YAML) в файл values_sentry.yaml command = "echo '${local.sentry_config}' > values_sentry.yaml" } triggers = { # Триггер перезапуска ресурса при изменении содержимого values_sentry.yaml.tpl sentry_config = local.sentry_config } } locals { # Локальная переменная с конфигурацией Sentry, генерируемая из шаблона values_sentry.yaml.tpl sentry_config = templatefile("values_sentry.yaml.tpl", { # Пароль администратора Sentry sentry_admin_password = local.sentry_admin_password # Email пользователя-администратора user_email = "admin@sentry.apatsev.org.ru" # URL системы Sentry # В этом коде не стал делать переменные чтобы не усложнять код system_url = "http://sentry.apatsev.org.ru" # TODO в след посте использовать переменную # Включение/отключение Nginx nginx_enabled = false # Использование Ingress для доступа к Sentry ingress_enabled = true # Имя хоста, используемого Ingress # В этом коде не стал делать переменные чтобы не усложнять код ingress_hostname = "sentry.apatsev.org.ru" # TODO в след посте использовать переменную # Имя класса Ingress-контроллера ingress_class_name = "nginx" # Стиль регулярных путей в Ingress ingress_regex_path_style = "nginx" # Аннотации Ingress для настройки nginx ingress_annotations = { proxy_body_size = "200m" # Максимальный размер тела запроса proxy_buffers_number = "16" # Количество буферов proxy_buffer_size = "32k" # Размер каждого буфера } # Настройки S3-хранилища для файлового хранилища (filestore) filestore = { s3 = { accessKey = yandex_storage_bucket.filestore.access_key secretKey = yandex_storage_bucket.filestore.secret_key bucketName = yandex_storage_bucket.filestore.bucket } } # Настройки S3-хранилища для хранения событий (nodestore) nodestore = { s3 = { accessKey = yandex_storage_bucket.nodestore.access_key secretKey = yandex_storage_bucket.nodestore.secret_key bucketName = yandex_storage_bucket.nodestore.bucket } } # Отключение встроенного PostgreSQL, использование внешнего postgresql_enabled = false # Настройки подключения к внешнему PostgreSQL external_postgresql = { password = local.postgres_password host = "c-${yandex_mdb_postgresql_cluster.postgresql_cluster.id}.rw.mdb.yandexcloud.net" port = 6432 username = yandex_mdb_postgresql_user.postgresql_user.name database = yandex_mdb_postgresql_database.postgresql_database.name } # Отключение встроенного Redis, использование внешнего redis_enabled = false # Настройки подключения к внешнему Redis external_redis = { password = local.redis_password host = "c-${yandex_mdb_redis_cluster.sentry.id}.rw.mdb.yandexcloud.net" port = 6380 # 6380 — если используется SSL, иначе 6379 } # Настройки внешнего Kafka external_kafka = { cluster = [ # Получение всех узлов Kafka с ролью "KAFKA" for host in yandex_mdb_kafka_cluster.sentry.host : { host = host.name port = 9091 # 9091 — если используется SSL, иначе 9092 } if host.role == "KAFKA" ] # Настройки аутентификации SASL sasl = { mechanism = "SCRAM-SHA-512" username = local.kafka_user password = local.kafka_password } # Настройки безопасности Kafka security = { protocol = "SASL_SSL" # Использовать SASL_SSL (или SASL_PLAINTEXT при отсутствии SSL) } } # Отключение встроенного Kafka kafka_enabled = false # Отключение встроенного Zookeeper zookeeper_enabled = false # Отключение встроенного Clickhouse, использование внешнего clickhouse_enabled = false # Настройки подключения к внешнему Clickhouse external_clickhouse = { password = local.clickhouse_password host = yandex_mdb_clickhouse_cluster.sentry.host[0].fqdn database = one(yandex_mdb_clickhouse_cluster.sentry.database[*].name) httpPort = 8123 tcpPort = 9000 username = local.clickhouse_user } }) }
values_sentry.yaml.tpl
Скрытый текст
# Пользовательская конфигурация для Sentry user: password: "${sentry_admin_password}" # Пароль администратора Sentry email: "${user_email}" # Email администратора # Системная информация system: url: "${system_url}" # URL-адрес системы # Контейнерные образы компонентов Sentry images: sentry: repository: ghcr.io/patsevanton/ghcr-sentry-custom-images # Кастомный образ Sentry snuba: repository: ghcr.io/patsevanton/ghcr-snuba-custom-images # Кастомный образ Snuba relay: repository: ghcr.io/patsevanton/ghcr-relay-custom-images # Кастомный образ Relay # Настройка NGINX nginx: enabled: ${nginx_enabled} # Включен ли встроенный NGINX # Настройка ingress-контроллера ingress: enabled: ${ingress_enabled} # Включение ingress hostname: "${ingress_hostname}" # Хостнейм для доступа ingressClassName: "${ingress_class_name}" # Класс ingress-контроллера regexPathStyle: "${ingress_regex_path_style}" # Использование регулярных выражений в путях annotations: nginx.ingress.kubernetes.io/proxy-body-size: "${ingress_annotations.proxy_body_size}" # Максимальный размер тела запроса nginx.ingress.kubernetes.io/proxy-buffers-number: "${ingress_annotations.proxy_buffers_number}" # Количество буферов nginx.ingress.kubernetes.io/proxy-buffer-size: "${ingress_annotations.proxy_buffer_size}" # Размер буфера # Настройки файлового хранилища filestore: backend: "s3" # Тип backend для хранения файлов — S3 s3: accessKey: "${filestore.s3.accessKey}" # Access Key от S3 secretKey: "${filestore.s3.secretKey}" # Secret Key от S3 region_name: ru-central1 # Регион — Яндекс.Облако bucketName: "${filestore.s3.bucketName}" # Название бакета endpointUrl: "https://storage.yandexcloud.net" # Endpoint для доступа к S3 location: "debug-files" # Папка для хранения debug-файлов # Настройки NODESTORE хранилища config: sentryConfPy: | SENTRY_NODESTORE = "sentry_s3_nodestore.backend.S3NodeStorage" SENTRY_NODESTORE_OPTIONS = { "bucket_name": "${nodestore.s3.bucketName}", "region": "ru-central1", "endpoint": "https://storage.yandexcloud.net", "aws_access_key_id": "${nodestore.s3.accessKey}", "aws_secret_access_key": "${nodestore.s3.secretKey}", } # Встроенная PostgreSQL база данных postgresql: enabled: ${postgresql_enabled} # Использовать ли встроенный PostgreSQL не для NodeStore, а для нужд самой Sentry # Конфигурация внешней PostgreSQL базы данных externalPostgresql: password: "${external_postgresql.password}" # Пароль БД host: "${external_postgresql.host}" # Хост БД port: ${external_postgresql.port} # Порт username: "${external_postgresql.username}" # Имя пользователя database: "${external_postgresql.database}" # Название БД sslMode: require # Добавляем если нужен SSL, если SSL не нужен удаляем эту строку # Встроенный Redis redis: enabled: ${redis_enabled} # Включить ли встроенный Redis # Подключение к внешнему Redis externalRedis: password: "${external_redis.password}" # Пароль Redis host: "${external_redis.host}" # Хост Redis port: ${external_redis.port} # Порт Redis ssl: true # Добавляем если нужен SSL, если SSL не нужен удаляем эту строку # Внешний кластер Kafka externalKafka: cluster: %{ for kafka_host in external_kafka.cluster ~} - host: "${kafka_host.host}" # Хост Kafka брокера port: ${kafka_host.port} # Порт Kafka брокера %{ endfor } sasl: mechanism: "${external_kafka.sasl.mechanism}" # Механизм аутентификации (например, PLAIN, SCRAM) username: "${external_kafka.sasl.username}" # Имя пользователя Kafka password: "${external_kafka.sasl.password}" # Пароль Kafka security: protocol: "${external_kafka.security.protocol}" # Протокол безопасности (например, SASL_SSL, SASL_PLAINTEXT) # Встроенный кластер Kafka kafka: enabled: ${kafka_enabled} # Включить встроенный Kafka # Встроенный ZooKeeper zookeeper: enabled: ${zookeeper_enabled} # Включить встроенный ZooKeeper # Встроенный Clickhouse clickhouse: enabled: ${clickhouse_enabled} # Включить встроенный Clickhouse # Подключение к внешнему Clickhouse externalClickhouse: password: "${external_clickhouse.password}" # Пароль host: "${external_clickhouse.host}" # Хост database: "${external_clickhouse.database}" # Название БД httpPort: ${external_clickhouse.httpPort} # HTTP-порт tcpPort: ${external_clickhouse.tcpPort} # TCP-порт username: "${external_clickhouse.username}" # Имя пользователя
Кастомный image
Проект выглядит так
├── ca-certs │ └── yandex-ca.crt ├── Dockerfile ├── enhance-image.sh
Dockerfile
ARG SENTRY_IMAGE FROM ${SENTRY_IMAGE} COPY ca-certs/*.crt /usr/local/share/ca-certificates/ COPY enhance-image.sh /usr/src/sentry/ RUN if [ -s /usr/src/sentry/enhance-image.sh ]; then \ /usr/src/sentry/enhance-image.sh; \ fi RUN if [ -s /usr/src/sentry/requirements.txt ]; then \ echo "sentry/requirements.txt is deprecated, use sentry/enhance-image.sh - see https://github.com/getsentry/self-hosted#enhance-sentry-image"; \ pip install -r /usr/src/sentry/requirements.txt; \ fi
enhance-image.sh
#!/bin/bash pip install https://github.com/pavels/sentry-s3-nodestore/releases/download/v1.0.3/sentry-s3-nodestore-1.0.3.tar.gz for c in $(ls -1 /usr/local/share/ca-certificates/) do cat /usr/local/share/ca-certificates/$c >> $(python3 -m certifi) && echo >> $(python3 -m certifi) done update-ca-certificates
Github action .github/workflows/build.yml
name: Build and Push Sentry Docker Images on: push: branches: - main workflow_dispatch: env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build: strategy: matrix: SENTRY_VERSION: [25.2.0, 25.3.0] runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout code uses: actions/checkout@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build Docker image run: | docker build \ --build-arg SENTRY_IMAGE=getsentry/sentry:${{ matrix.SENTRY_VERSION }} \ -t $REGISTRY/${{ env.IMAGE_NAME }}:${{ matrix.SENTRY_VERSION }} . - name: Push Docker image run: | docker push $REGISTRY/${{ env.IMAGE_NAME }}:${{ matrix.SENTRY_VERSION }}
ссылка на оригинал статьи https://habr.com/ru/articles/900526/
Добавить комментарий