В данном туториале максимально просто расскажу и покажу на практике как настроить автоматический выпуск сертификатов в локальном kubernetes так, что бы ваша локальная машина доверяла им. Я постарался написать его так, чтобы даже новичкам можно было настроить свой куб просто следуя данной инструкции.
Итак, наш план примерно такой:
Развернем локально кластер локально. Установим MetalLB, cert-manager, ingress и поднимем тестовые приложения grafana и argocd.
Настроим нашу локальную машину так, чтобы она доверяла выпущенным в кубе сертификатам.
Задействуем nip.io, чтобы не прописывать локально в hosts хостнеймы разворачиваемых приложений.
Поднимем grafana и argocd двумя разными способами (yaml и helm chart).
Поднимаем кластер
Тут совершенно нет никаких проблем, я буду использовать стандартный Docker Desktop для запуска. Ставим галочку в настройках Docker Desktop, что нам нужен куб и погнали дальше.

Установка и настройка MetalLB
Теперь давайте установим MetalLB и сконфигурируем просто для нашего удобства. MetalLB выдаст ingress Load Balancer наш локальный IP-адрес.
MetalLB — это балансировщик нагрузки для Kubernetes, предназначенный для работы в средах, где нет встроенного облачного балансировщика, например, в bare-metal кластерах.
# Для начала проверим что мы точно в нужном кластере $ kubectl config get-contexts CURRENT NAME CLUSTER AUTHINFO NAMESPACE * docker-desktop docker-desktop docker-desktop orbstack orbstack orbstack # Устанавливаем $ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/main/config/manifests/metallb-native.yaml namespace/metallb-system created ....... serviceaccount/speaker created ....... validatingwebhookconfiguration.admissionregistration.k8s.io/metallb-webhook-configuration created # Проверяем что подики поднялись $ kubectl get pods -n metallb-system -w NAME READY STATUS RESTARTS AGE controller-8b7b6bf6b-4phg6 0/1 Running 0 5s speaker-h75cw 0/1 Running 0 5s controller-8b7b6bf6b-bc2jw 1/1 Running 0 36s speaker-xd8xt 1/1 Running 0 36s
Теперь сразу же настроим, чтобы выдавался только IP 127.0.0.1. Если у вас есть необходимость пошарить ваши приложения из куба в локальную сеть, то вы можете указать ваш локальный IP адрес в качестве диапазона адресов для пула metallb.
$ kubectl apply -f - <<EOF apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: ingress-ip-pool namespace: metallb-system spec: addresses: - "127.0.0.1-127.0.0.1" --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: advert namespace: metallb-system EOF
Домены, сертификаты и Ingress
Для начала нам надо выпустить корневой сертификат. Для этих целей будем использовать mkcert. Это утилита, которая позволяет легко создавать локальные SSL/TLS-сертификаты без необходимости подписывать их у внешнего удостоверяющего центра (CA). Основное преимущество mkcert — автоматическая генерация доверенного корневого сертификата и выпуск локальных сертификатов, которые сразу же распознаются браузерами и системами без дополнительных настроек. Я не буду описывать процесс установки. Это есть в документации.
# Запустим утилиту. Это надо сделать один раз, она сгенерит CA и пропишет в нашу ОС. $ mkcert --install
Далее установим наш cert-manager в kubernetes и добавим наш CA в кластер, чтобы мы могли выпускать сертификаты.
# Устанавливаем cert-manager $ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.1/cert-manager.crds.yaml $ helm repo add jetstack https://charts.jetstack.io --force-update $ helm install cert-manager --namespace cert-manager --version v1.17.1 jetstack/cert-manager --create-namespace # cert-manager сможет использовать этот CA для автоматической выдачи сертификатов в кластере. $ kubectl create secret tls mkcert-ca-key-pair --key "$(mkcert -CAROOT)"/rootCA-key.pem --cert "$(mkcert -CAROOT)"/rootCA.pem -n cert-manager # Создаем объект ClusterIssuer в Kubernetes, который будет использовать сертификаты из секрета mkcert-ca-key-pair для подписывания локальных TLS-сертификатов. $ kubectl apply -f - <<EOF apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: mkcert-issuer namespace: cert-manager spec: ca: secretName: mkcert-ca-key-pair EOF
Поскольку мы хотим в ingress задавать нужный домен и чтобы он сразу был доступен на локальной машине, то нет ничего проще чем использовать nip.io.
nip.io — это бесплатный сервис для динамического DNS, который позволяет использовать валидные доменные имена, привязанные к IP-адресу. Сервис автоматически подставляет IP-адрес при запросе. Ты просто используешь домен в формате: <IP-адрес>.nip.io. Например:
grafana.127.0.0.1.nip.io→ резолвится в127.0.0.1192.168.1.52.nip.io→ разолвится в192.168.1.52demo.203.0.113.20.nip.io→ резолвится в203.0.113.20
Таким образом наши приложения будут иметь домены grafana.127.0.0.1.nip.io и argocd.127.0.0.1.nip.io. И они оба будут резолвиться в 127.0.0.1. В файл hosts ничего прописывать не нужно.
Теперь давайте создадим wildcard сертификат, который будет использован по умолчанию в нашем будущем ingress.
$ kubectl apply -f - <<EOF apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: default-ingress-cert namespace: cert-manager spec: secretName: default-ingress-cert issuerRef: name: mkcert-issuer kind: ClusterIssuer dnsNames: - "*.127.0.0.1.nip.io" EOF
Устанавливаем ingress и делаем так, чтобы он использовал этот сертификат по умолчанию. Как вы уже поняли это позволит нам не выпускать каждый раз новые сертификаты для наших приложений. Хотя в целом вам ничего не мешает использовать аннотацию в ingress для выпуска сертификата под любой другой домен.
$ helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx --force-update $ helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx --create-namespace \ --set controller.extraArgs.default-ssl-certificate="cert-manager/default-ingress-cert"

Запускаем приложения и тестируем
Применяем подготовленные манифесты с Deployment, Service и Ingress для запуска grafana. Данный пост не преследует цель показать best way запуска grafana . Поэтому мы опустим все нюансы pv, сессий и тд. Пример ниже мы просто используем для демонстрации.
kubectl create namespace grafana kubectl apply -f - <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: grafana-deployment namespace: grafana labels: app: grafana spec: replicas: 1 selector: matchLabels: app: grafana template: metadata: labels: app: grafana spec: containers: - image: grafana/grafana:11.5.1 name: grafana ports: - containerPort: 3000 --- apiVersion: v1 kind: Service metadata: name: grafana-service namespace: grafana labels: app: grafana spec: selector: app: grafana ports: - protocol: TCP port: 3000 targetPort: 3000 type: ClusterIP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: grafana-ingress namespace: grafana spec: ingressClassName: nginx tls: - hosts: - grafana.127.0.0.1.nip.io rules: - host: grafana.127.0.0.1.nip.io http: paths: - path: / pathType: Prefix backend: service: name: grafana-service port: number: 3000 EOF

Хочу обратить внимание, что я специально не задавал аннотаций. В нашем случае будет использован уже выпущенный сертификат ранее default-ingress-cert. Но если необходимо использовать для своего приложения отдельный, то вам ничего не мешает задействовать аннотации к ingress.
Пример ingress.yml с выпуском сертификата для grafana.127.0.0.1.nip.io
--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: grafana-ingress annotations: cert-manager.io/cluster-issuer: mkcert-issuer spec: ingressClassName: nginx tls: - hosts: - grafana.127.0.0.1.nip.io secretName: grafana-ingress-cert rules: - host: grafana.127.0.0.1.nip.io http: paths: - path: / pathType: Prefix backend: service: name: grafana-service port: number: 3000
В результате получаем приложение с валидным на локальной машине самоподписным сертификатом по адресу https://grafana.127.0.0.1.nip.io.

Давайте для примера еще установим argocd и проверим что все работает как надо.
$ helm repo add bitnami https://charts.bitnami.com/bitnami --force-update $ helm install argocd bitnami/argo-cd \ --namespace argocd --create-namespace \ --set server.ingress.enabled=true \ --set server.ingress.hostname="argocd.127.0.0.1.nip.io" \ --set server.ingress.ingressClassName="nginx" \ --set server.ingress.path="/" \ --set server.ingress.pathType="Prefix" \ --set server.ingress.tls=true \ --set server.insecure=true
В результате получаем agocd, прикрытый выпущенным сертификатом, валидный в рамках нашей машины.

Хватит читать DevOps-статьи от людей без продакшена. Я рассказываю про свой реальный опыт в своем Telegram-канале DevOps Brain 🧠 ↩
