Um operator Kubernetes (k8s) é uma extensão do Kubernetes que automatiza e gerencia recursos personalizados do cluster. Ele é composto pelos seguintes recursos:
Então, um operador Kubernetes é composto principalmente por um CRD, CRs, um controlador e RBAC para permitir que o controlador gerencie os recursos personalizados no cluster.
Para criar e implantar um operador Kubernetes, siga estas etapas:
Os operadores Kubernetes são ferramentas poderosas para estender a funcionalidade do Kubernetes e automatizar o gerenciamento de recursos e aplicativos complexos. Ao seguir estas etapas, você pode desenvolver e implantar com sucesso um operador Kubernetes para atender às suas necessidades específicas.
Criar um operador Kubernetes completo exige bastante código e planejamento. No entanto, posso te mostrar um exemplo simples de um operador em Go usando a biblioteca "Operator SDK" da Red Hat. Para simplificar, vamos criar um operador que gerencia um recurso personalizado chamado "HelloWorld".
Primeiro, certifique-se de que você possui o Operator SDK instalado. Siga as instruções de instalação em: https://sdk.operatorframework.io/docs/installation/
Agora, crie um novo projeto de operador:
operator-sdk init --domain example.com --repo github.com/your-username/helloworld-operator
Em seguida, crie um novo API e controlador para o recurso personalizado HelloWorld:
cd helloworld-operator
operator-sdk create api --group hello --version v1 --kind HelloWorld --resource --controller
Isso criará os arquivos básicos para nosso CRD e controlador.
Abra o arquivo api/v1/helloworld_types.go e defina a estrutura HelloWorld:
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// HelloWorldSpec define o estado desejado de HelloWorld
type HelloWorldSpec struct {
Message string `json:"message,omitempty"`
}
// HelloWorldStatus define o estado observado de HelloWorld
type HelloWorldStatus struct {
// INSERIR CAMPO DE STATUS ADICIONAL - definir o estado observado do cluster
// Importante: execute "make" para regenerar o código após modificar este arquivo
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// HelloWorld é o esquema para a API helloworlds
type HelloWorld struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec HelloWorldSpec `json:"spec,omitempty"`
Status HelloWorldStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// HelloWorldList contém uma lista de HelloWorld
type HelloWorldList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []HelloWorld `json:"items"`
}
Execute make generate e make manifests para gerar os arquivos CRD e DeepCopy.
Agora, vamos adicionar a lógica do controlador. Abra o arquivo controllers/helloworld_controller.go e adicione o seguinte código no método Reconcile:
package controllers
import (
"context"
"fmt"
"github.com/go-logr/logr"
hellov1 "github.com/your-username/helloworld-operator/api/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// HelloWorldReconciler reconcilia um objeto HelloWorld
type HelloWorldReconciler struct {
client.Client
Log logr.Logger
}
//+kubebuilder:rbac:groups=hello.example.com,resources=helloworlds,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=hello.example.com,resources=helloworlds/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=hello.example.com,resources=helloworlds/finalizers,verbs=update
func (r *HelloWorldReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("helloworld", req.NamespacedName)
//Obtenha o recurso HelloWorld
var hw hellov1.HelloWorld
if err := r.Get(ctx, req.NamespacedName, &hw); err != nil {
log.Error(err, "unable to fetch HelloWorld")
return ctrl.Result{}, client.IgnoreNotFound(err) }
// Imprimir mensagem HelloWorld
fmt.Printf("HelloWorld message: %s\n", hw.Spec.Message)
// Atualize o status HelloWorld
hw.Status.Message = fmt.Sprintf("HelloWorld message: %s", hw.Spec.Message)
if err := r.Status().Update(ctx, &hw); err != nil {
log.Error(err, "unable to update HelloWorld status")
return ctrl.Result{}, err}return ctrl.Result{}, nil
// SetupWithManager configura o controlador com o Manager.
func (r *HelloWorldReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr)
For(&hellov1.HelloWorld{})
Complete(r) }}
Aqui, nós modificamos o método `Reconcile` para buscar o recurso HelloWorld, imprimir a mensagem especificada e atualizar o status do recurso HelloWorld com a mensagem.
Agora, compile o operador e gere o arquivo de imagem do Docker:
make docker-build docker-push IMG=<your-docker-image>
Substitua <your-docker-image> pelo caminho do seu repositório de imagens Docker.
Implante o operador no seu cluster Kubernetes:
make deploy IMG=<your-docker-image>
Crie um exemplo de recurso HelloWorld:
apiVersion: hello.example.com/v1
kind: HelloWorld
metadata:
name: helloworld-sample
spec:
message: "Olá, mundo!"
Salve-o como helloworld-sample.yaml e aplique no cluster:
kubectl apply -f helloworld-sample.yaml
Agora você deve ver o operador imprimir a mensagem "Hello, Word!" no log do pod do controlador. Você também pode verificar o status do recurso HelloWorld:
kubectl get helloworlds.hello.example.com helloworld-sample -o jsonpath='{.status.message}'
Este exemplo é bastante simples e apenas ilustra a estrutura básica de um operador em Go. Um operador real pode envolver a criação e gerenciamento de outros recursos do Kubernetes, como Deployments e Services, para automatizar o gerenciamento de aplicativos complexos.
Para expandir o exemplo anterior, você pode começar a explorar o gerenciamento de recursos adicionais do Kubernetes, como Deployments e Services, dentro do seu operador HelloWorld. Vamos adicionar a criação de um Deployment e um Service sempre que um novo recurso HelloWorld for criado ou atualizado.
Primeiro, adicione as permissões necessárias no arquivo config/rbac/role.yaml. Adicione o seguinte:
- apiGroups:
- apps
resources:
- deployments
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
Agora, vamos adicionar a lógica de criação e atualização de Deployment e Service no controlador. Modifique o arquivo controllers/helloworld_controller.go:
import (
// ...
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
// ...
func (r *HelloWorldReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// ...
// Verifique se o Deployment já existe, caso contrário crie um novo
foundDeployment := &appsv1.Deployment{}
err := r.Get(ctx, types.NamespacedName{Name: hw.Name, Namespace: hw.Namespace}, foundDeployment)
if err != nil && errors.IsNotFound(err) {
// Definir uma nova implantação
dep := r.deploymentForHelloWorld(&hw)
log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
err = r.Create(ctx, dep)
if err != nil {
log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
return ctrl.Result{}, err
}
} else if err != nil {
log.Error(err, "Failed to get Deployment")
return ctrl.Result{}, err
}
// Verifique se o serviço já existe, caso contrário crie um novo
foundService := &corev1.Service{}
err = r.Get(ctx, types.NamespacedName{Name: hw.Name, Namespace: hw.Namespace}, foundService)
if err != nil && errors.IsNotFound(err) {
// Definir um novo serviço
svc := r.serviceForHelloWorld(&hw)
log.Info("Creating a new Service", "Service.Namespace", svc.Namespace, "Service.Name", svc.Name)
err = r.Create(ctx, svc)
if err != nil {
log.Error(err, "Failed to create new Service", "Service.Namespace", svc.Namespace, "Service.Name", svc.Name)
return ctrl.Result{}, err
}
} else if err != nil {
log.Error(err, "Failed to get Service")
return ctrl.Result{}, err
}
// ...
return ctrl.Result{}, nil
}
func (r *HelloWorldReconciler) deploymentForHelloWorld(hw *hellov1.HelloWorld) *appsv1.Deployment {
// Defina o Deployment desejado para HelloWorld
}
func (r *HelloWorldReconciler) serviceForHelloWorld(hw *hellov1.HelloWorld) *corev1.Service {
// Defina o Serviço desejado para HelloWorld }
Agora, implemente as funções `deploymentForHelloWorld` e `serviceForHelloWorld` para criar os recursos necessários:
func (r *HelloWorldReconciler) deploymentForHelloWorld(hw *hellov1.HelloWorld) *appsv1.Deployment {
labels := map[string]string{
"app": hw.Name,
}
replicas := int32(1)
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: hw.Name,
Namespace: hw.Namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: labels,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "hello-world",
Image: "busybox",
Args: []string{
"/bin/sh",
"-c",
fmt.Sprintf("echo '%s' && sleep 3600", hw.Spec.Message),
},
}},
},
},
},
}
ctrl.SetControllerReference(hw, dep, r.Scheme)
return dep
}
func (r *HelloWorldReconciler) serviceForHelloWorld(hw *hellov1.HelloWorld) *corev1.Service {
labels := map[string]string{
"app": hw.Name,
}
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: hw.Name,
Namespace: hw.Namespace,
},
Spec: corev1.ServiceSpec{
Selector: labels,
Ports: []corev1.ServicePort{{
Port: 80,
TargetPort: intstr.FromInt(8080),
}},
Type: corev1.ServiceTypeClusterIP,
},
}
ctrl.SetControllerReference(hw, svc, r.Scheme)
return svc
}
A função deploymentForHelloWorld cria um Deployment que executa um contêiner BusyBox e imprime a mensagem especificada no recurso HelloWorld. A função serviceForHelloWorld cria um Service do tipo ClusterIP para expor o contêiner no cluster.
Recompile o operador, gere a imagem do Docker e implante o operador novamente, conforme descrito anteriormente.
Agora, quando você criar ou atualizar um recurso HelloWorld, o operador criará ou atualizará automaticamente um Deployment e um Service associados.
Lembre-se de que este é apenas um exemplo simples para ilustrar como um operador Kubernetes pode gerenciar recursos adicionais. Operators reais podem gerenciar muitos recursos diferentes e aplicar lógica complexa para gerenciar o estado desejado dos aplicativos.