Перейти к основному содержимому

Развертывание Django приложения в Яндекс Облаке на Kubernetes и MDB

· 7 мин. чтения

Как развернуть Django приложение в Яндекс Облаке на Kubernetes и Managed Database.

Django приложение

Для примера возьмем простое Django приложение. Создадим Virtualenv и активируем его. Для этого создадим новый проект и приложение в нем.

python3 -m venv venv
source venv/bin/activate
pip install django
django-admin startproject demo

По умолчанию Django использует SQLite базу данных. Для работы с PostgreSQL нам нужно установить драйвер для нее.

pip install psycopg2

Теперь настроим подключение к базе данных. Для этого откроем файл settings.py и внесем следующие изменения.

djangok8s/settings.py
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME"),
"USER": os.environ.get("DB_USER"),
"PASSWORD": os.environ.get("DB_PASSWORD"),
"HOST": os.environ.get("DB_HOST"),
"PORT": os.environ.get("DB_PORT" or 6432),
'OPTIONS': {
'sslmode': 'verify-full',
'sslrootcert': os.path.join(BASE_DIR, 'ca-certificate.crt'),
},
}
}

Теперь перейдем в директорию проекта и создадим Dockerfile для нашего приложения.

cd demo
touch Dockerfile

В Dockerfile пропишем следующее:

Dockerfile
# pull official base image
FROM python:3.11.4-slim-buster

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install dependencies
RUN pip install --upgrade pip
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY src /app
WORKDIR /app
EXPOSE 8000
CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "djangok8s.wsgi"]

Этот Dockerfile создаст образ на основе Ubuntu 22.04, установит необходимые пакеты и зависимости для работы Django приложения, а также запустит Gunicorn для обработки запросов на 8000 порту.

Теперь выполним сборку образа и запустим контейнер.

docker build -t demo .

Флаг -t задает название для образа. После сборки образа мы можем посмотреть список образов на нашей машине.

docker images

Вывод будет примерно таким:

REPOSITORY   TAG         IMAGE ID       CREATED         SIZE
demo latest 0e382e58571c 9 minutes ago 579MB

Локальная база данных

Для локальной базы данных мы будем использовать PostgreSQL. Её мы тоже запустим в контейнере.

docker-compose.yml
version: '3.9'

services:
postgres:
image: postgres:14
ports:
- 5432:5432
volumes:
- ./postgres:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=somepassword
- POSTGRES_USER=djangouser
- POSTGRES_DB=djangodb
django:
build:
context: .
dockerfile: Dockerfile
ports:
- 8000:8000
depends_on:
- postgres
environment:
- DB_HOST=postgres
- DB_NAME=djangodb
- DB_USER=djangouser
- DB_PASSWORD=somepassword

В этом файле мы описали два сервиса: postgres и django. В сервисе postgres мы указали образ, который мы будем использовать, а также порт, на котором будет доступна база данных. В сервисе django мы указали образ, который мы создали ранее, а также зависимость от сервиса postgres. Также мы указали переменные окружения, которые будут использоваться в нашем приложении.

Создадим директорию для хранения данных базы данных.

mkdir postgres

Запуск контейнера

Теперь мы можем запустить наши сервисы.

docker-compose up

После запуска контейнеров мы можем зайти в контейнер с Django приложением и выполнить миграции.

docker exec -it demo-django-1 bash

В контейнере выполним миграции.

python3 manage.py makemigrations
python3 manage.py migrate

Теперь мы можем зайти на страницу http://localhost:8000 и увидеть стандартную страницу Django.

Отлично. Теперь пришло время подготовить облачную инфраструктуру.

Инфраструктура в Яндекс Облаке

Для развертывания приложения в Яндекс Облаке нам понадобится Kubernetes кластер и Managed Database. Чтобы их создать мы можем воспользоваться Terraform.

Начнем с конфигурации провайдера.

terraform.tf
terraform {
required_providers {
yandex = {
source = "yandex-cloud/yandex"
}
helm = {
source = "hashicorp/helm"
version = "2.12.1"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.0.0"
}
}
required_version = ">= 0.13"
}

provider "yandex" {
cloud_id = var.cloud_id
folder_id = var.folder_id
zone = var.zone
}

Кластер Kubernetes

Опишем переменные, которые мы будем использовать.

variables.tf
variable "cloud_id" {
description = "Cloud ID"
type = string
}
variable "folder_id" {
description = "Folder ID"
type = string
}
variable "zone" {
description = "Zone"
type = string
default = "ru-central1-a"
}
variable "master_version" {
type = string
description = "Kubernetes version for master nodes"
default = "1.28"
}

variable "nodes_version" {
type = string
description = "Kubernetes version for nodes"
default = "1.28"
}
variable "node_groups_defaults" {
description = "Map of common default values for Node groups."
type = map(any)
default = {
platform_id = "standard-v3"
node_cores = 4
node_memory = 8
node_gpus = 0
core_fraction = 100
disk_type = "network-ssd"
disk_size = 32
preemptible = false
nat = false
ipv4 = true
ipv6 = false
size = 1
}
}

Теперь подготовим сервисные аккаунты для Kubernetes кластера и Managed Database.

iam.tf
locals {
service_account = {
master = "k8s-master-sa"
node = "k8s-node-sa"
}
}

resource "yandex_iam_service_account" "master_sa" {
folder_id = var.folder_id
description = "Service account for k8s cluster. Master"
name = local.service_account.master
}

resource "yandex_iam_service_account" "node_sa" {
folder_id = var.folder_id
description = "Service account for nodes in k8s cluster. Nodes"
name = local.service_account.node
}



resource "yandex_resourcemanager_folder_iam_member" "master_sa_roles" {

folder_id = var.folder_id
for_each = toset([
"k8s.clusters.agent", // This role is needed to node groups
"k8s.tunnelClusters.agent", // This role is needed to manage network policies
"vpc.publicAdmin", // This role is needed to manage public ip addresses
"load-balancer.admin", // This role is needed to manage load balancers
"logging.writer", // This role is needed to write logs to log group
])
role = each.value
member = "serviceAccount:${yandex_iam_service_account.master_sa.id}"
depends_on = [
yandex_iam_service_account.master_sa,
]
sleep_after = 5
}

resource "yandex_resourcemanager_folder_iam_member" "node_sa_roles" {
folder_id = var.folder_id
for_each = toset([
"container-registry.images.puller", // This role is needed to pull images from k8s cluster registry
"kms.keys.encrypterDecrypter", // This role is needed to decrypt on nodes secrets created by Cilium
])
role = each.value
member = "serviceAccount:${yandex_iam_service_account.node_sa.id}"
depends_on = [
yandex_iam_service_account.node_sa,
]
sleep_after = 5
}

Теперь создадим ключ для шифрования секретов.

kms.tf
resource "yandex_kms_symmetric_key" "kms_key" {
name = "k8s-kms-key"
description = "K8S KMS symetric key"
default_algorithm = "AES_256"
rotation_period = "8760h"
}

Группа в сервисе Cloud Logging.

kms.tf
resource "yandex_logging_group" "k8s_log_group" {
name = "k8s-logging-group"
folder_id = var.folder_id
}

Container Registry, в котором будут храниться наши образы.

container-registry.tf
resource "yandex_container_registry" "django-registry" {
name = "django-registry"
folder_id = var.folder_id
}

resource "yandex_container_repository" "django-repo" {
name = "${yandex_container_registry.django-registry.id}/django-app"
}

Подготовим сеть для кластера.

kms.tf
resource "yandex_vpc_network" "default" {
name = "default"
}

resource "yandex_vpc_subnet" "default" {
for_each = {
a = "10.128.0.0/24",
b = "10.129.0.0/24",
d = "10.131.0.0/24",
}
name = "default-${each.key}"
network_id = yandex_vpc_network.default.id
zone = "ru-central1-${each.key}"

v4_cidr_blocks = [
each.value
]
route_table_id = yandex_vpc_route_table.default.id
}

resource "yandex_vpc_gateway" "default" {
name = "default"
shared_egress_gateway {}
}

resource "yandex_vpc_route_table" "default" {
name = "default"
network_id = yandex_vpc_network.default.id
static_route {
destination_prefix = "0.0.0.0/0"
gateway_id = yandex_vpc_gateway.default.id
}
}


Теперь создадим Kubernetes кластер.

k8s.tf
resource "yandex_kubernetes_cluster" "k8s_cluster" {
name = "django-k8s-cluster"
description = "Cluster for Django application"

network_id = yandex_vpc_network.default.id

master {
version = var.master_version
zonal {
zone = yandex_vpc_subnet.default["a"].zone
subnet_id = yandex_vpc_subnet.default["a"].id
}

public_ip = true

maintenance_policy {
auto_upgrade = true

maintenance_window {
start_time = "15:00"
duration = "3h"
}
}

master_logging {
enabled = true
log_group_id = yandex_logging_group.k8s_log_group.id
kube_apiserver_enabled = true
cluster_autoscaler_enabled = true
events_enabled = true
audit_enabled = true
}
}

service_account_id = yandex_iam_service_account.master_sa.id
node_service_account_id = yandex_iam_service_account.node_sa.id

release_channel = "RAPID"
network_implementation {
cilium {}
}
kms_provider {
key_id = yandex_kms_symmetric_key.kms_key.id
}
depends_on = [
yandex_resourcemanager_folder_iam_member.master_sa_roles,
]
}

resource "yandex_kubernetes_node_group" "django_node_group" {
cluster_id = yandex_kubernetes_cluster.k8s_cluster.id
name = "default-nodes"
description = "K8s Cluster Nodes"
version = var.nodes_version

instance_template {
platform_id = var.node_groups_defaults.platform_id

network_interface {
ipv4 = var.node_groups_defaults.ipv4
nat = var.node_groups_defaults.nat
subnet_ids = [yandex_vpc_subnet.default["a"].id]
}

resources {
memory = var.node_groups_defaults.node_memory
cores = var.node_groups_defaults.node_cores
core_fraction = var.node_groups_defaults.core_fraction
}

boot_disk {
type = var.node_groups_defaults.disk_type
size = var.node_groups_defaults.disk_size
}

scheduling_policy {
preemptible = var.node_groups_defaults.preemptible
}

container_runtime {
type = "containerd"
}
}

scale_policy {
fixed_scale {
size = var.node_groups_defaults.size
}
}

allocation_policy {
location {
zone = yandex_vpc_subnet.default["a"].zone
}
}

maintenance_policy {
auto_upgrade = true
auto_repair = true

maintenance_window {
day = "monday"
start_time = "15:00"
duration = "3h"
}

maintenance_window {
day = "friday"
start_time = "10:00"
duration = "4h30m"
}
}

depends_on = [
yandex_resourcemanager_folder_iam_member.node_sa_roles,
]
}

Теперь, чтобы применить нашу конфигурацию, выполним команду terraform apply.

terraform apply --auto-approve

Примерно через 10 минут мы получим готовый кластер.

Managed Database

Теперь создадим Managed Database.

mdb.tf
resource "yandex_mdb_postgresql_cluster" "django-cluster" {
name = "django-db"
environment = "PRODUCTION"
network_id = data.yandex_vpc_network.default.id

config {
version = 15
resources {
resource_preset_id = "s2.micro"
disk_type_id = "network-ssd"
disk_size = 16
}
postgresql_config = {
max_connections = 395
enable_parallel_hash = true
autovacuum_vacuum_scale_factor = 0.34
default_transaction_isolation = "TRANSACTION_ISOLATION_READ_COMMITTED"
shared_preload_libraries = "SHARED_PRELOAD_LIBRARIES_AUTO_EXPLAIN,SHARED_PRELOAD_LIBRARIES_PG_HINT_PLAN"
}
access {
web_sql = true
}
}

maintenance_window {
type = "WEEKLY"
day = "SAT"
hour = 12
}

host {
zone = var.zone
subnet_id = data.yandex_vpc_subnet.default_a.id
assign_public_ip = true
}

}

resource "yandex_mdb_postgresql_database" "django-db" {
cluster_id = yandex_mdb_postgresql_cluster.django-cluster.id
name = "django"
owner = yandex_mdb_postgresql_user.django-user.name
lc_collate = "en_US.UTF-8"
lc_type = "en_US.UTF-8"

extension {
name = "uuid-ossp"
}

}

resource "random_password" "django-password" {
length = 24
special = true
min_lower = 1
min_numeric = 1
min_special = 1
min_upper = 1
override_special = "-_()[]{}!%^"
}


resource "yandex_mdb_postgresql_user" "django-user" {
cluster_id = yandex_mdb_postgresql_cluster.django-cluster.id
name = "django"
password = random_password.django-password.result
}

resource "yandex_lockbox_secret" "django-password" {
name = "MDB password for django database"
}

resource "yandex_lockbox_secret_version" "django-password" {
secret_id = yandex_lockbox_secret.django-password.id
entries {
key = "password"
text_value = random_password.django-password.result
}
}

Чтобы применить эти изменения, выполним команду снова выполним terraform apply.

terraform apply --auto-approve

Развертывание приложения

Авторизация в Kubernetes кластере

Теперь, когда у нас есть Kubernetes кластер и Managed Database, мы можем развернуть наше приложение.

Для начала нам нужно получить конфигурацию для подключения к кластеру.

yc managed-kubernetes cluster get-credentials django-k8s-cluster --external

В ответ в консоли мы увидим следующее:

Context 'yc-django-k8s-cluster' was added as default to kubeconfig '~/.kube/config'.
Check connection to cluster using 'kubectl cluster-info --kubeconfig ~/.kube/config'.

Note, that authentication depends on 'yc' and its config profile 'default'.
To access clusters using the Kubernetes API, please use Kubernetes Service Account.

И если выполнить команду kubectl cluster-info --kubeconfig ~/.kube/config, то мы увидим следующее:

Kubernetes control plane is running at https://158.160.127.157
CoreDNS is running at https://158.160.127.157/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

Создадим namespace для нашего приложения.

kubectl create namespace django-app

Подготовка образа

Теперь нам нужно запушить наше приложение в Container Registry. Сначала соберем контейнер.

repo_url=$(terraform -chdir=tf output -raw repo_url)
docker build --platform linux/amd64 -t "$repo_url/app:v2" .

Теперь запушим его в Container Registry.

repo_url=$(terraform -chdir=tf output -raw repo_url)
docker push $repo_url/app:v2

Создание секретов

Теперь нам нужно создать секреты для подключения к базе данных. Для начала нам нужно добавить в кластер секрет с ключом от сервисного аккаунта. Подробнее про работу с секретами при помощи kubectl можно прочитать в документации.

Нужно следующую команду:

yc iam key create \
--service-account-name external-secrets-sa \
--output authorized_key.json \
--description "Key for external-secrets-sa"
kubectl --namespace django-app create secret generic yc-auth --from-file=authorized-key=authorized_key.json
rm authorized_key.json

Чтобы проверить, что секрет создался, выполним команду kubectl get secrets --namespace django-app.

В ответ мы увидим следующее:

NAME      TYPE     DATA   AGE
yc-auth Opaque 1 4m52s

Нужно будет создать SecretStore или ClusterSecretStore. Разница в том, что ClusterSecretStore будет доступен из любого namespace, а SecretStore только из того, в котором создан. Создадим манифест secret-store-yc.yaml для SecretStore:

secret-store.yaml
---
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: django-app-secret-store
namespace: django-app
spec:
provider:
yandexlockbox:
auth:
authorizedKeySecretRef:
name: yc-auth
key: authorized-key

Теперь применим его командой kubectl apply -f secret-store.yaml.

Далее нам необходимо создать ExternalSecret. Для этого создадим манифест external-secret.yaml:

external-secret.yaml
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: django-postgres-password
namespace: django-app
spec:
refreshInterval: 10m
secretStoreRef:
name: django-app-secret-store
kind: SecretStore
target:
name: postgres-password
data:
- secretKey: password
remoteRef:
key: $POSTGRES_PASSWORD_SECRET_ID
property: password

Теперь применим его командой:

cd k8s
export POSTGRES_PASSWORD_SECRET_ID=$(terraform -chdir=../tf output -raw postgres_password_secret_id)
cat external-secret.yaml | envsubst | kubectl apply -f - --namespace django-app

Проверить, что секрет создался, можно командой:

kubectl --namespace django-app get secret postgres-password \
--output=json | \
jq --raw-output ."data"."password" | \
base64 --decode

В ответ мы увидим пароль от базы данных.

Создание Deployment и Service для развертывания приложения

Теперь нам нужно создать Deployment и Service для нашего приложения.

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: django-app
labels:
app: django
spec:
replicas: 3
selector:
matchLabels:
app: django
template:
metadata:
labels:
app: django
spec:
containers:
- image: $REPO_URL/app:v2
name: django
ports:
- containerPort: 8000
name: gunicorn
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-password
key: password
- name: DB_USER
value: django
- name: DB_NAME
value: django
- name: DB_HOST
value: $POSTGRES_HOST
- name: DB_PORT
value: "6432"
- name: ALLOWED_HOSTS
value: "matrosov.xyz"
cd k8s
export REPO_URL=$(terraform -chdir=../tf output -raw repo_url)
export POSTGRES_HOST=$(terraform -chdir=../tf output -raw postgres_host)
cat deployment.yaml | envsubst | kubectl apply -f - --namespace django-app
cat service.yaml | envsubst | kubectl apply -f - --namespace django-app

Если все прошло успешно, то мы можем увидеть наше приложение командой kubectl get pods --namespace django-app.

NAME                          READY   STATUS    RESTARTS   AGE
django-app-76b6f887c4-b555h 1/1 Running 0 10s
django-app-76b6f887c4-kcv89 1/1 Running 0 10s
django-app-76b6f887c4-qpdqk 1/1 Running 0 10s

Создание Ingress для доступа к приложению

Для этого нам понадобится сертификат для домена. Мы можем создать его при помощи сервиса Certificate Manager.

certificate.tf
resource "yandex_cm_certificate" "django-cert" {
name = "django-cert"
domains = ["matrosov.xyz"]

managed {
challenge_type = "DNS_CNAME"
challenge_count = 1
}
}

resource "yandex_dns_zone" "django-zone" {
name = "django-zone"
zone = "matrosov.xyz."
public = true
}

resource "yandex_dns_recordset" "django-cert" {
count = yandex_cm_certificate.django-cert.managed[0].challenge_count
zone_id = yandex_dns_zone.django-zone.id
name = yandex_cm_certificate.django-cert.challenges[count.index].dns_name
type = yandex_cm_certificate.django-cert.challenges[count.index].dns_type
data = [yandex_cm_certificate.django-cert.challenges[count.index].dns_value]
ttl = 60
}

resource "yandex_vpc_address" "alb-external-ip" {
name = "alb-external-ip"

external_ipv4_address {
zone_id = "ru-central1-a"
}
}


resource "yandex_dns_recordset" "matrosov-xyz" {
zone_id = yandex_dns_zone.django-zone.id
name = "matrosov.xyz."
type = "A"
ttl = 60
data = [yandex_vpc_address.alb-external-ip.external_ipv4_address[0].address]
}

Теперь применим изменения командой terraform apply --auto-approve.

А также нам нужно создать Ingress для доступа к приложению. Мы будем использовать сервис Application Load Balancer. Для этого установим alb-ingress при помощи Helm.

alb-ingress.tf
resource "yandex_iam_service_account" "k8s_cluster_alb" {
folder_id = var.folder_id
description = "Service account for k8s cluster ALB Ingress Controller"
name = "k8s-cluster-alb-sa"
}

resource "yandex_resourcemanager_folder_iam_member" "k8s_cluster_alb_roles" {
folder_id = var.folder_id

//alb.editor — для создания необходимых ресурсов.
//vpc.publicAdmin — для управления внешней связностью.
//certificate-manager.certificates.downloader — для работы с сертификатами, зарегистрированными в сервисе Yandex Certificate Manager.
//compute.viewer — для использования узлов кластера Managed Service for Kubernetes в целевых группах балансировщика.
for_each = toset([
"alb.editor",
"vpc.publicAdmin",
"certificate-manager.certificates.downloader",
"compute.viewer",
])
role = each.value
member = "serviceAccount:${yandex_iam_service_account.k8s_cluster_alb.id}"
depends_on = [
yandex_iam_service_account.k8s_cluster_alb,
]
sleep_after = 5
}

resource "yandex_iam_service_account_key" "k8s_cluster_alb" {
service_account_id = yandex_iam_service_account.k8s_cluster_alb.id
depends_on = [
yandex_iam_service_account.k8s_cluster_alb,
]
}

resource "kubernetes_namespace" "alb_ingress" {
metadata {
name = "alb-ingress"
}
}

resource "kubernetes_secret" "yc_alb_ingress_controller_sa_key" {
metadata {
name = "yc-alb-ingress-controller-sa-key"
namespace = "alb-ingress"
}
data = {
"sa-key.json" = jsonencode(
{
"id" : yandex_iam_service_account_key.k8s_cluster_alb.id,
"service_account_id" : yandex_iam_service_account_key.k8s_cluster_alb.service_account_id,
"created_at" : yandex_iam_service_account_key.k8s_cluster_alb.created_at,
"key_algorithm" : yandex_iam_service_account_key.k8s_cluster_alb.key_algorithm,
"public_key" : yandex_iam_service_account_key.k8s_cluster_alb.public_key,
"private_key" : yandex_iam_service_account_key.k8s_cluster_alb.private_key
}
)
}

type = "kubernetes.io/Opaque"
depends_on = [
kubernetes_namespace.alb_ingress
]
}

resource "helm_release" "alb_ingress" {
name = "alb-ingress"
namespace = "alb-ingress"
repository = "oci://cr.yandex/yc-marketplace/yandex-cloud/yc-alb-ingress"
chart = "yc-alb-ingress-controller-chart"
version = "v0.1.24"
create_namespace = true

values = [
<<-EOF
folderId: ${var.folder_id}
clusterId: ${module.k8s.cluster_id}
daemonsetTolerations:
- operator: Exists
auth:
json: ${jsonencode(
{
"id" : yandex_iam_service_account_key.k8s_cluster_alb.id,
"service_account_id" : yandex_iam_service_account_key.k8s_cluster_alb.service_account_id,
"created_at" : yandex_iam_service_account_key.k8s_cluster_alb.created_at,
"key_algorithm" : yandex_iam_service_account_key.k8s_cluster_alb.key_algorithm,
"public_key" : yandex_iam_service_account_key.k8s_cluster_alb.public_key,
"private_key" : yandex_iam_service_account_key.k8s_cluster_alb.private_key
}
)}
EOF
]

depends_on = [
module.k8s,
yandex_resourcemanager_folder_iam_member.k8s_cluster_alb_roles,
yandex_iam_service_account_key.k8s_cluster_alb,
kubernetes_namespace.alb_ingress,
kubernetes_secret.yc_alb_ingress_controller_sa_key
]
}

resource "yandex_vpc_security_group" "alb" {
name = "k8s-alb"
description = "alb security group"
network_id = module.k8s.network_id
folder_id = var.folder_id

ingress {
protocol = "ICMP"
description = "ping"
v4_cidr_blocks = ["0.0.0.0/0"]
}

ingress {
protocol = "TCP"
description = "http"
v4_cidr_blocks = ["0.0.0.0/0"]
port = 80
}

ingress {
protocol = "TCP"
description = "https"
v4_cidr_blocks = ["0.0.0.0/0"]
port = 443
}

ingress {
protocol = "TCP"
description = "Rule allows availability checks from load balancer's address range. It is required for a db cluster"
predefined_target = "loadbalancer_healthchecks"
from_port = 0
to_port = 65535
}

ingress {
protocol = "ANY"
description = "Rule allows master and slave communication inside a security group."
predefined_target = "self_security_group"
from_port = 0
to_port = 65535
}

egress {
protocol = "TCP"
description = "Enable traffic from ALB to K8s services"
# predefined_target = "self_security_group"
v4_cidr_blocks = flatten([for cidr in module.k8s.subnet_cidr : cidr])
from_port = 30000
to_port = 65535
}

egress {
protocol = "TCP"
description = "Enable probes from ALB to K8s"
v4_cidr_blocks = flatten([for cidr in module.k8s.subnet_cidr : cidr])
port = 10501
}
}

Теперь применим изменения командой terraform apply --auto-approve.

Теперь нам нужно создать манифест для Ingress.

ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: django-app-ingress
annotations:
ingress.alb.yc.io/subnets: $SUBNET_IDS
ingress.alb.yc.io/security-groups: $SECURITY_GROUP_IDS
ingress.alb.yc.io/external-ipv4-address: $EXTERNAL_IP
ingress.alb.yc.io/group-name: django
spec:
tls:
- hosts:
- matrosov.xyz
secretName: yc-certmgr-cert-id-$CERTIFICATE_ID
rules:
- host: matrosov.xyz
http:
paths:
- path: "/"
pathType: Prefix
backend:
service:
name: django-app-service
port:
number: 8000

cd k8s
export SUBNET_IDS=$(terraform -chdir=../tf output -json subnet_ids | jq '. | join(",")')
export SECURITY_GROUP_IDS=$(terraform -chdir=../tf output -raw security_group_id)
export CERTIFICATE_ID=$(terraform -chdir=../tf output -raw certificate_id)
export EXTERNAL_IP=$(terraform -chdir=../tf output -raw alb_ip_address)
cat ingress.yaml | envsubst | kubectl apply -f - --namespace django-app

Миграции базы данных

Для этого мы создадим Job, который выполнит миграции базы данных. В нем мы будем использовать образ, который мы создали ранее, изменив только команду запуска. Для запуска миграций нам нужно выполнить следующую команду python manage.py migrate.

job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: migrations
labels:
app.kubernetes.io/name: demo
app.kubernetes.io/component: migrations
spec:
ttlSecondsAfterFinished: 100
activeDeadlineSeconds: 120
template:
metadata:
labels:
app.kubernetes.io/name: demo
app.kubernetes.io/component: migrations
spec:
restartPolicy: Never
containers:
- name: migrations
image: $REPO_URL/app:v1
imagePullPolicy: IfNotPresent
command:
- python
- manage.py
- migrate
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-password
key: password
- name: DB_USER
value: django
- name: DB_NAME
value: django
- name: DB_HOST
value: $POSTGRES_HOST
- name: DB_PORT
value: "6432"

Обратите внимание, что в этом манифесте мы указали ttlSecondsAfterFinished: 100. Это значит, что после выполнения Job он будет удален через 100 секунд и нам не придется удалять его вручную.

Полную конфигурацию можно посмотреть в репозитории.