В данном туториале максимально просто расскажу и покажу на практике как настроить автоматический выпуск сертификатов в локальном 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.1
192.168.1.52.nip.io
→ разолвится в192.168.1.52
demo.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, прикрытый выпущенным сертификатом, валидный в рамках нашей машины.

А на этом всё — спасибо за внимание. При желании заглядывайте в тележку.