CloudNativePG: подключение внешних приложений

Теперь, когда мы разобрались, как провести бенчмаркинг развёртывания CloudNativePG, пора рассмотреть, как подключать внешние приложения к кластеру PostgreSQL. Обычно приложения работают в том же кластере Kubernetes и могут напрямую обращаться к нашему развёртыванию PostgreSQL, но иногда требуется подключить и внешние приложения или сервисы. По умолчанию это не работает, поскольку наружу ничего не опубликовано.

Убедиться в этом легко, посмотрев на сервисы, которые у нас сейчас есть:

k8s@k8s1:~$ kubectl get services -n default
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 32d
my-pg-cluster-r ClusterIP 10.107.190.52 <none> 5432/TCP 8d
my-pg-cluster-ro ClusterIP 10.109.169.21 <none> 5432/TCP 8d
my-pg-cluster-rw ClusterIP 10.103.171.191 <none> 5432/TCP 8d

Для всех подов кластера есть IP-адреса и сервисы, но эти адреса доступны только внутри кластера. Во внешних IP-адресах у всех них стоит "<none>".

Прежде чем сделать эти сервисы доступными снаружи, быстро разберём, что они означают:

  • my-pg-cluster-r: подключается к любому из узлов для операций только на чтение

  • my-pg-cluster-ro: всегда подключается к реплике только для чтения (hot standby)

  • my-pg-cluster-rw: всегда подключается к первичному узлу (primary)

Что бы ни подключалось к кластеру, оно должно использовать один из этих сервисов и никогда не подключаться к экземпляру PostgreSQL напрямую. Причина в том, что эти сервисы управляются оператором, и для подключения к сервисам кластера следует полагаться на внутренний DNS Kubernetes.

Чтобы опубликовать сервисы кластера PostgreSQL наружу, нам нужен Ingress и Ingress Controller поверх него в связке с балансировщиком нагрузки.

Одним из весьма популярных Ingress-контроллеров является Ingress-Nginx Controller, и именно его мы здесь и будем использовать. Установить его снова легко с помощью Helm, практически так же, как мы делали это с OpenEBS в статье про хранилище, но сначала мы развернём балансировщик нагрузки MetalLB:

k8s@k8s1:~$ helm install metallb metallb/metallb --namespace metallb-system --create-namespace
NAME: metallb
LAST DEPLOYED: Fri Aug 9 09:43:03 2024
NAMESPACE: metallb-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
MetalLB is now running in the cluster.
Now you can configure it via its CRs. Please refer to the metallb official docs
on how to use the CRs.

Это создаёт новое пространство имён с названием "metallb-system" и несколько подов:

k8s@k8s1:~$ kubectl get pods -A | grep metal
metallb-system metallb-controller-77cb7f5d88-hxndw 1/1 Running 0 26s
metallb-system metallb-speaker-5phx6 4/4 Running 0 26s
metallb-system metallb-speaker-bjdxj 4/4 Running 0 26s
metallb-system metallb-speaker-c54z6 4/4 Running 0 26s
metallb-system metallb-speaker-xzphl 4/4 Running 0 26s

Следующий шаг — создать Ingress-Nginx Controller:

k8s@k8s1:~$ helm upgrade --install ingress-nginx ingress-nginx --repo https://kubernetes.github.io/ingress-nginx --namespace ingress-nginx --create-namespace
Release "ingress-nginx" does not exist. Installing it now.
NAME: ingress-nginx
LAST DEPLOYED: Fri Aug 9 09:49:43 2024
NAMESPACE: ingress-nginx
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
The ingress-nginx controller has been installed.
It may take a few minutes for the load balancer IP to be available.
You can watch the status by running 'kubectl get service --namespace ingress-nginx ingress-nginx-controller --output wide --watch'
An example Ingress that makes use of the controller:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example
namespace: foo
spec:
ingressClassName: nginx
rules:
- host: www.example.com
http:
paths:
- pathType: Prefix
backend:
service:
name: exampleService
port:
number: 80
path: /
# This section is only required if TLS is to be enabled for the Ingress
tls:
- hosts:
- www.example.com
secretName: example-tls
If TLS is enabled for the Ingress, a Secret containing the certificate and key must also be provided:
apiVersion: v1
kind: Secret
metadata:
name: example-tls
namespace: foo
data:
tls.crt: <base64 encoded cert>
tls.key: <base64 encoded key>
type: kubernetes.io/tls

Та же история и здесь — мы получаем новое пространство имён:

k8s@k8s1:~$ kubectl get pods -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-69bd47995d-krt7h 1/1 Running 0 2m33s

На этом этапе вы заметите, что у нас по-прежнему нет ни одного сервиса, опубликованного наружу (в EXTERNAL-IP мы всё ещё видим "<pending>"):

k8s@k8s1:~$ kubectl get svc -A | grep nginx
ingress-nginx ingress-nginx-controller LoadBalancer 10.109.240.37 <pending> 80:31719/TCP,443:32412/TCP 103s
ingress-nginx ingress-nginx-controller-admission ClusterIP 10.103.255.169 <none> 443/TCP 103s

Это неудивительно, поскольку мы не сообщили балансировщику нагрузки, какие IP-адреса запрашивать/назначать. Делается это легко:

k8s@k8s1:~$ cat lb.yaml
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: default
namespace: metallb-system
spec:
addresses:
- 192.168.122.210-192.168.122.215
autoAssign: true
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: default
namespace: metallb-system
spec:
ipAddressPools:
- default
k8s@k8s1:~$ kubectl apply -f lb.yaml
ipaddresspool.metallb.io/default created
l2advertisement.metallb.io/default created
k8s@k8s1:~$ kubectl get services -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 10.109.240.37 192.168.122.210 80:31719/TCP,443:32412/TCP 3m32s
ingress-nginx-controller-admission ClusterIP 10.103.255.169 <none> 443/TCP 3m32s

С этого момента LoadBalancer автоматически получил IP-адрес из пула адресов, которые мы назначили. Дальнейшие шаги описаны в документации CloudNativePG: сначала нам нужен config map для сервиса, который мы хотим опубликовать:

k8s@k8s1:~$ cat tcp-services-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: tcp-services
namespace: ingress-nginx
data:
5432: default/my-pg-cluster-rw:5432
k8s@k8s1:~$ kubectl apply -f tcp-services-configmap.yaml
configmap/tcp-services created
k8s@k8s1:~$ kubectl get cm -n ingress-nginx
NAME DATA AGE
ingress-nginx-controller 1 6m4s
kube-root-ca.crt 1 6m8s
tcp-services 1 12s

Теперь нужно изменить сервис ingress-nginx, чтобы добавить новый порт:

k8s@k8s1:~$ kubectl get svc ingress-nginx-controller -n ingress-nginx -o yaml > service.yaml
k8s@k8s1:~$ vi service.yaml
...
ports:
- appProtocol: http
name: http
nodePort: 31719
port: 80
protocol: TCP
targetPort: http
- appProtocol: https
name: https
nodePort: 32412
port: 443
protocol: TCP
targetPort: https
- appProtocol: tcp
name: postgres
port: 5432
targetPort: 5432
...
k8s@k8s1:~$ kubectl apply -f service.yaml
Warning: resource services/ingress-nginx-controller is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
service/ingress-nginx-controller configured

Последний шаг — подключить наш config map к развёртыванию "ingress-nginx-controller":

k8s@k8s1:~$ kubectl edit deploy ingress-nginx-controller -n ingress-nginx
...
spec:
containers:
- args:
- /nginx-ingress-controller
- --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller
- --election-id=ingress-nginx-leader
- --controller-class=k8s.io/ingress-nginx
- --ingress-class=nginx
- --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
- --tcp-services-configmap=ingress-nginx/tcp-services
- --validating-webhook=:8443
- --validating-webhook-certificate=/usr/local/certificates/cert
- --validating-webhook-key=/usr/local/certificates/key
- --enable-metrics=false
...

С этого момента к кластеру PostgreSQL можно обращаться извне кластера Kubernetes:

k8s@k8s1:~$ kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 10.109.240.37 192.168.122.210 80:31719/TCP,443:32412/TCP,5432:32043/TCP 6d23h
ingress-nginx-controller-admission ClusterIP 10.103.255.169 <none> 443/TCP 6d23h
k8s@k8s1:~$ psql -h 192.168.122.210
Password for user k8s:
psql: error: connection to server at "192.168.122.210", port 5432 failed: FATAL: password authentication failed for user "k8s"
connection to server at "192.168.122.210", port 5432 failed: FATAL: password authentication failed for user "k8s"

Ошибка аутентификации по паролю в данном случае — это хороший знак: соединение с сервером PostgreSQL на порту 5432 по внешнему IP-адресу успешно установлено, а отказ произошёл лишь потому, что пользователь k8s не является корректной учётной записью PostgreSQL. Публикация сервиса my-pg-cluster-rw наружу через MetalLB и Ingress-Nginx работает как задумано.

© 2026 meganuke