В предыдущей статье мы рассмотрели, как выглядят интерфейсы при императивном подходе. В этом уроке мы рассмотрим, как можно скрыть что-то за интерфейсом, если у нас есть декларативный код.
Шлем (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;
- Создавая определенный уровень абстракции, мы должны следить за его размером и побочными эффектами.