Быстрый старт
Данный гид поможет вам развернуть инфраструктуру Medikt в Yandex Cloud с помощью Terraform и Kubernetes.
Предварительные требования
- Доступ к Yandex Cloud с правами создания ресурсов (рекомендуется роль admin)
- Terraform и Yandex Cloud CLI (yc)
- Права на создание ресурсов в облаке
- Делегированная в yandex-cloud dns-зона (должна быть делегирована до создания днс-зоны как ресурса)
Установка и настройка Yandex CLI
Перед использованием команд yc создайте и активируйте профиль:
yc init
Дополнительная информация: Создание профиля
I. Подготовка инфраструктуры
1. Создать каталог в Yandex Cloud
Создайте новый каталог для развертывания инфраструктуры Medikt через консоль Yandex Cloud или CLI:
yc iam project create --name medikt-prod
2. Создать сервисный аккаунт для Terraform
yc iam service-account create --name terraform --folder-id <folder_id>
Назначьте роли сервисному аккаунту (рекомендуется роль admin):
yc resource-manager folder add-access-binding \
--folder-id <folder_id> \
--role admin \
--subject serviceAccount:<sa_id>
Создайте IAM токен:
yc iam create-token --service-account-name terraform
3. Создать VPC и подсеть
Создайте VPC с одной подсетью для развертывания всех ресурсов:
module "vpc" {
source = "./modules/vpc"
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
provider-zone = var.provider-zone
provider-token = var.provider-token
zone-name = "medikt-net-zones"
dns-zone = "medikt.net"
subnet_cidr = "172.16.50.0/24"
}
Примечание: Чтобы получить внешний IP адрес для DNS записей, нужно сначала зарезервировать статический IP в облаке.
4. Создать DNS-зону без записей
Делегируйте DNS зону medikt.net в Yandex DNS без создания DNS записей:
module "dns_zone" {
source = "./modules/dns_zone"
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
provider-zone = var.provider-zone
provider-token = var.provider-token
dns_zone = "medikt.net"
dns_zone_is_public = true
dns_zone_private_networks = [module.vpc.vpc_id]
}
Примечание: DNS записи будут созданы после развертывания NLB ingress, так как они зависят от внешнего IP балансировщика.
5. Создать Yandex Lockbox для хранения секретов
Создайте Lockbox secret для хранения всех секретов:
yc lockbox secret create --name infra-secrets
Добавьте секреты (пример):
yc lockbox secret version add \
--secret-id <secret_id> \
--payload '[{"key":"mongodb_user_passwd","text_value":"<password>"}, ...]'
Примечание: Не забудьте сохранить значения секретов для последующих шагов.
II. Базовая инфраструктура
6. Создать управляемый кластер Kubernetes
module "k8s" {
source = "./modules/k8s_master"
vpc_id = module.vpc.vpc_id
subnet_id = module.vpc.subnet_id
k8s_cluster_name = "k8s-medikt-net"
provider-zone = var.provider-zone
provider-token = var.provider-token
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
zone-name = var.zone-name
k8s_public_ip = false
cluster_subnets = ["172.16.50.0/24"]
master_whitelist_cidr = ["172.16.50.0/24"]
}
Важно:
k8s_public_ip = false- для сетевой связности к кластеру Kubernetes необходим туннель. В текущей инфраструктуре это обеспечивается ВМgw0.medikt.net. Можно развернуть ВМ для туннелирования до деплоя или руками.
Порядок развертывания: Кластер Kubernetes разворачивается ДО создания DNS записей, так как для них используется NLB ingress (создается после K8s).
7. Создать node groups
Создайте node groups с необходимыми лейблами.
Примечание: Для тестового развертывания можно создать все лейблы, используемые в чартах, или удалить настройки affinity из самих чартов.
Core nodes (web-api, web) - для nlb_ingress:
module "core_nodes_ssd" {
source = "./modules/k8s_nodes"
provider-folderid = var.provider-folderid
provider-zone = var.provider-zone
provider-token = var.provider-token
k8s_cluster_id = module.k8s.id
k8s_cluster_name = module.k8s.name
preemptible_k8s_nodes = false
node_group_name = "web-api-ssd"
k8s_worker_nodes_mem = 8
k8s_worker_nodes_cores = 8
k8s_worker_nodes_boot_disk_type = "network-ssd"
node_labels = {
"api_affinity" = "true"
"web_affinity" = "true"
}
subnet_ids = [module.vpc.subnet_id]
security_group_ids = [module.k8s.k8s-main-sg, module.k8s.k8s-public-services]
auto_scale = {
initial = 2
max = 5
min = 2
}
}
Transcoder nodes:
module "transcoder_workers8" {
source = "./modules/k8s_nodes"
provider-folderid = var.provider-folderid
provider-zone = var.provider-zone
provider-token = var.provider-token
k8s_cluster_id = module.k8s.id
k8s_cluster_name = module.k8s.name
node_group_name = "transcoders"
preemptible_k8s_nodes = true
k8s_worker_nodes_mem = 8
k8s_worker_nodes_cores = 8
node_labels = {
"t_affinity" = "true"
}
subnet_ids = [module.vpc.subnet_id]
security_group_ids = [module.k8s.k8s-main-sg, module.k8s.k8s-public-services]
auto_scale = {
initial = 1
max = 5
min = 1
}
}
8. Создать Yandex Message Queue (transcoder_queues)
module "transcoder_queues" {
source = "./modules/transcoder_queues"
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
provider-zone = var.provider-zone
provider-token = var.provider-token
prefix = "medikt-net"
}
9. Создать S3 бакеты
Private bucket (для внутренних файлов):
module "private_bucket" {
source = "./modules/bucket"
bucket-name = "private.medikt.net"
provider-zone = var.provider-zone
provider-token = var.provider-token
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
cors_rule = {
allowed_headers = ["*"]
allowed_methods = ["PUT", "GET"]
allowed_origins = ["*"]
expose_headers = ["Etag"]
max_age_seconds = "0"
}
}
Public bucket (для публичных файлов):
module "public_bucket" {
source = "./modules/bucket"
bucket-name = "public.medikt.net"
provider-zone = var.provider-zone
provider-token = var.provider-token
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
bucket_grants = [{
type = "Group"
permissions = ["READ"]
uri = "http://acs.amazonaws.com/groups/global/AllUsers"
}]
cors_rule = {
allowed_headers = ["*"]
allowed_methods = ["PUT", "GET"]
allowed_origins = ["*"]
expose_headers = ["Etag"]
max_age_seconds = "0"
}
}
Transcoder bucket (для транскодированных файлов):
module "transcoder_bucket" {
source = "./modules/bucket"
bucket-name = "transcoderbucket.medikt.net"
provider-zone = var.provider-zone
provider-token = var.provider-token
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
expiration_rotation_period = 31
}
10. Создать MongoDB кластер
module "mongodb8_cluster" {
source = "./modules/mongodb-cluster"
subnet_id = module.vpc.subnet_id
cluster_instance_type = "c3-c4-m8"
mongodb_cluster_name = "medikt-net-mdb8"
vpc_id = module.vpc.vpc_id
allowed_cidr_list = ["172.16.50.0/24"]
provider-zone = var.provider-zone
provider-token = var.provider-token
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
env_prod = var.env_prod
mongod_disk_size = 64
mongod_disk_type_id = "network-ssd"
mongos_disk_size = 14
mongos_disk_type_id = "network-ssd"
mongocfg_disk_size = 14
mongocfg_disk_type_id = "network-ssd"
mongo_version = "8.0"
}
11. Создать OpenSearch кластер (опционально)
module "opensearch_mini" {
source = "./modules/opensearch-headless"
provider-folderid = var.provider-folderid
provider-zone = var.provider-zone
zone_ids = [var.provider-zone]
provider-token = var.provider-token
cluster_name = "opensearch-mini"
assign_public_ip_data = false
vpc_id = module.vpc.vpc_id
subnet_ids = [module.vpc.subnet_id]
ossadmin_passwd = lookup([for k in data.yandex_lockbox_secret_version.referenced.entries : k if k.key == "ossadmin_passwd"][0], "text_value", "")
opensearch_plugins = ["analysis-icu", "yandex-lemmer"]
}
III. Инфраструктурные сервисы
12. Создать Service Accounts и секреты
module "service_accounts" {
source = "./modules/service_accounts"
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
provider-zone = var.provider-zone
provider-token = var.provider-token
env_prod = var.env_prod
}
13. Создать JWT ключи
module "jwt_keys" {
depends_on = [module.namespaces]
source = "./modules/jwt"
k8s_namespace = var.medikt_namespace
}
Примечание: JWT-ключи используются для работы с auth-api.
14. Создать secrets для сторонних сервисов
Captcha:
Важно: Сначала создайте капчу в веб-консоли Яндекс Клауд: Создание капчи. Модуля для создания капчи через Terraform в каталоге modules нет, но провайдер уже добавил этот функционал.
module "captcha_secret" {
source = "./modules/k8s-secret"
depends_on = [module.namespaces]
k8s_secret_name = "medikt-yandex-captcha"
k8s_ns = var.medikt_namespace
data = {
client_key = lookup([for k in data.yandex_lockbox_secret_version.referenced.entries : k if k.key == "captcha_client_key"][0], "text_value", "")
server_key = lookup([for k in data.yandex_lockbox_secret_version.referenced.entries : k if k.key == "captcha_server_key"][0], "text_value", "")
}
}
Metrika:
Важно: Сначала создайте метрику на https://metrika.yandex.ru/, затем создайте секрет в Terraform.
module "ya_metric_secret" {
source = "./modules/k8s-secret"
depends_on = [module.namespaces]
k8s_secret_name = "medikt-yandex-metrika"
k8s_ns = var.medikt_namespace
data = {
id = lookup([for k in data.yandex_lockbox_secret_version.referenced.entries : k if k.key == "yandex_metrica_id"][0], "text_value", "")
}
}
15. Создать connection string для MongoDB
module "user_db8" {
source = "./modules/mongo-dbs"
mongodb_cluster_id = module.mongodb8_cluster.cluster_id
mongodb_address = module.mongodb8_cluster.hostname
mongodb_user = "userdb"
mongodb_password = lookup([for k in data.yandex_lockbox_secret_version.referenced.entries : k if k.key == "mongodb_user_passwd"][0], "text_value", "")
mongodb_database = "userdb"
provider-zone = var.provider-zone
provider-token = var.provider-token
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
k8s_secret_name = "userdb-connection-string"
k8s_namespace = "prod"
}
IV. Приложения
16. Создать namespace для приложений
module "namespaces" {
source = "./modules/k8s_namespaces"
namespaces = ["prod"]
}
17. Развернуть NLB ингресс-контроллер
Для внешнего доступа к сервисам в Kubernetes используется NLB ingress:
module "nlb_ingress" {
source = "./modules/nlb-ingress"
nlb_address_name = "nlb-addr"
zone_id = var.provider-zone
network_id = module.vpc.vpc_id
}
Примечание: Модуль nlb-ingress создает балансировщик с внешним IP, который используется в DNS записях. Получить IP можно из output
module.nlb_ingress.static_nlb_address.
Альтернатива: Можно использовать модуль alb-ingress для создания ALB балансировщика.
Для внутренних сервисов используйте Ingress-контроллер:
module "k8s_ingress" {
source = "./modules/alb-ingress"
provider-zone = var.provider-zone
provider-token = var.provider-token
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
vpc_id = module.vpc.vpc_id
subnet_id = module.vpc.subnet_id
dns_zone_id = module.dns_zone.dns_zone_id
}
18. Развернуть NATS кластер
Примечание: NATS разворачивается после создания Kubernetes кластера и необходимых node groups.
module "nats-namespace" {
source = "./modules/k8s_namespaces"
namespaces = ["nats"]
}
module "nats_nodes" {
source = "./modules/k8s_nodes"
provider-folderid = var.provider-folderid
provider-zone = var.provider-zone
provider-token = var.provider-token
k8s_cluster_id = module.k8s.id
k8s_cluster_name = module.k8s.name
node_group_name = "nats"
preemptible_k8s_nodes = false
k8s_worker_nodes_mem = 2
k8s_worker_nodes_cores = 2
node_labels = {
"nats_affinity" = "true"
}
subnet_ids = [module.vpc.subnet_id]
security_group_ids = [module.k8s.k8s-main-sg, module.k8s.k8s-public-services]
auto_scale = {
initial = 3
max = 3
min = 3
}
}
module "nats_helm_chart" {
source = "./modules/nats-chart"
node_affinity = "nats_affinity"
nats_password = lookup([for k in data.yandex_lockbox_secret_version.infra.entries : k if k.key == "nats_password"][0], "text_value", "")
nats_user = "medikt-nats"
nats_helm_release_name = "medikt-nats"
nats_namespace = "nats"
nats_cluster_enabled = true
nats_cluster_replicas = 3
nats_cluster_name = "medikt-cluster"
}
19. Настроить CDN
module "cdn" {
source = "./modules/cdn"
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
provider-zone = var.provider-zone
provider-token = var.provider-token
vpc_id = module.vpc.vpc_id
cdn_zone = module.dns_zone.yc_dns_zone
cdn_zone_name = module.dns_zone.yc_dns_name
cdn_zone_id = module.dns_zone.dns_zone_id
subnet_id = module.vpc.subnet_id
source_fqdn = module.public_bucket.provider-bucket-fqdn
destination_subdomain = "cdn"
edgecdn_url = "<edgecdn_url>" # появляется после деплоя CDN
provider_type = "ourcdn"
cdn_resource_certificate_id = var.cdn_certificate_id != null ? var.cdn_certificate_id : var.certificate_id
}
Примечание:
edgecdn_urlпоявляется только после первого деплоя CDN. В первой итерации закомментируйте эту строку.
20. Deploy инфраструктурных ресурсов, используемых приложениями (medikt-infra)
module "medikt-infra" {
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
provider-token = var.provider-token
provider-zone = var.provider-zone
source = "./modules/medikt-infra"
client_secret = lookup([for k in data.yandex_lockbox_secret_version.referenced.entries : k if k.key == "featbit_client_key"][0], "text_value", "")
server_secret = lookup([for k in data.yandex_lockbox_secret_version.referenced.entries : k if k.key == "featbit_server_key"][0], "text_value", "")
OpenSearch__Url = "https://<opensearch_host>:9200"
OpenSearch_Indexer_Login = "indexer"
OpenSearch_Indexer_Password = lookup([for k in data.yandex_lockbox_secret_version.referenced.entries : k if k.key == "opensearch_indexer"][0], "text_value", "")
OpenSearch_Searcher_Login = "searcher"
OpenSearch_Searcher_Password = lookup([for k in data.yandex_lockbox_secret_version.referenced.entries : k if k.key == "opensearch_searcher"][0], "text_value", "")
Payments_Gateways_Robokassa_Password1 = lookup([for k in data.yandex_lockbox_secret_version.referenced.entries : k if k.key == "robokassa_pwd1"][0], "text_value", "")
Payments_Gateways_Robokassa_Password2 = lookup([for k in data.yandex_lockbox_secret_version.referenced.entries : k if k.key == "robokassa_pwd2"][0], "text_value", "")
namespace = "prod"
use_cert_manager = true
nginx_ingress_medikt_admin_api = true
nginx_ingress_medikt_admin_spa = true
nginx_ingress_medikt_content_api = true
nginx_ingress_medikt_logs_api = true
nginx_ingress_medikt_notification_api = true
nginx_ingress_medikt_transcoder_api = true
nginx_ingress_medikt_upload_api = true
nginx_ingress_medikt_user_api = true
nginx_ingress_medikt_user_ssr = true
nginx_ingress_medikt_userauth_api = true
nginx_ingress_medikt_userreg_api = true
}
21. Создать DNS записи после NLB ingress
После создания NLB ingress создайте DNS записи с полученными IP адресами:
module "dns_records" {
source = "./modules/dns_zone"
provider-cloudid = var.provider-cloudid
provider-folderid = var.provider-folderid
provider-zone = var.provider-zone
provider-token = var.provider-token
dns_zone = "medikt.net"
dns_zone_is_public = true
dns_zone_private_networks = [module.vpc.vpc_id]
dns_records = {
"@" = [module.nlb_ingress.static_nlb_address]
"www" = [module.nlb_ingress.static_nlb_address]
"adm" = [module.nlb_ingress.static_nlb_address]
"admin" = [module.nlb_ingress.static_nlb_address]
"alertmanager" = [module.nlb_ingress.static_nlb_address]
"auth" = [module.nlb_ingress.static_nlb_address]
"content" = [module.nlb_ingress.static_nlb_address]
"grafana" = [module.nlb_ingress.static_nlb_address]
"logs" = [module.nlb_ingress.static_nlb_address]
"notification" = [module.nlb_ingress.static_nlb_address]
"payments" = [module.nlb_ingress.static_nlb_address]
"prometheus" = [module.nlb_ingress.static_nlb_address]
"reg" = [module.nlb_ingress.static_nlb_address]
"transcoder" = [module.nlb_ingress.static_nlb_address]
"upload" = [module.nlb_ingress.static_nlb_address]
"user" = [module.nlb_ingress.static_nlb_address]
}
}
Примечание о петле зависимостей: DNS записи зависят от внешнего IP балансировщика (NLB ingress), который создается после Kubernetes кластера и node groups с лейблом
web_affinity: "true". Порядок:
- Создать k8s + node_groups с web_affinity
- Создать nlb_ingress (получить IP адрес из output static_nlb_address)
- Создать dns_records с полученными IP адресами
Порядок развертывания
Развертывайте модули в следующем порядке (согласно зависимостям):
- Уровень 0: vpc
- Уровень 1: k8s, mongodb8_cluster, transcoder_queues, buckets, dns_zone (без dns_records)
- Уровень 2: k8s node_groups (с лейблом web_affinity для nlb_ingress)
- Уровень 3: nlb_ingress (после k8s и nodes с web_affinity)
- Уровень 4: service_accounts, jwt_keys, db connections
- Уровень 5: namespaces, secrets
- Уровень 6: nats, cdn
- Уровень 7: dns_records (с DNS записями после nlb_ingress), medikt-infra
Примечание о петле зависимостей: DNS записи зависят от внешнего IP балансировщика (NLB ingress), который создается после Kubernetes кластера и node groups с лейблом
web_affinity: "true".
Примечание про node groups для nlb_ingress: Модуль nlb-ingress использует
deploymentNodeSelectorсweb_affinity: "true", поэтому node groups должны иметь этот лейбл.