
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 из Pythonclickhouse.tf
— managed ClickHouse (Yandex Cloud)ip-dns.tf
– настраивает IP-адреса и записи DNS для ресурсов.k8s.tf
— managed Kuberbetes (Yandex Cloud) для деплоя Sentrykafka.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 }}