Skip to main content
Version: 1.12-grodno (current)

Быстрый старт

Данный гид поможет вам развернуть инфраструктуру 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". Порядок:

  1. Создать k8s + node_groups с web_affinity
  2. Создать nlb_ingress (получить IP адрес из output static_nlb_address)
  3. Создать dns_records с полученными IP адресами

Порядок развертывания

Развертывайте модули в следующем порядке (согласно зависимостям):

  1. Уровень 0: vpc
  2. Уровень 1: k8s, mongodb8_cluster, transcoder_queues, buckets, dns_zone (без dns_records)
  3. Уровень 2: k8s node_groups (с лейблом web_affinity для nlb_ingress)
  4. Уровень 3: nlb_ingress (после k8s и nodes с web_affinity)
  5. Уровень 4: service_accounts, jwt_keys, db connections
  6. Уровень 5: namespaces, secrets
  7. Уровень 6: nats, cdn
  8. Уровень 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 должны иметь этот лейбл.


Полезные ссылки