Terraform обладает огромными возможностями, когда речь идет об определении и поддержке инфраструктуры в виде кода. В сочетании с декларативным API, например, API облачного провайдера, он может определять, предварительно просматривать и применять изменения к кодифицированной инфраструктуре.
Поэтому часто команды используют Terraform для определения инфраструктуры своих кластеров Kubernetes. И как платформа для создания платформ, Kubernetes обычно требует ряд дополнительных сервисов перед развертыванием рабочих нагрузок. Подумайте о контроллерах входа, агентах регистрации и мониторинга и так далее. Но, несмотря на декларативный API Kubernetes и очевидные преимущества поддержки инфраструктуры и сервисов кластера из той же инфраструктуры, что и репозиторий кода, Terraform – далеко не первый выбор для предоставления ресурсов Kubernetes.
С Kubestack, фреймворком Terraform с открытым исходным кодом, который я поддерживаю, я выполняю миссию по обеспечению наилучшего опыта разработчиков для команд, работающих с Terraform и Kubernetes. И унифицированное предоставление всех компонентов платформы, от инфраструктуры кластера до кластерных сервисов, – это то, что я считаю критически важным в моем неустанном стремлении к обеспечению такого опыта для разработчиков.
Поэтому два распространенных подхода к обеспечению ресурсов Kubernetes с помощью Terraform никогда меня не привлекали.
С одной стороны, есть провайдер Kubernetes. И хотя он интегрирует ресурсы Kubernetes в Terraform, поддержка ресурсов Kubernetes в HCL требует больших усилий. Особенно для Kubernetes YAML, который вы потребляете из upstream. С другой стороны, есть провайдер Helm и провайдер Kubectl. Эти два используют родной YAML вместо HCL, но не интегрируют ресурсы Kubernetes в состояние Terraform и, как следствие, жизненный цикл.
Я считаю, что мои модули на основе провайдера Kustomize являются лучшей альтернативой из-за трех очевидных преимуществ:
- Как и в случае с Kustomize, YAML восходящего потока остается нетронутым, что означает, что обновления восходящего потока требуют минимальных усилий по обслуживанию.
- Благодаря определению оверлея Kustomize в HCL, все ресурсы Kubernetes полностью настраиваются с помощью значений из Terraform.
- Каждый ресурс Kubernetes отслеживается индивидуально в состоянии Terraform, поэтому дифы и планы показывают изменения в фактических ресурсах Kubernetes.
Чтобы сделать эти преимущества менее абстрактными, давайте сравним мой модуль Nginx ingress с модулем, использующим провайдер Helm для обеспечения Nginx ingress.
Конфигурация Terraform для обоих примеров доступна в этом репозитории. Сначала рассмотрим модуль Helm.
Модуль на основе Helm
Использование модуля очень простое. Сначала настройте провайдеры Kubernetes и Helm.
provider "kubernetes" {
config_path = "~/.kube/config"
}
provider "helm" {
kubernetes {
config_path = "~/.kube/config"
}
}
Затем определите пространство имен kubernetes_namespace и вызовите модуль release/helm.
resource "kubernetes_namespace" "nginx_ingress" {
metadata {
name = "ingress-nginx"
}
}
module "nginx_ingress" {
source = "terraform-module/release/helm"
version = "2.7.0"
namespace = kubernetes_namespace.nginx_ingress.metadata.0.name
repository = "https://kubernetes.github.io/ingress-nginx"
app = {
name = "ingress-nginx"
version = "4.1.0"
chart = "ingress-nginx"
force_update = true
wait = false
recreate_pods = false
deploy = 1
}
set = [
{
name = "replicaCount"
value = 2
}
]
}
Если теперь запустить план terraform для этой конфигурации, вы увидите ресурсы, которые будут созданы.
Terraform will perform the following actions:
# kubernetes_namespace.nginx_ingress will be created
+ resource "kubernetes_namespace" "nginx_ingress" {
+ id = (known after apply)
+ metadata {
+ generation = (known after apply)
+ name = "ingress-nginx"
+ resource_version = (known after apply)
+ uid = (known after apply)
}
}
# module.nginx_ingress.helm_release.this[0] will be created
+ resource "helm_release" "this" {
+ atomic = false
+ chart = "ingress-nginx"
+ cleanup_on_fail = false
+ create_namespace = false
+ dependency_update = false
+ disable_crd_hooks = false
+ disable_openapi_validation = false
+ disable_webhooks = false
+ force_update = true
+ id = (known after apply)
+ lint = true
+ manifest = (known after apply)
+ max_history = 0
+ metadata = (known after apply)
+ name = "ingress-nginx"
+ namespace = "ingress-nginx"
+ recreate_pods = false
+ render_subchart_notes = true
+ replace = false
+ repository = "https://kubernetes.github.io/ingress-nginx"
+ reset_values = false
+ reuse_values = false
+ skip_crds = false
+ status = "deployed"
+ timeout = 300
+ values = []
+ verify = false
+ version = "4.1.0"
+ wait = false
+ wait_for_jobs = false
+ set {
+ name = "replicaCount"
+ value = "2"
}
}
Plan: 2 to add, 0 to change, 0 to destroy.
И это ключевая проблема того, как Helm интегрирован в рабочий процесс Terraform. План не говорит вам, какие ресурсы Kubernetes будут созданы для ингресс-контроллера Nginx. И ресурсы Kubernetes также не отслеживаются в состоянии Terraform, как показано в выводе apply.
kubernetes_namespace.nginx_ingress: Creating...
kubernetes_namespace.nginx_ingress: Creation complete after 0s [id=ingress-nginx]
module.nginx_ingress.helm_release.this[0]: Creating...
module.nginx_ingress.helm_release.this[0]: Creation complete after 3s [id=ingress-nginx]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Аналогично, при планировании изменений опять же нет возможности определить, какие изменения будут внесены в ресурсы Kubernetes.
Так, если вы увеличите значение replicaCount
графика Helm, план терраформирования просто покажет изменение ресурса helm_release
.
set = [
{
name = "replicaCount"
value = 3
}
]
Каковы будут изменения в ресурсах Kubernetes? И, что более важно, будет ли это простое обновление на месте или потребуется уничтожение и воссоздание? Глядя на план, вы не можете этого знать.
Terraform will perform the following actions:
# module.nginx_ingress.helm_release.this[0] will be updated in-place
~ resource "helm_release" "this" {
id = "ingress-nginx"
name = "ingress-nginx"
# (27 unchanged attributes hidden)
- set {
- name = "replicaCount" -> null
- value = "2" -> null
}
+ set {
+ name = "replicaCount"
+ value = "3"
}
}
Plan: 0 to add, 1 to change, 0 to destroy.
Модуль на основе Kustomize
Теперь давайте рассмотрим те же шаги для модуля на основе Kustomize. Использование аналогично. Сначала необходимо установить провайдер kbst/kustomization и настроить его.
terraform {
required_providers {
kustomization = {
source = "kbst/kustomization"
}
}
}
provider "kustomization" {
kubeconfig_path = "~/.kube/config"
}
Затем вызовите модуль nginx/kustomization.
module "nginx_ingress" {
source = "kbst.xyz/catalog/nginx/kustomization"
version = "1.1.3-kbst.1"
configuration_base_key = "default"
configuration = {
default = {
replicas = [{
name = "ingress-nginx-controller"
count = 2
}]
}
}
}
В отличие от модуля на базе Helm, при запуске terraform plan теперь вы увидите каждый ресурс Kubernetes и его фактическую конфигурацию по отдельности. Чтобы сделать эту статью в блоге более понятной, я показываю детали только для пространства имен.
Terraform will perform the following actions:
# module.nginx_ingress.kustomization_resource.p0["_/Namespace/_/ingress-nginx"] will be created
+ resource "kustomization_resource" "p0" {
+ id = (known after apply)
+ manifest = jsonencode(
{
+ apiVersion = "v1"
+ kind = "Namespace"
+ metadata = {
+ annotations = {
+ "app.kubernetes.io/version" = "v0.46.0"
+ "catalog.kubestack.com/heritage" = "kubestack.com/catalog/nginx"
+ "catalog.kubestack.com/variant" = "base"
}
+ labels = {
+ "app.kubernetes.io/component" = "ingress-controller"
+ "app.kubernetes.io/instance" = "ingress-nginx"
+ "app.kubernetes.io/managed-by" = "kubestack"
+ "app.kubernetes.io/name" = "nginx"
}
+ name = "ingress-nginx"
}
}
)
}
# module.nginx_ingress.kustomization_resource.p1["_/ConfigMap/ingress-nginx/ingress-nginx-controller"] will be created
# module.nginx_ingress.kustomization_resource.p1["_/Service/ingress-nginx/ingress-nginx-controller"] will be created
# module.nginx_ingress.kustomization_resource.p1["_/Service/ingress-nginx/ingress-nginx-controller-admission"] will be created
# module.nginx_ingress.kustomization_resource.p1["_/ServiceAccount/ingress-nginx/ingress-nginx"] will be created
# module.nginx_ingress.kustomization_resource.p1["_/ServiceAccount/ingress-nginx/ingress-nginx-admission"] will be created
# module.nginx_ingress.kustomization_resource.p1["apps/Deployment/ingress-nginx/ingress-nginx-controller"] will be created
# module.nginx_ingress.kustomization_resource.p1["batch/Job/ingress-nginx/ingress-nginx-admission-create"] will be created
# module.nginx_ingress.kustomization_resource.p1["batch/Job/ingress-nginx/ingress-nginx-admission-patch"] will be created
# module.nginx_ingress.kustomization_resource.p1["networking.k8s.io/IngressClass/_/nginx"] will be created
# module.nginx_ingress.kustomization_resource.p1["rbac.authorization.k8s.io/ClusterRole/_/ingress-nginx"] will be created
# module.nginx_ingress.kustomization_resource.p1["rbac.authorization.k8s.io/ClusterRole/_/ingress-nginx-admission"] will be created
# module.nginx_ingress.kustomization_resource.p1["rbac.authorization.k8s.io/ClusterRoleBinding/_/ingress-nginx"] will be created
# module.nginx_ingress.kustomization_resource.p1["rbac.authorization.k8s.io/ClusterRoleBinding/_/ingress-nginx-admission"] will be created
# module.nginx_ingress.kustomization_resource.p1["rbac.authorization.k8s.io/Role/ingress-nginx/ingress-nginx"] will be created
# module.nginx_ingress.kustomization_resource.p1["rbac.authorization.k8s.io/Role/ingress-nginx/ingress-nginx-admission"] will be created
# module.nginx_ingress.kustomization_resource.p1["rbac.authorization.k8s.io/RoleBinding/ingress-nginx/ingress-nginx"] will be created
# module.nginx_ingress.kustomization_resource.p1["rbac.authorization.k8s.io/RoleBinding/ingress-nginx/ingress-nginx-admission"] will be created
# module.nginx_ingress.kustomization_resource.p2["admissionregistration.k8s.io/ValidatingWebhookConfiguration/_/ingress-nginx-admission"] will be created
Plan: 19 to add, 0 to change, 0 to destroy.
В приложении, опять же, есть все отдельные ресурсы Kubernetes. А поскольку модули используют явные depends_on
для обработки пространств имен и CRD в первую очередь, а webhooks – в последнюю, ресурсы надежно применяются в правильном порядке.
module.nginx_ingress.kustomization_resource.p0["_/Namespace/_/ingress-nginx"]: Creating...
module.nginx_ingress.kustomization_resource.p0["_/Namespace/_/ingress-nginx"]: Creation complete after 0s [id=369e8643-ad33-4eb4-95dc-f506cef4a198]
module.nginx_ingress.kustomization_resource.p1["rbac.authorization.k8s.io/RoleBinding/ingress-nginx/ingress-nginx"]: Creating...
module.nginx_ingress.kustomization_resource.p1["batch/Job/ingress-nginx/ingress-nginx-admission-create"]: Creating...
...
module.nginx_ingress.kustomization_resource.p1["batch/Job/ingress-nginx/ingress-nginx-admission-patch"]: Creation complete after 1s [id=58346878-70bd-42f2-af61-2730e3435ca7]
module.nginx_ingress.kustomization_resource.p1["_/ServiceAccount/ingress-nginx/ingress-nginx"]: Creation complete after 0s [id=f009bbb7-7d2e-4f28-a826-ce133c91cc15]
module.nginx_ingress.kustomization_resource.p2["admissionregistration.k8s.io/ValidatingWebhookConfiguration/_/ingress-nginx-admission"]: Creating...
module.nginx_ingress.kustomization_resource.p2["admissionregistration.k8s.io/ValidatingWebhookConfiguration/_/ingress-nginx-admission"]: Creation complete after 1s [id=3185b09f-1f67-4079-b44f-de01bff44bd2]
Apply complete! Resources: 19 added, 0 changed, 0 destroyed.
Естественно, это также означает, что если вы увеличите количество реплик, как это…
replicas = [{
name = "ingress-nginx-controller"
count = 3
}]
…план терраформирования показывает, какие ресурсы Kubernetes изменятся и какова разница.
Terraform will perform the following actions:
# module.nginx_ingress.kustomization_resource.p1["apps/Deployment/ingress-nginx/ingress-nginx-controller"] will be updated in-place
~ resource "kustomization_resource" "p1" {
id = "81e8ff18-6c6c-440d-bd8b-bf5f0d016953"
~ manifest = jsonencode(
~ {
~ spec = {
~ replicas = 2 -> 3
# (4 unchanged elements hidden)
}
# (3 unchanged elements hidden)
}
)
}
Plan: 0 to add, 1 to change, 0 to destroy.
Возможно, даже более важным является то, что провайдер Kustomization также правильно покажет, можно ли изменить ресурс с помощью обновления на месте. Или если требуется уничтожение и воссоздание, например, из-за изменения неизменяемого поля.
Это результат двух вещей:
- Как вы только что видели, каждый ресурс Kubernetes обрабатывается индивидуально в Terraform state, и
- что провайдер Kustomization использует сухие прогоны Kubernetes на стороне сервера для определения diff каждого ресурса.
Основываясь на результатах этого сухого прогона, провайдер дает Terraform указание создать план in-place или destroy-and-recreate.
Итак, в качестве примера такого изменения представьте, что вам нужно изменить spec.selector.matchLabels
. Поскольку matchLabels
является неизменяемым полем, вы увидите план, в котором говорится, что ресурс Deployment должен быть заменен. В сводке плана вы увидите 1 добавить и 1 уничтожить.
Terraform will perform the following actions:
# module.nginx_ingress.kustomization_resource.p1["apps/Deployment/ingress-nginx/ingress-nginx-controller"] must be replaced
-/+ resource "kustomization_resource" "p1" {
~ id = "81e8ff18-6c6c-440d-bd8b-bf5f0d016953" -> (known after apply)
~ manifest = jsonencode(
~ {
~ metadata = {
~ labels = {
+ example-selector = "example"
# (6 unchanged elements hidden)
}
name = "ingress-nginx-controller"
# (2 unchanged elements hidden)
}
~ spec = {
~ replicas = 2 -> 3
~ selector = {
~ matchLabels = {
+ example-selector = "example"
# (4 unchanged elements hidden)
}
}
~ template = {
~ metadata = {
~ labels = {
+ example-selector = "example"
# (4 unchanged elements hidden)
}
# (1 unchanged element hidden)
}
# (1 unchanged element hidden)
}
# (2 unchanged elements hidden)
}
# (2 unchanged elements hidden)
} # forces replacement
)
}
Plan: 1 to add, 0 to change, 1 to destroy.
Попробуйте сами
Вы можете найти исходный код для сравнения на GitHub, если хотите сами поэкспериментировать с различиями.
Если вы хотите попробовать модули Kustomize самостоятельно, вы можете использовать один из модулей из каталога, которые связывают upstream YAML, например, оператор Prometheus, Cert-Manager, Sealed secrets или Tekton.
Но это работает не только для сервисов восходящего потока. Существует также модуль, который можно использовать для обеспечения любого YAML Kubernetes точно таким же образом, как и модули каталога – он называется модулем пользовательского манифеста.
Принять участие
В настоящее время количество сервисов, доступных из каталога, все еще ограничено.
Если вы хотите принять участие, вы также можете найти исходники каталога на GitHub.
Фото Владислава Бабиенко на Unsplash