Docker & Kubernetes

Boas Práticas de Operators em Kubernetes: Construindo Controladores Customizados para Times Ágeis

17 min de leitura

Boas Práticas de Operators em Kubernetes: Construindo Controladores Customizados para Times Ágeis

Introdução: O que são Operators em Kubernetes Um Kubernetes Operator é um padrão de design que encapsula conhecimento operacional complexo em código executável. Ele estende as capacidades nativas do Kubernetes através de Custom Resources (CRs) e controladores customizados, permitindo automatizar tarefas repetitivas e complexas de gerenciamento de aplicações stateful. Enquanto recursos nativos como Deployments e StatefulSets lidam bem com aplicações stateless ou com estado simples, aplicações complexas como bancos de dados, sistemas de mensageria ou plataformas de análise exigem lógica sofisticada: provisioning, backup, failover, scaling inteligente e updates coordenados. Um Operator codifica essa expertise, tornando-a reutilizável e escalável. Fundamentos: Custom Resources e CRDs O que é uma Custom Resource Definition Uma Custom Resource Definition (CRD) é um mecanismo que permite estender a API do Kubernetes com novos tipos de recursos. Sem uma CRD, você só pode trabalhar com resources nativos (Pods, Services, etc.). Com uma CRD, você define a estrutura e validação de seus próprios recursos, criando uma abstraçãohigher-level para

<h2>Introdução: O que são Operators em Kubernetes</h2>

<p>Um Kubernetes Operator é um padrão de design que encapsula conhecimento operacional complexo em código executável. Ele estende as capacidades nativas do Kubernetes através de Custom Resources (CRs) e controladores customizados, permitindo automatizar tarefas repetitivas e complexas de gerenciamento de aplicações stateful.</p>

<p>Enquanto recursos nativos como Deployments e StatefulSets lidam bem com aplicações stateless ou com estado simples, aplicações complexas como bancos de dados, sistemas de mensageria ou plataformas de análise exigem lógica sofisticada: provisioning, backup, failover, scaling inteligente e updates coordenados. Um Operator codifica essa expertise, tornando-a reutilizável e escalável.</p>

<h2>Fundamentos: Custom Resources e CRDs</h2>

<h3>O que é uma Custom Resource Definition</h3>

<p>Uma Custom Resource Definition (CRD) é um mecanismo que permite estender a API do Kubernetes com novos tipos de recursos. Sem uma CRD, você só pode trabalhar com resources nativos (Pods, Services, etc.). Com uma CRD, você define a estrutura e validação de seus próprios recursos, criando uma abstraçãohigher-level para suas aplicações.</p>

<p>Vamos criar uma CRD simples para gerenciar uma aplicação de banco de dados PostgreSQL customizado:</p>

<pre><code class="language-yaml">apiVersion: apiextensions.k8s.io/v1

kind: CustomResourceDefinition

metadata:

name: postgresinstances.database.example.com

spec:

group: database.example.com

names:

kind: PostgresInstance

plural: postgresinstances

shortNames:

  • pg

scope: Namespaced

versions:

  • name: v1

served: true

storage: true

schema:

openAPIV3Schema:

type: object

properties:

spec:

type: object

properties:

version:

type: string

description: &quot;Versão do PostgreSQL&quot;

example: &quot;14.5&quot;

replicas:

type: integer

minimum: 1

maximum: 10

description: &quot;Número de replicas&quot;

storage:

type: string

description: &quot;Tamanho do volume de armazenamento&quot;

example: &quot;10Gi&quot;

required:

  • version
  • replicas
  • storage

status:

type: object

properties:

ready:

type: boolean

primaryPod:

type: string

replicaPods:

type: array

items:

type: string</code></pre>

<p>Essa CRD define um novo tipo de recurso chamado <code>PostgresInstance</code>. Agora você pode criar instâncias desse tipo:</p>

<pre><code class="language-yaml">apiVersion: database.example.com/v1

kind: PostgresInstance

metadata:

name: production-db

namespace: default

spec:

version: &quot;14.5&quot;

replicas: 3

storage: &quot;100Gi&quot;</code></pre>

<h3>A estrutura Spec e Status</h3>

<p>A separação entre <code>spec</code> (desejado) e <code>status</code> (observado) é fundamental em Kubernetes. O <code>spec</code> descreve o estado que você quer, enquanto o <code>status</code> reflete o estado atual do recurso. Seu Operator constantemente observa a diferença e age para reconciliar.</p>

<h2>Implementação: Construindo um Controlador com Go</h2>

<h3>Configuração do Projeto</h3>

<p>Para construir Operators em produção, usaremos o Operator SDK, que fornece scaffolding automático e bibliotecas essenciais. Aqui assumo que você já tem Go 1.20+ e kubebuilder/operator-sdk instalados.</p>

<pre><code class="language-bash">operator-sdk init --domain example.com --repo github.com/example/postgres-operator

operator-sdk create api --group database --version v1 --kind PostgresInstance --resource --controller</code></pre>

<p>Esses comandos criam a estrutura básica do projeto. Você encontrará:</p>

<ul>

<li><code>api/v1/postgresinstance_types.go</code> — definição da CRD em Go</li>

<li><code>controllers/postgresinstance_controller.go</code> — lógica do controlador</li>

<li><code>config/crd/</code> — manifestos YAML das CRDs</li>

</ul>

<h3>Definindo a CRD em Go</h3>

<p>O arquivo <code>api/v1/postgresinstance_types.go</code> define a estrutura Go que representa seu recurso:</p>

<pre><code class="language-go">package v1

import (

metav1 &quot;k8s.io/apimachinery/pkg/apis/meta/v1&quot;

)

// PostgresInstanceSpec define as propriedades desejadas

type PostgresInstanceSpec struct {

Version string json:&quot;version,omitempty&quot;

Replicas int32 json:&quot;replicas,omitempty&quot;

Storage string json:&quot;storage,omitempty&quot;

}

// PostgresInstanceStatus define o estado observado

type PostgresInstanceStatus struct {

Ready bool json:&quot;ready,omitempty&quot;

PrimaryPod string json:&quot;primaryPod,omitempty&quot;

ReplicaPods []string json:&quot;replicaPods,omitempty&quot;

ObservedGeneration int64 json:&quot;observedGeneration,omitempty&quot;

}

// +kubebuilder:object:root=true

// +kubebuilder:subresource:status

// +kubebuilder:printcolumn:name=&quot;Ready&quot;,type=boolean,JSONPath=.status.ready

// +kubebuilder:printcolumn:name=&quot;Age&quot;,type=date,JSONPath=.metadata.creationTimestamp

type PostgresInstance struct {

metav1.TypeMeta json:&quot;,inline&quot;

metav1.ObjectMeta json:&quot;metadata,omitempty&quot;

Spec PostgresInstanceSpec json:&quot;spec,omitempty&quot;

Status PostgresInstanceStatus json:&quot;status,omitempty&quot;

}

// +kubebuilder:object:root=true

type PostgresInstanceList struct {

metav1.TypeMeta json:&quot;,inline&quot;

metav1.ListMeta json:&quot;metadata,omitempty&quot;

Items []PostgresInstance json:&quot;items&quot;

}

func init() {

SchemeBuilder.Register(&amp;PostgresInstance{}, &amp;PostgresInstanceList{})

}</code></pre>

<p>Os comentários com <code>+kubebuilder</code> são marcadores que geram código e manifests automaticamente durante a build.</p>

<h3>Implementando a Lógica do Controlador</h3>

<p>O controlador é o coração do Operator. Ele observa recursos <code>PostgresInstance</code> e realiza ações (criar Pods, StatefulSets, Services, etc.). Aqui está uma implementação funcional simplificada:</p>

<pre><code class="language-go">package controllers

import (

&quot;context&quot;

&quot;fmt&quot;

appsv1 &quot;k8s.io/api/apps/v1&quot;

corev1 &quot;k8s.io/api/core/v1&quot;

apierrors &quot;k8s.io/apimachinery/pkg/api/errors&quot;

metav1 &quot;k8s.io/apimachinery/pkg/apis/meta/v1&quot;

&quot;k8s.io/apimachinery/pkg/runtime&quot;

&quot;k8s.io/apimachinery/pkg/types&quot;

ctrl &quot;sigs.k8s.io/controller-runtime&quot;

&quot;sigs.k8s.io/controller-runtime/pkg/client&quot;

&quot;sigs.k8s.io/controller-runtime/pkg/controller/controllerutil&quot;

&quot;sigs.k8s.io/controller-runtime/pkg/predicate&quot;

databasev1 &quot;github.com/example/postgres-operator/api/v1&quot;

)

const finalizerName = &quot;database.example.com/postgres-finalizer&quot;

// PostgresInstanceReconciler reconcilia objetos PostgresInstance

type PostgresInstanceReconciler struct {

client.Client

Scheme *runtime.Scheme

}

//+kubebuilder:rbac:groups=database.example.com,resources=postgresinstances,verbs=get;list;watch;create;update;patch;delete

//+kubebuilder:rbac:groups=database.example.com,resources=postgresinstances/status,verbs=get;update;patch

//+kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete

//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete

// Reconcile é chamado sempre que há mudanças no recurso observado

func (r *PostgresInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

log := ctrl.LoggerFrom(ctx)

// Busca a instância PostgreSQL

pg := &amp;databasev1.PostgresInstance{}

if err := r.Get(ctx, req.NamespacedName, pg); err != nil {

if apierrors.IsNotFound(err) {

log.Info(&quot;PostgresInstance não encontrada, ignorando&quot;)

return ctrl.Result{}, nil

}

log.Error(err, &quot;Erro ao buscar PostgresInstance&quot;)

return ctrl.Result{}, err

}

// Handle deletion with finalizer

if pg.ObjectMeta.DeletionTimestamp != nil {

if controllerutil.ContainsFinalizer(pg, finalizerName) {

// Aqui você faria lógica de limpeza (backup, etc)

log.Info(&quot;Limpando recursos do PostgresInstance&quot;, &quot;name&quot;, pg.Name)

controllerutil.RemoveFinalizer(pg, finalizerName)

if err := r.Update(ctx, pg); err != nil {

return ctrl.Result{}, err

}

}

return ctrl.Result{}, nil

}

// Add finalizer se não existe

if !controllerutil.ContainsFinalizer(pg, finalizerName) {

controllerutil.AddFinalizer(pg, finalizerName)

if err := r.Update(ctx, pg); err != nil {

return ctrl.Result{}, err

}

}

// Cria ou atualiza o StatefulSet para o PostgreSQL

sts := &amp;appsv1.StatefulSet{}

stsName := types.NamespacedName{Name: pg.Name, Namespace: pg.Namespace}

if err := r.Get(ctx, stsName, sts); err != nil {

if apierrors.IsNotFound(err) {

log.Info(&quot;Criando novo StatefulSet&quot;)

sts = r.constructStatefulSet(pg)

if err := controllerutil.SetControllerReference(pg, sts, r.Scheme); err != nil {

return ctrl.Result{}, err

}

if err := r.Create(ctx, sts); err != nil {

log.Error(err, &quot;Erro ao criar StatefulSet&quot;)

return ctrl.Result{}, err

}

} else {

log.Error(err, &quot;Erro ao buscar StatefulSet&quot;)

return ctrl.Result{}, err

}

} else {

// StatefulSet existe, atualiza se necessário

sts.Spec.Replicas = &amp;pg.Spec.Replicas

if err := r.Update(ctx, sts); err != nil {

log.Error(err, &quot;Erro ao atualizar StatefulSet&quot;)

return ctrl.Result{}, err

}

}

// Cria ou atualiza o Service

svc := &amp;corev1.Service{}

svcName := types.NamespacedName{Name: pg.Name, Namespace: pg.Namespace}

if err := r.Get(ctx, svcName, svc); err != nil {

if apierrors.IsNotFound(err) {

log.Info(&quot;Criando novo Service&quot;)

svc = r.constructService(pg)

if err := controllerutil.SetControllerReference(pg, svc, r.Scheme); err != nil {

return ctrl.Result{}, err

}

if err := r.Create(ctx, svc); err != nil {

log.Error(err, &quot;Erro ao criar Service&quot;)

return ctrl.Result{}, err

}

} else {

log.Error(err, &quot;Erro ao buscar Service&quot;)

return ctrl.Result{}, err

}

}

// Atualiza o status

pg.Status.Ready = true

pg.Status.PrimaryPod = fmt.Sprintf(&quot;%s-0&quot;, pg.Name)

pg.Status.ReplicaPods = make([]string, 0)

for i := int32(1); i &lt; pg.Spec.Replicas; i++ {

pg.Status.ReplicaPods = append(pg.Status.ReplicaPods, fmt.Sprintf(&quot;%s-%d&quot;, pg.Name, i))

}

pg.Status.ObservedGeneration = pg.Generation

if err := r.Status().Update(ctx, pg); err != nil {

log.Error(err, &quot;Erro ao atualizar status&quot;)

return ctrl.Result{}, err

}

log.Info(&quot;PostgresInstance reconciliado com sucesso&quot;, &quot;name&quot;, pg.Name)

return ctrl.Result{}, nil

}

// constructStatefulSet cria o StatefulSet para o PostgreSQL

func (r PostgresInstanceReconciler) constructStatefulSet(pg databasev1.PostgresInstance) *appsv1.StatefulSet {

labels := map[string]string{

&quot;app&quot;: &quot;postgres&quot;,

&quot;instance&quot;: pg.Name,

}

sts := &amp;appsv1.StatefulSet{

ObjectMeta: metav1.ObjectMeta{

Name: pg.Name,

Namespace: pg.Namespace,

Labels: labels,

},

Spec: appsv1.StatefulSetSpec{

ServiceName: pg.Name,

Replicas: &amp;pg.Spec.Replicas,

Selector: &amp;metav1.LabelSelector{

MatchLabels: labels,

},

Template: corev1.PodTemplateSpec{

ObjectMeta: metav1.ObjectMeta{

Labels: labels,

},

Spec: corev1.PodSpec{

Containers: []corev1.Container{

{

Name: &quot;postgres&quot;,

Image: fmt.Sprintf(&quot;postgres:%s&quot;, pg.Spec.Version),

Ports: []corev1.ContainerPort{

{

Name: &quot;postgres&quot;,

ContainerPort: 5432,

Protocol: corev1.ProtocolTCP,

},

},

Env: []corev1.EnvVar{

{

Name: &quot;POSTGRES_PASSWORD&quot;,

Value: &quot;changeme&quot;,

},

},

VolumeMounts: []corev1.VolumeMount{

{

Name: &quot;data&quot;,

MountPath: &quot;/var/lib/postgresql/data&quot;,

},

},

},

},

},

},

VolumeClaimTemplates: []corev1.PersistentVolumeClaim{

{

ObjectMeta: metav1.ObjectMeta{

Name: &quot;data&quot;,

},

Spec: corev1.PersistentVolumeClaimSpec{

AccessModes: []corev1.PersistentVolumeAccessMode{

corev1.ReadWriteOnce,

},

Resources: corev1.ResourceRequirements{

Requests: corev1.ResourceList{

corev1.ResourceStorage: *parseQuantity(pg.Spec.Storage),

},

},

},

},

},

},

}

return sts

}

// constructService cria o Service para o PostgreSQL

func (r PostgresInstanceReconciler) constructService(pg databasev1.PostgresInstance) *corev1.Service {

labels := map[string]string{

&quot;app&quot;: &quot;postgres&quot;,

&quot;instance&quot;: pg.Name,

}

svc := &amp;corev1.Service{

ObjectMeta: metav1.ObjectMeta{

Name: pg.Name,

Namespace: pg.Namespace,

Labels: labels,

},

Spec: corev1.ServiceSpec{

Selector: labels,

Ports: []corev1.ServicePort{

{

Name: &quot;postgres&quot;,

Port: 5432,

TargetPort: intstr.FromString(&quot;postgres&quot;),

Protocol: corev1.ProtocolTCP,

},

},

ClusterIP: corev1.ClusterIPNone, // Headless service para StatefulSets

},

}

return svc

}

// SetupWithManager registra o controlador com o manager

func (r *PostgresInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error {

return ctrl.NewControllerManagedBy(mgr).

For(&amp;databasev1.PostgresInstance{}).

Owns(&amp;appsv1.StatefulSet{}).

Owns(&amp;corev1.Service{}).

WithEventFilter(predicate.GenerationChangedPredicate{}). // Ignora updates de status

Complete(r)

}

// Helper para parsear quantity

func parseQuantity(s string) *resource.Quantity {

q, _ := resource.ParseQuantity(s)

return &amp;q

}</code></pre>

<h2>Ciclo de Vida e Padrões Avançados</h2>

<h3>Reconciliação: O Loop Fundamental</h3>

<p>A reconciliação é o mecanismo central dos Operators. O controlador observa recursos e, quando há mudanças (criação, update, delete), o método <code>Reconcile</code> é acionado. Importante: o reconciliador deve ser idempotente — pode ser chamado múltiplas vezes e o resultado final deve ser sempre o mesmo.</p>

<p>No exemplo anterior, nossa lógica:</p>

<ol>

<li>Busca o recurso <code>PostgresInstance</code></li>

<li>Verifica se está sendo deletado (finalizers)</li>

<li>Cria ou atualiza StatefulSet e Service</li>

<li>Atualiza o status refletindo o estado real</li>

</ol>

<p>Essa lógica roda continuously. Se alguém deletar manualmente o StatefulSet, na próxima reconciliação ele será recriado automaticamente.</p>

<h3>Finalizers: Limpeza Segura</h3>

<p>Quando você deleta um recurso, Kubernetes normalmente remove-o imediatamente. Com finalizers, você pode executar lógica de limpeza antes da deletion definitiva. No exemplo acima, adicionamos um finalizer que permite fazer backup ou limpeza de dados antes de remover o PostgresInstance.</p>

<h3>Owner References: Rastreamento de Dependências</h3>

<p>Usamos <code>SetControllerReference</code> para estabelecer uma relação pai-filho entre o <code>PostgresInstance</code> (pai) e seus <code>StatefulSet</code> e <code>Service</code> (filhos). Quando o pai é deletado, os filhos são automaticamente removidos (garbage collection).</p>

<h3>Observabilidade: Logs e Métricas</h3>

<pre><code class="language-go">import (

&quot;sigs.k8s.io/controller-runtime/pkg/log&quot;

)

// No seu Reconcile:

log := log.FromContext(ctx)

log.Info(&quot;Iniciando reconciliação&quot;, &quot;instance&quot;, pg.Name, &quot;replicas&quot;, pg.Spec.Replicas)

log.Error(err, &quot;Falha ao criar StatefulSet&quot;, &quot;name&quot;, pg.Name)</code></pre>

<p>O controller-runtime integra-se com loggers estruturados (Zap, por padrão) e você consegue rastrear exatamente o que seu Operator está fazendo.</p>

<h2>Deployment e Testes</h2>

<h3>Gerando e Instalando a CRD</h3>

<p>Após implementar seu controlador, execute:</p>

<pre><code class="language-bash">make manifests</code></pre>

<p>Isso gera os YAMLs em <code>config/crd/bases/</code>. Para instalar localmente em um cluster:</p>

<pre><code class="language-bash">kubectl apply -f config/crd/bases/</code></pre>

<h3>Rodando o Operator Localmente</h3>

<p>Durante desenvolvimento, você pode rodar o controlador na sua máquina:</p>

<pre><code class="language-bash">make install run</code></pre>

<p>Isso instala a CRD e executa o Operator localmente, conectando-se ao seu cluster via kubeconfig.</p>

<h3>Buildando a Imagem Docker</h3>

<p>Para deployar em produção, você precisa criar uma imagem Docker:</p>

<pre><code class="language-dockerfile"># Dockerfile (já fornecido pelo Operator SDK)

FROM golang:1.20 as builder

WORKDIR /workspace

COPY go.mod go.mod

COPY go.sum go.sum

RUN go mod download

COPY main.go main.go

COPY api/ api/

COPY controllers/ controllers/

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags=&quot;-w -s&quot; -o manager main.go

FROM gcr.io/distroless/static:nonroot

WORKDIR /

COPY --from=builder /workspace/manager .

USER 65532:65532

ENTRYPOINT [&quot;/manager&quot;]</code></pre>

<pre><code class="language-bash">docker build -t myregistry/postgres-operator:v0.1.0 .

docker push myregistry/postgres-operator:v0.1.0</code></pre>

<h3>Testando com Envtest</h3>

<p>Para testes unitários, use o framework <code>envtest</code> que spinna um APIServer e etcd reais:</p>

<pre><code class="language-go">package controllers

import (

&quot;testing&quot;

. &quot;github.com/onsi/ginkgo/v2&quot;

. &quot;github.com/onsi/gomega&quot;

&quot;k8s.io/client-go/kubernetes/scheme&quot;

&quot;sigs.k8s.io/controller-runtime/pkg/envtest&quot;

logf &quot;sigs.k8s.io/controller-runtime/pkg/log&quot;

&quot;sigs.k8s.io/controller-runtime/pkg/log/zap&quot;

databasev1 &quot;github.com/example/postgres-operator/api/v1&quot;

)

var testEnv *envtest.Environment

func TestAPIs(t *testing.T) {

RegisterFailHandler(Fail)

RunSpecs(t, &quot;Controller Suite&quot;)

}

var _ = BeforeSuite(func() {

logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

testEnv = &amp;envtest.Environment{}

cfg, err := testEnv.Start()

Expect(err).NotTo(HaveOccurred())

err = databasev1.AddToScheme(scheme.Scheme)

Expect(err).NotTo(HaveOccurred())

})

var _ = AfterSuite(func() {

Expect(testEnv.Stop()).To(Succeed())

})</code></pre>

<h2>Conclusão</h2>

<p>Ao longo deste artigo, você aprendeu que <strong>Operators não são aplicações genéricas, mas codificações de conhecimento operacional específico</strong>. Você agora compreende como CRDs funcionam como contratos que definem a API do seu Operator, permitindo que usuários declarem intenção ao invés de descrever procedimentos.</p>

<p>O padrão de reconciliação contínua, embora possa parecer simples, é extraordinariamente poderoso: o controlador sempre busca o estado desejado (declarado na CRD) e o estado real (Pods, Services, etc) e age para reconciliá-los. Isso significa que seu Operator recupera-se automaticamente de falhas, não é necessário polling manual e a lógica é centralizada e versionável.</p>

<p>Por fim, lembre-se que <strong>a qualidade de um Operator está na robustez, observabilidade e documentação</strong>. Testes automatizados, logs estruturados, finalizers bem implementados e CRDs com validação clara transformam um código inicial em uma ferramenta confiável que outros engenheiros podem usar com segurança em seus clusters.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/" target="_blank" rel="noopener noreferrer">Kubernetes Operator Pattern - Official Documentation</a></li>

<li><a href="https://sdk.operatorframework.io/" target="_blank" rel="noopener noreferrer">Operator SDK Documentation</a></li>

<li><a href="https://book.kubebuilder.io/" target="_blank" rel="noopener noreferrer">Kubebuilder Book - Practical Guide to Building Operators</a></li>

<li><a href="https://github.com/kubernetes-sigs/controller-runtime" target="_blank" rel="noopener noreferrer">Controller Runtime - Go Client Library</a></li>

<li><a href="https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/" target="_blank" rel="noopener noreferrer">Custom Resource Definitions in Kubernetes</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Docker & Kubernetes

O que Todo Dev Deve Saber sobre Dockerfile em Profundidade: Cada Instrução e seu Impacto no Build
O que Todo Dev Deve Saber sobre Dockerfile em Profundidade: Cada Instrução e seu Impacto no Build

Introdução: Por que entender Dockerfile é fundamental Um Dockerfile é um scri...

Dominando Segurança em Docker: Rootless Containers, Seccomp e AppArmor em Projetos Reais
Dominando Segurança em Docker: Rootless Containers, Seccomp e AppArmor em Projetos Reais

Introdução: O Cenário Atual de Segurança em Containers Docker revolucionou a...

GKE no GCP: Autopilot, Workload Identity e Cloud SQL Proxy: Do Básico ao Avançado
GKE no GCP: Autopilot, Workload Identity e Cloud SQL Proxy: Do Básico ao Avançado

GKE Autopilot: Gerenciamento Automático de Clusters Kubernetes O Google Kuber...