Implemente o PostgreSQL no GKE através do Zalando

O guia mostra como usar o operador Postgres da Zalando para implementar clusters Postgres no Google Kubernetes Engine (GKE).

O PostgreSQL é um sistema de base de dados relacional de objetos de código aberto potente com várias décadas de desenvolvimento ativo que lhe valeu uma forte reputação de fiabilidade, robustez de funcionalidades e desempenho.

Este guia destina-se a administradores de plataformas, arquitetos de nuvem e profissionais de operações interessados em executar o PostgreSQL como uma aplicação de base de dados no GKE em vez de usar o Cloud SQL para PostgreSQL.

Configure o seu ambiente

Para configurar o seu ambiente, siga estes passos

  1. Defina variáveis de ambiente:

    export PROJECT_ID=PROJECT_ID
    export KUBERNETES_CLUSTER_PREFIX=postgres
    export REGION=us-central1
    

    Substitua PROJECT_ID pelo seu Google Cloud ID do projeto.

  2. Clone o repositório do GitHub:

    git clone https://github.com/GoogleCloudPlatform/kubernetes-engine-samples
    
  3. Mude para o diretório de trabalho:

    cd kubernetes-engine-samples/databases/postgres-zalando
    

Crie a infraestrutura do cluster

Nesta secção, executa um script do Terraform para criar um cluster do GKE privado, de alta disponibilidade e regional.

Pode instalar o operador através de um cluster padrão ou do Autopilot.

Standard

O diagrama seguinte mostra um cluster GKE padrão regional privado implementado em três zonas diferentes:

Implemente esta infraestrutura:

export GOOGLE_OAUTH_ACCESS_TOKEN=$(gcloud auth print-access-token)
terraform -chdir=terraform/gke-standard init
terraform -chdir=terraform/gke-standard apply \
  -var project_id=${PROJECT_ID} \
  -var region=${REGION} \
  -var cluster_prefix=${KUBERNETES_CLUSTER_PREFIX}

Quando lhe for pedido, escreva yes. Este comando pode demorar vários minutos a ser concluído e o cluster a apresentar o estado pronto.

O Terraform cria os seguintes recursos:

  • Uma rede VPC e uma sub-rede privada para os nós do Kubernetes
  • Um router para aceder à Internet através de NAT
  • Um cluster do GKE privado na região de us-central1
  • Um conjunto de nós com o dimensionamento automático ativado (um a dois nós por zona, um nó por zona no mínimo)
  • Um ServiceAccount com autorizações de registo e monitorização
  • Cópia de segurança do GKE para recuperação de desastres
  • Google Cloud Managed Service for Prometheus para monitorização de clusters

O resultado é semelhante ao seguinte:

...
Apply complete! Resources: 14 added, 0 changed, 0 destroyed.
...

Piloto automático

O diagrama seguinte mostra um cluster do GKE Autopilot regional privado:

Implemente a infraestrutura:

export GOOGLE_OAUTH_ACCESS_TOKEN=$(gcloud auth print-access-token)
terraform -chdir=terraform/gke-autopilot init
terraform -chdir=terraform/gke-autopilot apply \
  -var project_id=${PROJECT_ID} \
  -var region=${REGION} \
  -var cluster_prefix=${KUBERNETES_CLUSTER_PREFIX}

Quando lhe for pedido, escreva yes. Este comando pode demorar vários minutos a ser concluído e o cluster a apresentar o estado pronto.

O Terraform cria os seguintes recursos:

  • Uma rede VPC e uma sub-rede privada para os nós do Kubernetes
  • Um router para aceder à Internet através de NAT
  • Um cluster do GKE privado na região de us-central1
  • Um ServiceAccount com autorização de registo e monitorização
  • Google Cloud Managed Service for Prometheus para monitorização de clusters

O resultado é semelhante ao seguinte:

...
Apply complete! Resources: 12 added, 0 changed, 0 destroyed.
...

Estabeleça ligação ao cluster

Configure o kubectl para comunicar com o cluster:

gcloud container clusters get-credentials ${KUBERNETES_CLUSTER_PREFIX}-cluster --location ${REGION}

Implemente o operador Zalando no seu cluster

Implemente o operador Zalando no seu cluster do Kubernetes através de um gráfico Helm.

  1. Adicione o repositório do gráfico Helm do operador Zalando:

    helm repo add postgres-operator-charts https://opensource.zalando.com/postgres-operator/charts/postgres-operator
    
  2. Crie um espaço de nomes para o operador Zalando e o cluster Postgres:

    kubectl create ns postgres
    kubectl create ns zalando
    
  3. Implemente o operador Zalando através da ferramenta de linha de comandos Helm:

    helm install postgres-operator postgres-operator-charts/postgres-operator -n zalando \
        --set configKubernetes.enable_pod_antiaffinity=true \
        --set configKubernetes.pod_antiaffinity_preferred_during_scheduling=true \
        --set configKubernetes.pod_antiaffinity_topology_key="topology.kubernetes.io/zone" \
        --set configKubernetes.spilo_fsgroup="103"
    

    Não pode configurar as definições podAntiAffinity diretamente no recurso personalizado que representa o cluster do Postgres. Em alternativa, defina as podAntiAffinity definições globalmente para todos os clusters do Postgres nas definições do operador.

  4. Verifique o estado da implementação do operador Zalando através do Helm:

    helm ls -n zalando
    

    O resultado é semelhante ao seguinte:

    NAME                 NAMESPACE    REVISION    UPDATED                                STATUS      CHART                       APP VERSION
    postgres-operator    zalando     1           2023-10-13 16:04:13.945614 +0200 CEST    deployed    postgres-operator-1.10.1    1.10.1
    

Implemente o Postgres

A configuração básica da instância do cluster Postgres inclui os seguintes componentes:

  • Três réplicas do Postgres: um líder e duas réplicas em espera.
  • Alocação de recursos de CPU de um pedido de CPU e dois limites de CPU, com 4 GB de pedidos e limites de memória.
  • Tolerâncias, nodeAffinities e topologySpreadConstraints configurados para cada carga de trabalho, garantindo a distribuição adequada pelos nós do Kubernetes, usando os respetivos node pools e diferentes zonas de disponibilidade.

Esta configuração representa a configuração mínima necessária para criar um cluster do Postgres pronto para produção.

O manifesto seguinte descreve um cluster do Postgres:

apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: my-cluster
spec:
  dockerImage: ghcr.io/zalando/spilo-15:3.0-p1
  teamId: "my-team"
  numberOfInstances: 3
  users:
    mydatabaseowner:
    - superuser
    - createdb
    myuser: []
  databases:
    mydatabase: mydatabaseowner
  postgresql:
    version: "15"
    parameters:
      shared_buffers: "32MB"
      max_connections: "10"
      log_statement: "all"
      password_encryption: scram-sha-256
  volume:
    size: 5Gi
    storageClass: premium-rwo
  enableShmVolume: true
  podAnnotations:
    cluster-autoscaler.kubernetes.io/safe-to-evict: "true"
  tolerations:
  - key: "app.stateful/component"
    operator: "Equal"
    value: "postgres-operator"
    effect: NoSchedule
  nodeAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 1
      preference:
        matchExpressions:
        - key: "app.stateful/component"
          operator: In
          values:
          - "postgres-operator"
  resources:
    requests:
      cpu: "1"
      memory: 4Gi
    limits:
      cpu: "2"
      memory: 4Gi
  sidecars:
    - name: exporter
      image: quay.io/prometheuscommunity/postgres-exporter:v0.14.0
      args:
      - --collector.stat_statements
      ports:
      - name: exporter
        containerPort: 9187
        protocol: TCP
      resources:
        limits:
          cpu: 500m
          memory: 256M
        requests:
          cpu: 100m
          memory: 256M
      env:
      - name: "DATA_SOURCE_URI"
        value: "localhost/postgres?sslmode=require"
      - name: "DATA_SOURCE_USER"
        value: "$(POSTGRES_USER)"
      - name: "DATA_SOURCE_PASS"
        value: "$(POSTGRES_PASSWORD)"

Este manifesto tem os seguintes campos:

  • spec.teamId: um prefixo para os objetos de cluster que escolher
  • spec.numberOfInstances: o número total de instâncias para um cluster
  • spec.users: a lista de utilizadores com privilégios
  • spec.databases: a lista de bases de dados no formato dbname: ownername
  • spec.postgresql: parâmetros postgres
  • spec.volume: parâmetros do disco persistente
  • spec.tolerations: o modelo de pod de tolerâncias que permite agendar pods de cluster em nós pool-postgres
  • spec.nodeAffinity: o modelo de agrupamento nodeAffinity que indica ao GKE que os agrupamentos preferem ser agendados em nós pool-postgres.
  • spec.resources: pedidos e limites para agrupamentos de pods
  • spec.sidecars: uma lista de contentores auxiliares, que contém postgres-exporter

Para mais informações, consulte a referência do manifesto do cluster na documentação do Postgres.

Crie um cluster Postgres básico

  1. Crie um novo cluster do Postgres com a configuração básica:

    kubectl apply -n postgres -f manifests/01-basic-cluster/my-cluster.yaml
    

    Este comando cria um recurso personalizado do PostgreSQL do operador Zalando com:

    • Pedidos e limites de CPU e memória
    • Taints e afinidades para distribuir as réplicas de pods aprovisionadas pelos nós do GKE.
    • Uma base de dados
    • Dois utilizadores com autorizações de proprietário da base de dados
    • Um utilizador sem autorizações
  2. Aguarde que o GKE inicie as cargas de trabalho necessárias:

    kubectl wait pods -l cluster-name=my-cluster  --for condition=Ready --timeout=300s -n postgres
    

    Este comando pode demorar alguns minutos a ser concluído.

  3. Verifique se o GKE criou as cargas de trabalho do Postgres:

    kubectl get pod,svc,statefulset,deploy,pdb,secret -n postgres
    

    O resultado é semelhante ao seguinte:

    NAME                                    READY   STATUS  RESTARTS   AGE
    pod/my-cluster-0                        1/1     Running   0         6m41s
    pod/my-cluster-1                        1/1     Running   0         5m56s
    pod/my-cluster-2                        1/1     Running   0         5m16s
    pod/postgres-operator-db9667d4d-rgcs8   1/1     Running   0         12m
    
    NAME                        TYPE        CLUSTER-IP  EXTERNAL-IP   PORT(S)   AGE
    service/my-cluster          ClusterIP   10.52.12.109   <none>       5432/TCP   6m43s
    service/my-cluster-config   ClusterIP   None        <none>      <none>  5m55s
    service/my-cluster-repl     ClusterIP   10.52.6.152 <none>      5432/TCP   6m43s
    service/postgres-operator   ClusterIP   10.52.8.176 <none>      8080/TCP   12m
    
    NAME                        READY   AGE
    statefulset.apps/my-cluster   3/3   6m43s
    
    NAME                                READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/postgres-operator   1/1     1           1           12m
    
    NAME                                                MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
    poddisruptionbudget.policy/postgres-my-cluster-pdb   1              N/A             0                   6m44s
    
    NAME                                                            TYPE                DATA   AGE
    secret/my-user.my-cluster.credentials.postgresql.acid.zalan.do  Opaque              2   6m45s
    secret/postgres.my-cluster.credentials.postgresql.acid.zalan.do   Opaque            2   6m44s
    secret/sh.helm.release.v1.postgres-operator.v1                  helm.sh/release.v1   1      12m
    secret/standby.my-cluster.credentials.postgresql.acid.zalan.do  Opaque              2   6m44s
    secret/zalando.my-cluster.credentials.postgresql.acid.zalan.do  Opaque              2   6m44s
    

O operador cria os seguintes recursos:

  • Um StatefulSet do Postgres, que controla três réplicas de pods para o Postgres
  • Um PodDisruptionBudgets, que garante um mínimo de uma réplica disponível
  • O my-cluster serviço, que segmenta apenas a réplica principal
  • O serviço my-cluster-repl, que expõe a porta do Postgres para ligações recebidas e para replicação entre réplicas do Postgres
  • O serviço sem interface gráfica my-cluster-config para obter a lista de réplicas de pods do Postgres em execução
  • Informações secretas com credenciais de utilizador para aceder à base de dados e à replicação entre nós do Postgres

Autentique-se no Postgres

Pode criar utilizadores do Postgres e atribuir-lhes autorizações de base de dados. Por exemplo, o seguinte manifesto descreve um recurso personalizado que atribui utilizadores e funções:

apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: my-cluster
spec:
  ...
  users:
    mydatabaseowner:
    - superuser
    - createdb
    myuser: []
  databases:
    mydatabase: mydatabaseowner

Neste manifesto:

  • O utilizador mydatabaseowner tem as funções SUPERUSER e CREATEDB, que permitem direitos de administrador completos (ou seja, gerir a configuração do Postgres, criar novas bases de dados, tabelas e utilizadores). Não deve partilhar este utilizador com os clientes. Por exemplo, o Cloud SQL não permite que os clientes tenham acesso a utilizadores com a função SUPERUSER.
  • O utilizador myuser não tem funções atribuídas. Isto segue a prática recomendada de usar o SUPERUSER para criar utilizadores com o mínimo de privilégios. Os direitos detalhados são concedidos a myuser por mydatabaseowner. Para manter a segurança, só deve partilhar credenciais myuser com aplicações cliente.

Armazene palavras-passe

Deve usar o scram-sha-256 método recomendado para armazenar palavras-passe. Por exemplo, o seguinte manifesto descreve um recurso personalizado que especifica a encriptação scram-sha-256 através do campo postgresql.parameters.password_encryption:

apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: my-cluster
spec:
  ...
  postgresql:
    parameters:
      password_encryption: scram-sha-256

Rode as credenciais de utilizador

Pode rodar as credenciais do utilizador armazenadas em segredos do Kubernetes com o Zalando. Por exemplo, o seguinte manifesto descreve um recurso personalizado que define a rotação das credenciais do utilizador através do campo usersWithSecretRotation:

apiVersion: "acid.zalan.do/v1"
kind: postgresql
metadata:
  name: my-cluster
spec:
  ...
  usersWithSecretRotation:
  - myuser
  - myanotheruser
  - ...

Exemplo de autenticação: ligue-se ao Postgres

Esta secção mostra-lhe como implementar um exemplo de cliente Postgres e estabelecer ligação à base de dados através da palavra-passe de um segredo do Kubernetes.

  1. Execute o pod cliente para interagir com o cluster do Postgres:

    kubectl apply -n postgres -f manifests/02-auth/client-pod.yaml
    

    As credenciais dos utilizadores myuser e mydatabaseowner são retiradas dos segredos relacionados e montadas como variáveis de ambiente no pod.

  2. Ligue-se ao Pod quando estiver pronto:

    kubectl wait pod postgres-client --for=condition=Ready --timeout=300s -n postgres
    kubectl exec -it postgres-client -n postgres -- /bin/bash
    
  3. Associe-se ao Postgres e tente criar uma nova tabela com as seguintes myuser credenciais:

    PGPASSWORD=$CLIENTPASSWORD psql \
      -h my-cluster \
      -U $CLIENTUSERNAME \
      -d mydatabase \
      -c "CREATE TABLE test (id serial PRIMARY KEY, randomdata VARCHAR ( 50 ) NOT NULL);"
    

    O comando deve falhar com um erro semelhante ao seguinte:

    ERROR:  permission denied for schema public
    LINE 1: CREATE TABLE test (id serial PRIMARY KEY, randomdata VARCHAR...
    

    O comando falha porque os utilizadores sem privilégios atribuídos por predefinição só podem iniciar sessão no Postgres e listar bases de dados.

  4. Crie uma tabela com as credenciais mydatabaseowner e conceda todos os privilégios na tabela a myuser:

    PGPASSWORD=$OWNERPASSWORD psql \
      -h my-cluster \
      -U $OWNERUSERNAME \
      -d mydatabase \
      -c "CREATE TABLE test (id serial PRIMARY KEY, randomdata VARCHAR ( 50 ) NOT NULL);GRANT ALL ON test TO myuser;GRANT ALL ON SEQUENCE test_id_seq TO myuser;"
    

    O resultado é semelhante ao seguinte:

    CREATE TABLE
    GRANT
    GRANT
    
  5. Insira dados aleatórios na tabela com as credenciais myuser:

    for i in {1..10}; do
      DATA=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13)
      PGPASSWORD=$CLIENTPASSWORD psql \
      -h my-cluster \
      -U $CLIENTUSERNAME \
      -d mydatabase \
      -c "INSERT INTO test(randomdata) VALUES ('$DATA');"
    done
    

    O resultado é semelhante ao seguinte:

    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    INSERT 0 1
    
  6. Obtenha os valores que inseriu:

    PGPASSWORD=$CLIENTPASSWORD psql \
      -h my-cluster \
      -U $CLIENTUSERNAME \
      -d mydatabase \
      -c "SELECT * FROM test;"
    

    O resultado é semelhante ao seguinte:

    id |  randomdata
    ----+---------------
      1 | jup9HYsAjwtW4
      2 | 9rLAyBlcpLgNT
      3 | wcXSqxb5Yz75g
      4 | KoDRSrx3muD6T
      5 | b9atC7RPai7En
      6 | 20d7kC8E6Vt1V
      7 | GmgNxaWbkevGq
      8 | BkTwFWH6hWC7r
      9 | nkLXHclkaqkqy
     10 | HEebZ9Lp71Nm3
    (10 rows)
    
  7. Saia da shell do Pod:

    exit
    

Compreenda como o Prometheus recolhe métricas para o seu cluster do Postgres

O diagrama seguinte mostra como funciona a recolha de métricas do Prometheus:

No diagrama, um cluster privado do GKE contém:

  • Um pod do Postgres que recolhe métricas no caminho / e na porta 9187
  • Recolhedores baseados no Prometheus que processam as métricas do pod do Postgres
  • Um recurso PodMonitoring que envia métricas para o Cloud Monitoring

O Google Cloud Managed Service for Prometheus suporta a recolha de métricas no formato Prometheus. O Cloud Monitoring usa um painel de controlo integrado para métricas do Postgres.

A Zalando expõe métricas de cluster no formato Prometheus através do componente postgres_exporter como um contentor sidecar.

  1. Crie o recurso PodMonitoring para extrair métricas por labelSelector:

    kubectl apply -n postgres -f manifests/03-prometheus-metrics/pod-monitoring.yaml
    
  2. Na Google Cloud consola, aceda à página Painel de controlo de clusters do GKE.

    Aceda ao painel de controlo de clusters do GKE

    O painel de controlo mostra uma taxa de carregamento de métricas diferente de zero.

  3. Na Google Cloud consola, aceda à página Painéis de controlo.

    Aceder a Painéis de controlo

  4. Abra o painel de controlo de vista geral do PostgreSQL Prometheus. O painel de controlo mostra o número de linhas obtidas. O aprovisionamento automático do painel de controlo pode demorar vários minutos.

  5. Ligue-se ao pod do cliente:

    kubectl exec -it postgres-client -n postgres -- /bin/bash
    
  6. Inserir dados aleatórios:

    for i in {1..100}; do
      DATA=$(tr -dc A-Za-z0-9 </dev/urandom | head -c 13)
      PGPASSWORD=$CLIENTPASSWORD psql \
      -h my-cluster \
      -U $CLIENTUSERNAME \
      -d mydatabase \
      -c "INSERT INTO test(randomdata) VALUES ('$DATA');"
    done
    
  7. Atualize a página. Os gráficos Linhas e Blocos são atualizados para mostrar o estado real da base de dados.

  8. Saia da shell do Pod:

    exit