Запускаем Sentry в Kubernetes в Яндекс облаке и храним Nodestore в S3

от автора

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

Запускаем инфраструктуру:

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, параметризуем через Terraform templatefile

    • 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

Исходный terraform код

clickhouse.tf

Скрытый текст
# Создание кластера 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-адрес, полученный ранее } 

k8s.tf

Скрытый текст
# Создание сервисного аккаунта для управления 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.tf

Скрытый текст
# Создание 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 } 

locals.tf

Скрытый текст
# Получаем информацию о конфигурации клиента 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` } 

net.tf

Скрытый текст
# Ресурс для создания сети 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 сети, к которой будет привязана подсеть } 

postgres.tf

Скрытый текст
# Создание кластера 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.tf

Скрытый текст
# Создание кластера 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}" } 

templatefile.tf

Скрытый текст
# Ресурс 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *