В предыдущей статье мы рассмотрели, как выглядят интерфейсы при императивном подходе. В этом уроке мы рассмотрим, как можно скрыть что-то за интерфейсом, если у нас есть декларативный код.

Шлем (YAML)

Хотя мы можем написать программу для создания сущностей в k8s, используя доступные API и библиотеки на разных языках, наиболее распространенным способом является написание файлов конфигурации на языке YAML. Эти файлы описывают запросы к API k8s, выполняемые программой kubectl.

Для создания интерфейсов для внешнего интерфейса также используются языки шаблонов, такие как jinja или text/template. Они позволяют заменять предопределенные переменные и создавать динамические страницы. Нам понадобится что-то похожее, но для конфигураций в YAML.

Помимо Helm есть инструмент Kustomize, который также позволяет создавать интерфейсы для развертывания приложений в k8s. Kustomize рассматривает файлы YAML как структуры и применяет к ним исправления, которые переопределяют их содержимое. Важно отметить, что примеры таких патчей описаны в RFC 6902 относительно JSON (**https://datatracker.ietf.org/doc/html/rfc6902**), который также поддерживается в Kustomize. Это позволяет обеспечить более гибкую настройку для различных сред и упрощает их обслуживание.

Далее давайте рассмотрим конкретно использование Helm. Предположим, у нас есть несколько сервисов, которые мы хотим развернуть в k8s, и мы хотим стандартизировать процесс их развертывания. Мы можем создать следующий шаблон:

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.appName }}-deployment
spec:
  selector:
    matchLabels:
      app: {{ .Values.appName }}
  replicas: {{ .Values.replicaCount }}
  template:
    metadata:
      labels:
        app: {{ .Values.appName }}
    spec:
      containers:
        - name: {{ .Values.appName }}-container
          image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          ports:
            - containerPort: {{ .Values.containerPort }}
          env:
            {{- range $env := .Values.env }}
            - name: {{ $env.name }}
              value: {{ $env.value }}
            {{- end }}

---
# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.appName }}-service
spec:
  selector:
    app: {{ .Values.appName }}
  ports:
    - name: http
      port: {{ .Values.servicePort }}
      targetPort: {{ .Values.containerPort }}
  type: {{ .Values.serviceType }}

---
# templates/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ .Values.appName }}-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: {{ .Values.ingress.host }}
      http:
        paths:
          - path: /{{ .Values.ingress.path }}
            pathType: Prefix
            backend:
              service:
                name: {{ .Values.appName }}-service
                port:
                  name: http

В этом сегменте рассказывается, как можно использовать Helm и Kustomize для создания интерфейсов для развертывания приложений в Kubernetes, используя методы шаблонов в файлах YAML. Это показывает гибкость и удобство обслуживания, которые эти инструменты предлагают в процессе развертывания.

Чтобы использовать этот шаблон, нам нужно будет собрать пакет Helm. Для этого мы определим файл Chart.yaml:

apiVersion: v2
name: my-chart
description: A Helm chart for my application
version: 0.1.0

Чтобы построить его:

helm init
helm package my-chart/

Это создаст файл my-chart-0.1.0.tgz. После этого мы можем определить следующий файл значений:

# values.yaml
appName: my-app
replicaCount: 3
image:
  repository: my-docker-repo/my-app
  tag: 1.0
containerPort: 8080
servicePort: 80
serviceType: ClusterIP
ingress:
  host: my-app.example.com
  path: my-app

И устанавливаем его в k8s:

helm install my-release my-chart-0.1.0.tgz -f values.yaml

Если нам нужно обновить диаграмму до новой версии, мы сделаем следующее:

helm upgrade my-release my-chart-0.2.0.tgz -f values.yaml

Однако нам следует подумать, где провести грань между тем, что должна решить система, и тем, что может решить пользователь, который будет использовать эту систему. Давайте рассмотрим содержимое файлаvalues.yaml. Некоторые поля кажутся лишними:

  • containerPort — можно предположить, что все сервисы будут использовать порт 80 и не указывать этот параметр явно;
  • servicePort – аналогично можно предположить, что все сервисы будут доступны на порту 80;
  • serviceType – этот параметр может существенно варьировать доступность услуги на разных уровнях, поэтому его можно исключить;
  • Настройки входящего трафика также можно считать стандартизированными и не указанными явно в файлеvalues.yaml.

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

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.appName }}-deployment
spec:
  selector:
    matchLabels:
      app: {{ .Values.appName }}
  replicas: {{ .Values.replicaCount }}
  template:
    metadata:
      labels:
        app: {{ .Values.appName }}
    spec:
      containers:
        - name: {{ .Values.appName }}-container
          image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          ports:
            - containerPort: 80
          env:
            {{- range $env := .Values.env }}
            - name: {{ $env.name }}
              value: {{ $env.value }}
            {{- end }}

---
# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.appName }}-service
spec:
  selector:
    app: {{ .Values.appName }}
  ports:
    - name: http
      port: 80
      targetPort: 80
  type: ClusterIP

---
# templates/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ .Values.appName }}-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
    - host: {{ .Values.appName }}.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ .Values.appName }}-service
                port:
                  name: http
appName: my-app
replicaCount: 3
image:
  repository: my-docker-registry/my-app
  tag: 1.0.0
env:
  - name: DB_HOST
    value: db.example.com
  - name: DB_PORT
    value: "5432"
  - name: DB_USER
    value: my_db_user
  - name: DB_PASSWORD
    value: my_db_password

Здесь основное внимание уделяется упрощению и оптимизации диаграммы Хелма путем предположения о значениях по умолчанию и удаления избыточных параметров. Это делает диаграмму более удобной и простой в управлении, особенно для тех пользователей, которые не являются экспертами в Kubernetes.

Также необходимо учитывать версионность. У нас есть несколько версий:

  • Версия диаграммы, которая определяет, что мы развертываем;
  • Версия сервиса (тег контейнера);
  • Версия Values.yaml, определяющая конфигурацию службы.

Как мы можем решить эту проблему? Файлvalues.yaml должен быть доступен разработчикам, поскольку они определяют параметры, которые будут использоваться при развертывании сервиса. Таким образом, версия Values.yaml может соответствовать версии службы.

В этом контексте можно вспомнить семантическое версионирование: https://semver.org/.

Наконец, кажется, что наш интерфейс стал слишком сложным. Чтобы решить эту проблему, мы можем разделить интерфейс на базовые диаграммы, описывающие каждый блок указанной выше диаграммы.

# deployment/Chart.yaml
name: deployment
version: 0.1.0
description: A Helm chart for the deployment of a Kubernetes deployment
# Omitting values.yaml and helpers.tpl for brevity
files:
  - templates/template.yaml
# deployment/template.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.appName }}-deployment
spec:
...

Тогда мы можем построить индивидуальную диаграмму для сервиса:

# Chart.yaml
name: service
version: 0.1.0
description: A Helm chart for deploying a Kubernetes service
dependencies:
  - name: deployment
    version: 0.1.0
    repository: "deployment"
  - name: service
    version: 0.1.0
    repository: "service"
  - name: ingress
    version: 0.1.0
    repository: "ingress"

---
# template.yaml
{{- include "deployment.deployment" . }}
{{- include "service.service" . }}
{{- include "ingress.ingress" . }}

Какие преимущества мы получаем, используя интерфейсы управления версиями и разделения в наших сервисах, а также принципы SOLID?

Благодаря версионированию мы можем гибко настраивать наши сервисы, учитывать зависимости и контролировать изменения. Нам будут доступны следующие версии:

  • Базовые графики;
  • Графики обслуживания;
  • Файлы Values.yaml, соответствующие версии образов Docker.

Стоит отметить, что с помощью управления версиями мы эффективно разграничиваем обязанности, вводим понятные интерфейсы и управляем зависимостями — все это основные принципы SOLID.

Итак, в итоге мы добиваемся:

  • Принципы SOLID соблюдаются для декларативных языков;
  • Стандартизация на уровне компании/продукта позволяет уменьшить размеры интерфейса;
  • Управление версиями в декларативных языках более явное;
  • В Helm есть типы, унаследованные от Go (ссылка на документацию: https://helm.sh/docs/chart_template_guide/data_types/).

Терраформ (HCL)

Как мы можем описать интерфейсы для инфраструктуры в целом, используя Terraform, также известный как язык конфигурации Hashicorp? Давайте рассмотрим пример и опишем развертывание лямбда-функции в AWS с использованием Terraform.

Мы можем использовать Terraform для создания инфраструктуры в виде кода и определения ее состояния. В этом случае мы можем использовать его для создания лямбда-функции и управления ею в AWS.

provider "aws" {
  region = "us-west-2"
  access_key = "ACCESS_KEY"
  secret_key = "SECRET_KEY"
}

variable "function_name" {
  type = string
}
resource "aws_lambda_function" "lambda_function" {
  function_name = var.function_name
  role = "arn:aws:iam::ACCOUNT_ID:role/LambdaRole"
  handler = "handler.lambda_handler"
  runtime = "python3.8"
  memory_size = 256
  timeout = 10
  filename = "path/to/lambda_function.zip"
}

Мы предполагаем, что функция хранится в zip-архиве; функция может быть простым скриптом.

Давайте развернем его:

zip -j path/to/lambda_function.zip path/to/lambda_function.py
terraform init
terraform plan
terraform apply

Что дальше? Точно! Давайте сделаем интерфейс минимальным и удобным. В HCL это решают модули:

# lambda/main.tf
provider "aws" {
  region = var.region
}

resource "aws_lambda_function" "lambda_function" {
  function_name = var.function_name
  role = "arn:aws:iam::ACCOUNT_ID:role/LambdaRole"
  handler = "handler.lambda_handler"
  runtime = "python3.8"
  memory_size = 256
  timeout = 10
  filename = "path/to/{{var.function_name}}.zip"
}

# lambda/variables.tf
variable "function_name" {
  type = string
}

# lambda/outputs.tf
output "lambda_function_arn" {
  value = aws_lambda_function.lambda_function.arn
}

Как видите, у нас осталась всего одна функция, которой достаточно для развертывания. Теперь давайте разместим файлы в отдельные папки и будем использовать их как модуль.

Если нам нужно создать несколько функций, мы можем использовать следующий синтаксис:

variable "function_names" {
  type    = list(string)
  default = ["test1", "test2", "test3"]
}

module "lambda_functions" {
  source = "./lambda/"
  for_each = toset(var.function_names)
  function_name = each.key
}

Заключение

  • Мы также можем описать инфраструктуру, используя принципы SOLID;
  • Создавая определенный уровень абстракции, мы должны следить за его размером и побочными эффектами.