Artigo Mikhail Khomenko · Nov. 19, 2021 15m read

Da última vez, lançamos uma aplicação IRIS no Google Cloud usando seu serviço GKE.

E, embora criar um cluster manualmente (ou por meio do gcloud) seja fácil, a abordagem de Infraestrutura como Código (IaC) moderna recomenda que a descrição do cluster Kubernetes também seja armazenada no repositório como código. Como escrever este código é determinado pela ferramenta que é usada para IaC.

No caso do Google Cloud, existem várias opções, entre elas o Deployment Manager e o Terraform. As opiniões estão divididas quanto o que é melhor: se você quiser saber mais, leia este tópico no Reddit Opiniões sobre Terraform vs. Deployment Manager? e o artigo no Medium Comparando o GCP Deployment Manager e o Terraform

Para este artigo, escolheremos o Terraform, já que ele está menos vinculado a um fornecedor específico e você pode usar seu IaC com diferentes provedores em nuvem.

Suporemos que você leu o artigo anterior e já tem uma conta do Google, e que criou um projeto chamado “Desenvolvimento”, como no artigo anterior. Neste artigo, seu ID é mostrado como . Nos exemplos abaixo, altere-o para o ID de seu próprio projeto

Lembre-se de que o Google não é gratuito, embora tenha um nível gratuito. Certifique-se de controlar suas despesas.

Também presumiremos que você já bifurcou o repositório original. Chamaremos essa bifurcação (fork) de “my-objectscript-rest-docker-template” e nos referiremos ao seu diretório raiz como"" ao longo deste artigo.

Todos os exemplos de código são armazenados neste repositório para simplificar a cópia e a colagem.

O diagrama a seguir descreve todo o processo de implantação em uma imagem:

Então, vamos instalar a versão mais recente do Terraform no momento desta postagem:

$ terraform version
Terraform v0.12.17

A versão é importante aqui, pois muitos exemplos na Internet usam versões anteriores, e a 0.12 trouxe muitas mudanças.

Queremos que o Terraform execute certas ações (use certas APIs) em nossa conta do GCP. Para ativar isso, crie uma conta de serviço com o nome 'terraform' e ative a API do Kubernetes Engine. Não se preocupe sobre como vamos conseguir isso — basta continuar lendo e suas perguntas serão respondidas.

Vamos tentar um exemplo com o utilitário gcloud, embora também possamos usar o console web.

Usaremos alguns comandos diferentes nos exemplos a seguir. Consulte os tópicos da documentação a seguir para obter mais detalhes sobre esses comandos e recursos.

  • Como criar contas de serviço IAM no gcloud
  • Como atribuir papéis a uma conta de serviço para recursos específicos
  • Como criar chaves de conta de serviço IAM no gcloud
  • Como ativar uma API no projeto do Google Cloud
  • Agora vamos analisar o exemplo.

    $ gcloud init

    Como trabalhamos com o gcloud no artigo anterior, não discutiremos todos os detalhes de configuração aqui. Para este exemplo, execute os seguintes comandos:

    $ cd
    $ mkdir terraform; cd terraform
    $ gcloud iam service-accounts create terraform --description "Terraform" --display-name "terraform"

    Agora, vamos adicionar alguns papéis à conta de serviço do terraform além de “Administrador do Kubernetes Engine” (container.admin). Essas funções serão úteis para nós no futuro.

    $ gcloud projects add-iam-policy-binding \
      --member serviceAccount:terraform@.iam.gserviceaccount.com \
      --role roles/container.admin

    $ gcloud projects add-iam-policy-binding \
      --member serviceAccount:terraform@.iam.gserviceaccount.com \
      --role roles/iam.serviceAccountUser

    $ gcloud projects add-iam-policy-binding \
      --member serviceAccount:terraform@.iam.gserviceaccount.com \
      --role roles/compute.viewer

    $ gcloud projects add-iam-policy-binding \
      --member serviceAccount:terraform@.iam.gserviceaccount.com \
      --role roles/storage.admin

    $ gcloud iam service-accounts keys create account.json \
    --iam-account terraform@.iam.gserviceaccount.com

    Observe que a última entrada cria o seu arquivo account.json. Certifique-se de manter este arquivo em segredo.

    $ gcloud projects list
    $ gcloud config set project
    $ gcloud services list --available | grep 'Kubernetes Engine'
    $ gcloud services enable container.googleapis.com
    $ gcloud services list --enabled | grep 'Kubernetes Engine'
    container.googleapis.com Kubernetes Engine API

     

    A seguir, vamos descrever o cluster GKE na linguagem HCL do Terraform. Observe que usamos vários placeholders aqui; substitua-os por seus valores:

    <td>
      Significado
    </td>
    
    <td>
      Exemplo
    </td>
    
    <td>
        ID do projeto do GCP
    </td>
    
    <td>
        possible-symbol-254507
    </td>
    
    <td>
       Armazenamento para estado/bloqueio do Terraform - deve ser único
    </td>
    
    <td>
        circleci-gke-terraform-demo
    </td>
    
    <td>
        Região onde os recursos serão criados
    </td>
    
    <td>
        europe-west1
    </td>
    
    <td>
        Zona onde os recursos serão criados
    </td>
    
    <td>
        europe-west1-b
    </td>
    
    <td>
        Nome do cluster GKE
    </td>
    
    <td>
        dev-cluster
    </td>
    
    <td>
        Nome do pool de nós de trabalho do GKE
    </td>
    
    <td>
        dev-cluster-node-pool
    </td>
    
    Placeholder
      
     
     
     
     

     

    Aqui está a configuração HCL para o cluster na prática:

    $ cat main.tf
    terraform {
      required_version = "~> 0.12"
      backend "gcs" {
        bucket = ""
        prefix = "terraform/state"
        credentials = "account.json"
      }
    }

    provider "google" {
      credentials = file("account.json")
      project = ""
      region = ""
    }

    resource "google_container_cluster" "gke-cluster" {
      name = ""
      location = ""
      remove_default_node_pool = true
      # No cluster regional (localização é região, não zona) 
      # este é um número de nós por zona 
      initial_node_count = 1
    }

    resource "google_container_node_pool" "preemptible_node_pool" {
      name = ""
      location = ""
      cluster = google_container_cluster.gke-cluster.name
      # No cluster regional (localização é região, não zona) 
      # este é um número de nós por zona
      node_count = 1

      node_config {
        preemptible = true
        machine_type = "n1-standard-1"
        oauth_scopes = [
          "storage-ro",
          "logging-write",
          "monitoring"
        ]
      }
    }

    Para garantir que o código HCL esteja no formato adequado, o Terraform fornece um comando de formatação útil que você pode usar:

    $ terraform fmt

    O fragmento de código (snippet) mostrado acima indica que os recursos criados serão fornecidos pelo Google e os próprios recursos são google_container_cluster e google_container_node_pool, que designamos como preemptivos para economia de custos. Também optamos por criar nosso próprio pool em vez de usar o padrão.

    Vamos nos concentrar brevemente na seguinte configuração:

    terraform {
      required_version = "~> 0.12"
      backend "gcs" {
        Bucket = ""
        Prefix = "terraform/state"
        credentials = "account.json"
      }
    }

    O Terraform grava tudo o que é feito no arquivo de status e usa esse arquivo para outro trabalho. Para um compartilhamento conveniente, é melhor armazenar este arquivo em algum lugar remoto. Um lugar típico é umGoogle Bucket.

    Vamos criar este bucket. Use o nome do seu bucket em vez do placeholder . Antes da criação do bucket, vamos verificar se está disponível, pois deve ser único em todo o GCP:

    $ gsutil acl get gs://

    Boa resposta:

    BucketNotFoundException: 404 gs:// bucket does not exist

    A resposta ocupado "Busy" significa que você deve escolher outro nome:

    AccessDeniedException: 403 does not have storage.buckets.get access to

    Também vamos habilitar o controle de versão, como o Terraform recomenda.

    $ gsutil mb -l EU gs://

    $ gsutil versioning get gs://
    gs://: Suspended

    $ gsutil versioning set on gs://

    $ gsutil versioning get gs://
    gs://: Enabled

    O Terraform é modular e precisa adicionar um plugin de provedor do Google para criar algo no GCP. Usamos o seguinte comando para fazer isso:

    $ terraform init

    Vejamos o que o Terraform fará para criar um cluster do GKE:

    $ terraform plan -out dev-cluster.plan

    A saída do comando inclui detalhes do plano. Se você não tem objeções, vamos implementar este plano:

    $ terraform apply dev-cluster.plan

    A propósito, para excluir os recursos criados pelo Terraform, execute este comando a partir do diretório /terraform/:

    $ terraform destroy -auto-approve

    Vamos deixar o cluster como está por um tempo e seguir em frente. Mas primeiro observe que não queremos colocar tudo no repositório, então vamos adicionar vários arquivos às exceções:

    $ cat /.gitignore
    .DS_Store
    terraform/.terraform/
    terraform/*.plan
    terraform/*.json

    Usando Helm

    No artigo anterior, armazenamos os manifestos do Kubernetes como arquivos YAML no diretório /k8s/, que enviamos ao cluster usando o comando "kubectl apply". 

    Desta vez, tentaremos uma abordagem diferente: usando o gerenciador de pacotes Helm do Kubernetes, que foi atualizado recentemente para a versão 3. Use a versão 3 ou posterior porque a versão 2 tinha problemas de segurança do lado do Kubernetes (veja Executando o Helm na produção: melhores práticas de Segurança para mais detalhes). Primeiro, empacotaremos os manifestos Kubernetes de nosso diretório k8s/ em um pacote Helm, que é conhecido como chart. Um chart Helm instalado no Kubernetes é chamado de release. Em uma configuração mínima, um chart consistirá em vários arquivos:

    $ mkdir /helm; cd /helm
    $ tree /helm/
    helm/
    ├── Chart.yaml
    ├── templates
    │   ├── deployment.yaml
    │   ├── _helpers.tpl
    │   └── service.yaml
    └── values.yaml

    Seu propósito está bem descrito no site oficial. As práticas recomendadas para criar seus próprios charts são descritas no Guia de Melhores Práticas do Chart na documentação do Helm. 

    Esta é a aparência do conteúdo de nossos arquivos:

    $ cat Chart.yaml
    apiVersion: v2
    name: iris-rest
    version: 0.1.0
    appVersion: 1.0.3
    description: Helm for ObjectScript-REST-Docker-template application
    sources:
    - https://github.com/intersystems-community/objectscript-rest-docker-template
    - https://github.com/intersystems-community/gke-terraform-circleci-objectscript-rest-docker-template
    $ cat templates/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: {{ template "iris-rest.name" . }}
      labels:
        app: {{ template "iris-rest.name" . }}
        chart: {{ template "iris-rest.chart" . }}
        release: {{ .Release.Name }}
        heritage: {{ .Release.Service }}
    spec:
      replicas: {{ .Values.replicaCount }}
      strategy:
        {{- .Values.strategy | nindent 4 }}
      selector:
        matchLabels:
          app: {{ template "iris-rest.name" . }}
          release: {{ .Release.Name }}
      template:
        metadata:
          labels:
            app: {{ template "iris-rest.name" . }}
            release: {{ .Release.Name }}
        spec:
          containers:
          - image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
            name: {{ template "iris-rest.name" . }}
            ports:
            - containerPort: {{ .Values.webPort.value }}
              name: {{ .Values.webPort.name }}
    $ cat templates/service.yaml
    {{- if .Values.service.enabled }}
    apiVersion: v1
    kind: Service
    metadata:
      name: {{ .Values.service.name }}
      labels:
        app: {{ template "iris-rest.name" . }}
        chart: {{ template "iris-rest.chart" . }}
        release: {{ .Release.Name }}
        heritage: {{ .Release.Service }}
    spec:
      selector:
        app: {{ template "iris-rest.name" . }}
        release: {{ .Release.Name }}
      ports:
      {{- range $key, $value := .Values.service.ports }}
        - name: {{ $key }}
    {{ toYaml $value | indent 6 }}
      {{- end }}
      type: {{ .Values.service.type }}
      {{- if ne .Values.service.loadBalancerIP "" }}
      loadBalancerIP: {{ .Values.service.loadBalancerIP }}
      {{- end }}
    {{- end }}
    $ cat templates/_helpers.tpl
    {{/* vim: set filetype=mustache: */}}
    {{/*
    Expande o nome do chart.
    */}}

    {{- define "iris-rest.name" -}}
    {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
    {{- end -}}

    {{/*
    Cria o nome e a versão do chart conforme usado pelo rótulo do chart.
    */}}
    {{- define "iris-rest.chart" -}}
    {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
    {{- end -}}

    $ cat values.yaml
    namespaceOverride: iris-rest

    replicaCount: 1

    strategy: |
      type: Recreate

    image:
      repository: eu.gcr.io/iris-rest
      tag: v1

    webPort:
      name: web
      value: 52773

    service:
      enabled: true
      name: iris-rest
      type: LoadBalancer
      loadBalancerIP: ""
      ports:
        web:
          port: 52773
          targetPort: 52773
          protocol: TCP

    Para criar os charts do Helm, instaleo cliente Helm e o utilitário de linha de comando kubectl.

    $ helm version
    version.BuildInfo{Version:"v3.0.1", GitCommit:"7c22ef9ce89e0ebeb7125ba2ebf7d421f3e82ffa", GitTreeState:"clean", GoVersion:"go1.13.4"}

    Crie um namespace chamado iris. Seria bom se isso fosse criado durante a implantação, mas até agora não é o caso.

    Primeiro, adicione credenciais para o cluster criado pelo Terraform ao kube-config:

    $ gcloud container clusters get-credentials --zone --project
    $ kubectl create ns iris

     

    Confirme (sem iniciar uma implantação real) se o Helm criará o seguinte no Kubernetes:

    $ cd /helm
    $ helm upgrade iris-rest \
      --install \
      . \
      --namespace iris \
      --debug \
      --dry-run

    A saída — os manifestos do Kubernetes — foi omitida por causa do espaço aqui. Se tudo estiver certo, vamos implantar:

    $ helm upgrade iris-rest --install . --namespace iris
    $ helm list -n iris --all
    Iris-rest  iris  1  2019-12-14 15:24:19.292227564  +0200  EET  deployed    iris-rest-0.1.0  1.0.3

    Vemos que o Helm implantou nossa aplicação, mas como ainda não criamos a imagem Docker eu.gcr.io/iris-rest:v1, o Kubernetes não pode extraí-la (ImagePullBackOff):

    $ kubectl -n iris get po
    NAME                                           READY  STATUS                       RESTARTS  AGE
    iris-rest-59b748c577-6cnrt 0/1         ImagePullBackOff  0                    10m

    Vamos terminar com isso por agora:

    $ helm delete iris-rest -n iris

    O Lado do CircleCI

    Agora que experimentamos o Terraform e o cliente Helm, vamos colocá-los em uso durante o processo de implantação no lado do CircleCI.

    $ cat /.circleci/config.yml
    version: 2.1

    orbs:
      gcp-gcr: circleci/gcp-gcr@0.6.1

    jobs:
      terraform:
        docker:
        # A versão da imagem do Terraform deve ser a mesma de quando
        # você executa o terraform antes da máquina local
          - image: hashicorp/terraform:0.12.17
        steps:
          - checkout
          - run:
              name: Create Service Account key file from environment variable
              working_directory: terraform
              command: echo ${TF_SERVICE_ACCOUNT_KEY} > account.json
          - run:
              name: Show Terraform version
              command: terraform version
          - run:
              name: Download required Terraform plugins
              working_directory: terraform
              command: terraform init
          - run:
              name: Validate Terraform configuration
              working_directory: terraform
              command: terraform validate
          - run:
              name: Create Terraform plan
              working_directory: terraform
              command: terraform plan -out /tmp/tf.plan
          - run:
              name: Run Terraform plan
              working_directory: terraform
              command: terraform apply /tmp/tf.plan
      k8s_deploy:
        docker:
          - image: kiwigrid/gcloud-kubectl-helm:3.0.1-272.0.0-218
        steps:
          - checkout
          - run:
              name: Authorize gcloud on GKE
              working_directory: helm
              command: |
                echo ${GCLOUD_SERVICE_KEY} > gcloud-service-key.json
                gcloud auth activate-service-account --key-file=gcloud-service-key.json
                gcloud container clusters get-credentials ${GKE_CLUSTER_NAME} --zone ${GOOGLE_COMPUTE_ZONE} --project ${GOOGLE_PROJECT_ID}
          - run:
              name: Wait a little until k8s worker nodes up
              command: sleep 30 # It’s a place for improvement
          - run:
              name: Create IRIS namespace if it doesn't exist
              command: kubectl get ns iris || kubectl create ns iris
          - run:
              name: Run Helm release deployment
              working_directory: helm
              command: |
                helm upgrade iris-rest \
                  --install \
                  . \
                  --namespace iris \
                  --wait \
                  --timeout 300s \
                  --atomic \
                  --set image.repository=eu.gcr.io/${GOOGLE_PROJECT_ID}/iris-rest \
                  --set image.tag=${CIRCLE_SHA1}
          - run:
              name: Check Helm release status
              command: helm list --all-namespaces --all
          - run:
              name: Check Kubernetes resources status
              command: |
                kubectl -n iris get pods
                echo
                kubectl -n iris get services
    workflows:
      main:
        jobs:
          - terraform
          - gcp-gcr/build-and-push-image:
              dockerfile: Dockerfile
              gcloud-service-key: GCLOUD_SERVICE_KEY
              google-compute-zone: GOOGLE_COMPUTE_ZONE
              google-project-id: GOOGLE_PROJECT_ID
              registry-url: eu.gcr.io
              image: iris-rest
              path: .
              tag: ${CIRCLE_SHA1}
          - k8s_deploy:
              requires:
                - terraform
                - gcp-gcr/build-and-push-image

    Você precisará adicionar várias variáveis de ambiente ao seu projeto no lado do CircleCI:

    O GCLOUD_SERVICE_KEY é a chave da conta de serviço CircleCI e o TF_SERVICE_ACCOUNT_KEY é a chave da conta de serviço Terraform. Lembre-se de que a chave da conta de serviço é todo o conteúdo do arquivo account.json.

    A seguir, vamos enviar nossas alterações para um repositório:

    $ cd
    $ git add .circleci/ helm/ terraform/ .gitignore
    $ git commit -m "Add Terraform and Helm"
    $ git push

    O painel da IU do CircleCI deve mostrar que tudo está bem:

    Terraform é uma ferramenta idempotente e se o cluster GKE estiver presente, o trabalho "terraform" não fará nada. Se o cluster não existir, ele será criado antes da implantação do Kubernetes.
    Por fim, vamos verificar a disponibilidade de IRIS:

    $ gcloud container clusters get-credentials --zone --project

    $ kubectl -n iris get svc
    NAME        TYPE                     CLUSTER-IP     EXTERNAL-IP   PORT(S)                     AGE
    Iris-rest    LoadBalancer  10.23.249.42    34.76.130.11    52773:31603/TCP   53s

    $ curl -XPOST -H "Content-Type: application/json" -u _system:SYS 34.76.130.11:52773/person/ -d '{"Name":"John Dou"}'

    $ curl -XGET -u _system:SYS 34.76.130.11:52773/person/all
    [{"Name":"John Dou"},]

    Conclusão

    Terraform e Helm são ferramentas DevOps padrão e devem ser perfeitamente integrados à implantação do IRIS.

    Eles exigem algum aprendizado, mas depois de alguma prática, eles podem realmente economizar seu tempo e esforço.

    0
    0 200
    Artigo Mikhail Khomenko · Mar. 2, 2021 17m read

    Introdução
    Vários recursos nos dizem como executar o IRIS em um cluster Kubernetes, como Implantar uma solução InterSystems IRIS no EKS usando GitHub Actions e Implantar a solução InterSystems IRIS no GKE usando GitHub Actions. Esses métodos funcionam, mas exigem que você crie manifestos do Kubernetes e gráficos do Helm, o que pode ser bastante demorado.
    Para simplificar a implantação do IRIS, a InterSystems desenvolveu uma ferramenta incrível chamada InterSystems Kubernetes Operator (IKO). Vários recursos oficiais explicam o uso de IKO em detalhes, como  Novo vídeo: Intersystems IRIS Kubernetes Operator e InterSystems Kubernetes Operator.

    A documentação do Kubernetes diz que os operadores substituem um operador humano que sabe como lidar com sistemas complexos no Kubernetes. Eles fornecem configurações do sistema na forma de recursos personalizados. Um operador inclui um controlador personalizado que lê essas configurações e executa as etapas definidas pelas configurações para configurar e manter corretamente sua aplicação. O controlador personalizado é um pod simples implantado no Kubernetes. Portanto, de modo geral, tudo que você precisa fazer para fazer um operador trabalhar é implantar um pod de controlador e definir suas configurações em recursos personalizados.
    Você pode encontrar uma explicação de alto nível sobre os operadores em: Como explicar os operadores do Kubernetes em inglês simples. Além disso, um e-book gratuito da O’Reilly está disponível para download.
    Neste artigo, veremos mais de perto o que são os operadores e o que os faz funcionar. Também escreveremos nosso próprio operador.

    Pré-requisitos e configuração

    Para acompanhar, você precisará instalar as seguintes ferramentas:

    kind

    $ kind --versionkind version 0.9.0
    golang
    $ go versiongo version go1.13.3 linux/amd64
    kubebuilder
    $ kubebuilder versionVersion: version.Version{KubeBuilderVersion:"2.3.1"…
    $ kubectl versionClient Version: version.Info{Major:"1", Minor:"15", GitVersion:"v1.15.11"...
    $ operator-sdk versionoperator-sdk version: "v1.2.0"…

    Recursos Personalizados

    Os recursos da API são um conceito importante no Kubernetes. Esses recursos permitem que você interaja com o Kubernetes por meio de endpoints HTTP que podem ser agrupados e versionados. A API padrão pode ser estendida com recursos personalizados, o que exige que você forneça uma Definição de Recurso Personalizado (CRD). Dê uma olhada na páginaEstender a API Kubernetes com CustomResourceDefinitions para informações detalhadas.
    Aqui está um exemplo de um CRD:

    $ cat crd.yaml apiVersion: apiextensions.k8s.io/v1beta1kind: CustomResourceDefinitionmetadata:  name: irises.example.comspec:  group: example.com  version: v1alpha1  scope: Namespaced  names:    plural: irises    singular: iris    kind: Iris    shortNames:    - ir  validation:    openAPIV3Schema:      required: ["spec"]      properties:        spec:          required: ["replicas"]          properties:            replicas:              type: "integer"              minimum: 0

    No exemplo acima, definimos o recurso API GVK(Group/Version/Kind) como example.com/v1alpha1/Iris, com réplicas como o único campo obrigatório.
    Agora vamos definir um recurso personalizado com base em nosso CRD:

    $ cat crd-object.yaml apiVersion: example.com/v1alpha1kind: Irismetadata:  name: irisspec:  test: 42  replicas: 1

    Em nosso recurso personalizado, podemos definir quaisquer campos além de réplicas, o que é exigido pelo CRD.
    Depois de implantar os dois arquivos acima, nosso recurso personalizado deve se tornar visível para o kubectl padrão.
    Vamos iniciar o Kubernetes localmente usando kind e, em seguida, executar os seguintes comandos kubectl:

    $ kind create cluster
    $ kubectl apply -f crd.yaml
    $ kubectl get crd irises.example.com
    NAME                 CREATED AT
    irises.example.com   2020-11-14T11:48:56Z
    
    $ kubectl apply -f crd-object.yaml
    $ kubectl get iris
    NAME   AGE
    iris   84s

    Embora tenhamos definido uma quantidade de réplica para o nosso IRIS, nada realmente acontece no momento. Isso é esperado. Precisamos implantar um controlador - a entidade que pode ler nosso recurso personalizado e realizar algumas ações baseadas em configurações.
    Por enquanto, vamos limpar o que criamos:

    $ kubectl delete -f crd-object.yaml$ kubectl delete -f crd.yaml

    Controlador

    Um controlador pode ser escrito em qualquer idioma. Usaremos Golang como linguagem "nativa" do Kubernetes. Poderíamos escrever a lógica de um controlador do zero, mas o pessoal do Google e da RedHat nos deu uma vantagem. Eles criaram dois projetos que podem gerar o código do operador que exigirá apenas alterações mínimas –kubebuilder e operator-sdk. Esses dois são comparados na página kubebuilder vs operator-sdk, bem como aqui: Qual é a diferença entre kubebuilder e operator-sdk #1758.

    Kubebuilder

    É conveniente começar nosso contato com o Kubebuilder na página do livro do Kubebuilder. O vídeo Tutorial: Zero ao Operador em 90 minutos do mantenedor do Kubebuilder também pode ajudar.

    Implementações de exemplo do projeto Kubebuilder podem ser encontradas nos repositórios sample-controller-kubebuilder e kubebuilder-sample-controller.

    Vamos construir um novo projeto de operador:

    $ mkdir iris$ cd iris$ go mod init iris # Cria um novo módulo, chame-o de iris$ kubebuilder init --domain myardyas.club # Um domínio arbitrário, usado abaixo como um sufixo no grupo da API

    Fazer scaffolding inclui muitos arquivos e manifestos. O arquivo main.go, por exemplo, é o ponto de entrada do código. Ele importa a biblioteca controller-runtime, e instancia e executa um gerenciador especial que mantém o controle da execução do controlador. Nada há mudar em nenhum desses arquivos.

    Vamos criar o CRD:

    $ kubebuilder create api --group test --version v1alpha1 --kind IrisCreate Resource [y/n]yCreate Controller [y/n]y

    Novamente, muitos arquivos são gerados. Eles são descritos em detalhes na página Adicionando uma nova API. Por exemplo, você pode ver que um arquivo do tipo Iris foi adicionado em api/v1alpha1/iris_types.go. Em nosso primeiro CRD de exemplo, definimos o campo de réplicasnecessário. Vamos criar um campo idêntico aqui, desta vez na estrutura IrisSpec. Também adicionaremos o campo DeploymentName. A contagem de réplicas também deve estar visível na seção Status , portanto, precisamos fazer as seguintes alterações:

    $ vim api/v1alpha1/iris_types.gotype IrisSpec struct {        // +kubebuilder:validation:MaxLength=64        DeploymentName string `json:"deploymentName"`        // +kubebuilder:validation:Minimum=0        Replicas *int32 `json:"replicas"`}type IrisStatus struct {        ReadyReplicas int32 `json:"readyReplicas"`}

    Depois de editar a API, passaremos para a edição do boilerplate do controlador. Toda a lógica deve ser definida no método Reconcile (este exemplo é retirado principalmente do mykind_controller.go). Também adicionamos alguns métodos auxiliares e reescrevemos o método SetupWithManager.

    $ vim controllers/iris_controller.go
    …
    import (
    ...
    // Deixe as importações existentes e adicione esses pacotes
            apps "k8s.io/api/apps/v1"
            core "k8s.io/api/core/v1"
            apierrors "k8s.io/apimachinery/pkg/api/errors"
            metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
            "k8s.io/client-go/tools/record"
    )
    // Adicione o campo Recorder para habilitar eventos Kubernetes
    type IrisReconciler struct {
            client.Client
            Log    logr.Logger
            Scheme *runtime.Scheme
            Recorder record.EventRecorder
    }
    …
    // +kubebuilder:rbac:groups=test.myardyas.club,resources=iris,verbs=get;list;watch;create;update;patch;delete
    // +kubebuilder:rbac:groups=test.myardyas.club,resources=iris/status,verbs=get;update;patch
    // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;delete
    // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
    
    func (r *IrisReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
        ctx := context.Background()
        log := r.Log.WithValues("iris", req.NamespacedName)
        // Buscar por objetos Iris por nome
        log.Info("fetching Iris resource")
        iris := testv1alpha1.Iris{}
        if err := r.Get(ctx, req.NamespacedName, &iris); err != nil {
            log.Error(err, "unable to fetch Iris resource")
            return ctrl.Result{}, client.IgnoreNotFound(err)
        }
        if err := r.cleanupOwnedResources(ctx, log, &iris); err != nil {
            log.Error(err, "failed to clean up old Deployment resources for Iris")
            return ctrl.Result{}, err
        }
    
        log = log.WithValues("deployment_name", iris.Spec.DeploymentName)
        log.Info("checking if an existing Deployment exists for this resource")
        deployment := apps.Deployment{}
        err := r.Get(ctx, client.ObjectKey{Namespace: iris.Namespace, Name: iris.Spec.DeploymentName}, &deployment)
        if apierrors.IsNotFound(err) {
            log.Info("could not find existing Deployment for Iris, creating one...")
    
            deployment = *buildDeployment(iris)
            if err := r.Client.Create(ctx, &deployment); err != nil {
                log.Error(err, "failed to create Deployment resource")
                return ctrl.Result{}, err
            }
    
            r.Recorder.Eventf(&iris, core.EventTypeNormal, "Created", "Created deployment %q", deployment.Name)
            log.Info("created Deployment resource for Iris")
            return ctrl.Result{}, nil
        }
        if err != nil {
            log.Error(err, "failed to get Deployment for Iris resource")
            return ctrl.Result{}, err
        }
    
        log.Info("existing Deployment resource already exists for Iris, checking replica count")
    
        expectedReplicas := int32(1)
        if iris.Spec.Replicas != nil {
            expectedReplicas = *iris.Spec.Replicas
        }
        
        if *deployment.Spec.Replicas != expectedReplicas {
            log.Info("updating replica count", "old_count", *deployment.Spec.Replicas, "new_count", expectedReplicas)            
            deployment.Spec.Replicas = &expectedReplicas
            if err := r.Client.Update(ctx, &deployment); err != nil {
                log.Error(err, "failed to Deployment update replica count")
                return ctrl.Result{}, err
            }
    
            r.Recorder.Eventf(&iris, core.EventTypeNormal, "Scaled", "Scaled deployment %q to %d replicas", deployment.Name, expectedReplicas)
    
            return ctrl.Result{}, nil
        }
    
        log.Info("replica count up to date", "replica_count", *deployment.Spec.Replicas)
        log.Info("updating Iris resource status")
        
        iris.Status.ReadyReplicas = deployment.Status.ReadyReplicas
        if r.Client.Status().Update(ctx, &iris); err != nil {
            log.Error(err, "failed to update Iris status")
            return ctrl.Result{}, err
        }
    
        log.Info("resource status synced")
        return ctrl.Result{}, nil
    }
    
    // Exclui os recursos de implantação que não correspondem mais ao campo iris.spec.deploymentName
    func (r *IrisReconciler) cleanupOwnedResources(ctx context.Context, log logr.Logger, iris *testv1alpha1.Iris) error {
        log.Info("looking for existing Deployments for Iris resource")
    
        var deployments apps.DeploymentList
        if err := r.List(ctx, &deployments, client.InNamespace(iris.Namespace), client.MatchingField(deploymentOwnerKey, iris.Name)); err != nil {
            return err
        }
        
        deleted := 0
        for _, depl := range deployments.Items {
            if depl.Name == iris.Spec.DeploymentName {
                // Sai da implantação se o nome corresponder ao do recurso Iris
                continue
            }
    
            if err := r.Client.Delete(ctx, &depl); err != nil {
                log.Error(err, "failed to delete Deployment resource")
                return err
            }
    
            r.Recorder.Eventf(iris, core.EventTypeNormal, "Deleted", "Deleted deployment %q", depl.Name)
            deleted++
        }
    
        log.Info("finished cleaning up old Deployment resources", "number_deleted", deleted)
        return nil
    }
    
    func buildDeployment(iris testv1alpha1.Iris) *apps.Deployment {
        deployment := apps.Deployment{
            ObjectMeta: metav1.ObjectMeta{
                Name:            iris.Spec.DeploymentName,
                Namespace:       iris.Namespace,
                OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(&iris, testv1alpha1.GroupVersion.WithKind("Iris"))},
            },
            Spec: apps.DeploymentSpec{
                Replicas: iris.Spec.Replicas,
                Selector: &metav1.LabelSelector{
                    MatchLabels: map[string]string{
                        "iris/deployment-name": iris.Spec.DeploymentName,
                    },
                },
                Template: core.PodTemplateSpec{
                    ObjectMeta: metav1.ObjectMeta{
                        Labels: map[string]string{
                            "iris/deployment-name": iris.Spec.DeploymentName,
                        },
                    },
                    Spec: core.PodSpec{
                        Containers: []core.Container{
                            {
                                Name:  "iris",
                                Image: "store/intersystems/iris-community:2020.4.0.524.0",
                            },
                        },
                    },
                },
            },
        }
        return &deployment
    }
    
    var (
        deploymentOwnerKey = ".metadata.controller"
    )
    
    // Especifica como o controlador é construído para assistir um CR e outros recursos 
    // que pertencem e são gerenciados por esse controlador
    func (r *IrisReconciler) SetupWithManager(mgr ctrl.Manager) error {
        if err := mgr.GetFieldIndexer().IndexField(&apps.Deployment{}, deploymentOwnerKey, func(rawObj runtime.Object) []string {
            // pega o objeto Deployment, extraia o proprietário...
            depl := rawObj.(*apps.Deployment)
            owner := metav1.GetControllerOf(depl)
            if owner == nil {
                return nil
            }
            // ...certifica-se de que é um Iris...
            if owner.APIVersion != testv1alpha1.GroupVersion.String() || owner.Kind != "Iris" {
                return nil
            }
    
            // ...e se for assim, o retorna
            return []string{owner.Name}
        }); err != nil {
            return err
        }
    
        return ctrl.NewControllerManagedBy(mgr).
            For(&testv1alpha1.Iris{}).
            Owns(&apps.Deployment{}).
            Complete(r)
    }

    Para fazer o registro de eventos funcionar, precisamos adicionar mais uma linha ao arquivo main.go:

    if err = (&controllers.IrisReconciler{                Client: mgr.GetClient(),                Log:    ctrl.Log.WithName("controllers").WithName("Iris"),                Scheme: mgr.GetScheme(),Recorder: mgr.GetEventRecorderFor("iris-controller"),        }).SetupWithManager(mgr); err != nil {

    Agora tudo está pronto para configurar um operador.
    Vamos instalar o CRD primeiro usando o Makefile de destino install:

    $ cat Makefile# Instala CRDs em um clusterinstall: manifests        kustomize build config/crd | kubectl apply -f -...$ make install

    Você pode dar uma olhada no arquivo CRD YAML resultante no diretório config/crd/bases/. 
    Agora verifique a existência do CRD no cluster:

    $ kubectl get crdNAME                      CREATED ATiris.test.myardyas.club   2020-11-17T11:02:02Z

    Vamos executar nosso controlador em outro terminal, localmente (não no Kubernetes) – apenas para ver se ele realmente funciona:

    $ make run...2020-11-17T13:02:35.649+0200INFOcontroller-runtime.metricsmetrics server is starting to listen{"addr": ":8080"}2020-11-17T13:02:35.650+0200INFOsetupstarting manager2020-11-17T13:02:35.651+0200INFOcontroller-runtime.managerstarting metrics server{"path": "/metrics"}2020-11-17T13:02:35.752+0200INFOcontroller-runtime.controllerStarting EventSource{"controller": "iris", "source": "kind source: /, Kind="}2020-11-17T13:02:35.852+0200INFOcontroller-runtime.controllerStarting EventSource{"controller": "iris", "source": "kind source: /, Kind="}2020-11-17T13:02:35.853+0200INFOcontroller-runtime.controllerStarting Controller{"controller": "iris"}2020-11-17T13:02:35.853+0200INFOcontroller-runtime.controllerStarting workers{"controller": "iris", "worker count": 1}

    Agora que temos o CRD e o controlador instalados, tudo o que precisamos fazer é criar uma instância do nosso recurso personalizado. Um modelo pode ser encontrado no arquivo config/samples/example.com_v1alpha1_iris.yaml. Neste arquivo, precisamos fazer alterações semelhantes àquelas em crd-object.yaml:

    $ cat config/samples/test_v1alpha1_iris.yamlapiVersion: test.myardyas.club/v1alpha1kind: Irismetadata:  name: irisspec:  deploymentName: iris  replicas: 1$ kubectl apply -f config/samples/test_v1alpha1_iris.yaml

    Após um breve atraso causado pela necessidade de extrair uma imagem IRIS, você deverá ver o pod IRIS em execução:

    $ kubectl get deployNAME   READY   UP-TO-DATE   AVAILABLE   AGEiris   1/1     1            1           119s$ kubectl get podNAME                   READY   STATUS    RESTARTS   AGEiris-6b78cbb67-vk2gq   1/1     Running   0          2m42s$ kubectl logs -f -l iris/deployment-name=iris

    Você pode abrir o portal IRIS usando o comando kubectl port-forward:

    $ kubectl port-forward deploy/iris 52773

    Vá em http://localhost:52773/csp/sys/UtilHome.csp em seu navegador. 
    E se mudarmos a contagem das réplicas no CRD? Vamos fazer e aplicar esta mudança:

    $ vi config/samples/test_v1alpha1_iris.yaml  replicas: 2$ kubectl apply -f config/samples/test_v1alpha1_iris.yaml

    Agora você deve ver outro pod Iris aparecer.

    $ kubectl get events54s         Normal   Scaled                    iris/iris                   Scaled deployment "iris" to 2 replicas54s         Normal   ScalingReplicaSet         deployment/iris             Scaled up replica set iris-6b78cbb67 to 2

    Veja as mensagens de log no terminal onde o controlador executa o relatório de reconciliação bem-sucedida:

    2020-11-17T13:09:04.102+0200INFOcontrollers.Irisreplica count up to date{"iris": "default/iris", "deployment_name": "iris", "replica_count": 2}2020-11-17T13:09:04.102+0200INFOcontrollers.Irisupdating Iris resource status{"iris": "default/iris", "deployment_name": "iris"}2020-11-17T13:09:04.104+0200INFOcontrollers.Irisresource status synced{"iris": "default/iris", "deployment_name": "iris"}2020-11-17T13:09:04.104+0200DEBUGcontroller-runtime.controllerSuccessfully Reconciled{"controller": "iris", "request": "default/iris"}

    Ok, nossos controladores parecem estar funcionando. Agora estamos prontos para implantar esse controlador dentro do Kubernetes como um pod. Para isso, precisamos criar o contêiner docker do controlador e enviá-lo para o registro. Pode ser qualquer registro que funcione com Kubernetes –  DockerHub, ECR, GCR e assim por diante. 
    Usaremos o Kubernetes local (kind), então vamos implantar o controlador no registro local usando o script kind-with-registry.sh disponível na página de Registro Local. Podemos simplesmente remover o cluster atual e recriá-lo:

    $ kind delete cluster$ ./kind_with_registry.sh$ make install$ docker build . -t localhost:5000/iris-operator:v0.1 # Dockerfile é autogerado por kubebuilder$ docker push localhost:5000/iris-operator:v0.1$ make deploy IMG=localhost:5000/iris-operator:v0.1

    O controlador será implantado no namespace do sistema IRIS. Como alternativa, você pode verificar todos os pods para encontrar um namespace como kubectl get pod -A):

    $ kubectl -n iris-system get poNAME                                      READY   STATUS    RESTARTS   AGEiris-controller-manager-bf9fd5855-kbklt   2/2     Running   0          54s

    Vamos verificar os logs:

    $ kubectl -n iris-system logs -f -l control-plane=controller-manager -c manager

    Você pode experimentar alterar a contagem de réplicas no CRD e observar como essas mudanças são refletidas na contagem de instâncias IRIS.

    Operator-SDK

    Outra ferramenta útil para gerar o código do operador é o Operator SDK. Para ter uma ideia inicial dessa ferramenta, dê uma olhada neste tutorial. Você deve instalar o operador-sdk primeiro.
    Para nosso caso de uso simples, o processo será semelhante ao que trabalhamos com o kubebuilder (você pode excluir/criar o kind cluster com o registro Docker antes de continuar). Execute em outro diretório:

    $ mkdir iris$ cd iris$ go mod init iris$ operator-sdk init --domain=myardyas.club$ operator-sdk create api --group=test --version=v1alpha1 --kind=Iris# Responda dois ‘yes’

    Agora altere as estruturas IrisSpec e IrisStatus no mesmo arquivo –  api/v1alpha1/iris_types.go.
    Usaremos o mesmo arquivo iris_controller.go que usamos no kubebuilder. Não se esqueça de adicionar o campoRecorder no arquivo main.go.
    Como  o kubebuilder e o operator-sdk usam versões diferentes dos pacotes Golang, você deve adicionar um contexto na função SetupWithManager em controllers/iris_controller.go:

    ctx := context.Background()if err := mgr.GetFieldIndexer().IndexField(ctx, &apps.Deployment{}, deploymentOwnerKey, func(rawObj runtime.Object) []string {

    Em seguida, instale o CRD e o operador (certifique-se de que o kind cluster está em execução):

    $ make install$ docker build . -t localhost:5000/iris-operator:v0.2$ docker push localhost:5000/iris-operator:v0.2$ make deploy IMG=localhost:5000/iris-operator:v0.2

    Agora você deve ver o CRD, o pod do operador e o(s) pod(s) IRIS semelhantes aos que vimos quando trabalhamos com o kubebuilder.

    Conclusão

    Embora um controlador inclua muito código, você viu que alterar as réplicas IRIS é apenas uma questão de alterar uma linha em um recurso personalizado. Toda a complexidade está oculta na implementação do controlador. Vimos como um operador simples pode ser criado usando ferramentas de scaffolding úteis. 
    Nosso operador se preocupava apenas com as réplicas IRIS. Agora imagine que realmente precisamos ter os dados IRIS persistentes no disco. Isso exigiria  StatefulSet e Volumes Persistentes. Além disso, precisaríamos de um Service e, talvez, Ingress para acesso externo. Devemos ser capazes de definir a versão do IRIS e a senha do sistema, espelhamento e/ou ECP e assim por diante. Você pode imaginar a quantidade de trabalho que a InterSystems teve que realizar para simplificar a implantação do IRIS, ocultando toda a lógica específica do IRIS dentro do código do operador. 
    No próximo artigo, veremos o IRIS Operator (IKO) em mais detalhes, e investigaremos suas possibilidades em cenários mais complexos.

    0
    0 196
    Artigo Mikhail Khomenko · Nov. 23, 2020 21m read

    Imagine que você queira ver o que a tecnologia InterSystems pode oferecer em termos de análise de dados. Você estudou a teoria e agora quer um pouco de prática. Felizmente, a InterSystems oferece um projeto que contém alguns bons exemplos: Samples BI. Comece com o arquivo README, pulando qualquer coisa associada ao Docker, e vá direto para a instalação passo a passo. Inicie uma instância virtual, instale o IRIS lá, siga as instruções para instalar o Samples BI e, a seguir, impressione o chefe com belos gráficos e tabelas. Por enquanto, tudo bem. 

    Inevitavelmente, porém, você precisará fazer alterações.

    Acontece que manter uma máquina virtual sozinha tem algumas desvantagens e é melhor mantê-la com um provedor em nuvem. A Amazon parece sólida e você cria uma conta no AWS (gratuita para iniciar), lê que usar a identidade do usuário root para tarefas diárias é ruim e cria um usuário IAM normal com permissões de administrador.

    Clicando um pouco, você cria sua própria rede VPC, sub-redes e uma instância virtual EC2, e também adiciona um grupo de segurança para abrir a porta web IRIS (52773) e a porta ssh (22) para você. Repete a instalação do IRIS e Samples BI. Desta vez, usa o script Bash ou Python, se preferir. Mais uma vez, impressiona o chefe.

    Mas o movimento DevOps onipresente leva você a começar a ler sobre infraestrutura como código e você deseja implementá-la. Você escolhe o Terraform, já que ele é bem conhecido de todos e sua abordagem é bastante universal - adequada com pequenos ajustes para vários provedores em nuvem. Você descreve a infraestrutura em linguagem HCL e traduz as etapas de instalação do IRIS e Samples BI para o Ansible. Em seguida, você cria mais um usuário IAM para permitir que o Terraform funcione. Executa tudo. Ganha um bônus no trabalho.

    Gradualmente, você chega à conclusão de que, em nossa era de microsserviços, é uma pena não usar o Docker, especialmente porque a InterSystems lhe diz como. Você retorna ao guia de instalação do Samples BI e lê as linhas sobre o Docker, que não parecem complicadas:

    $ docker pull intersystemsdc/iris-community:2019.4.0.383.0-zpm
    $ docker run --name irisce -d --publish 52773:52773 intersystemsdc/iris-community:2019.4.0.383.0-zpm
    $ docker exec -it irisce iris session iris
    USER>zpm
    zpm: USER>install samples-bi

    Depois de direcionar seu navegador para ttp://localhost:52773/csp/user/_DeepSee.UserPortal.Home.zen?$NAMESPACE=USER, você vai novamente ao chefe e tira um dia de folga por um bom trabalho.

    Você então começa a entender que “docker run” é apenas o começo e você precisa usar pelo menos docker-compose. Não é um problema:

    $ cat docker-compose.yml
    version: "3.7"
    services:
      irisce:
        container_name: irisce
        image: intersystemsdc/iris-community:2019.4.0.383.0-zpm
        ports:
        - 52773:52773
    $ docker rm -f irisce # We don’t need the previous container
    $ docker-compose up -d

    Então, você instala o Docker e o docker-compose com o Ansible e, em seguida, apenas executa o contêiner, que fará o download de uma imagem se ainda não estiver presente na máquina. Em seguida, você instala Samples BI.

    Você certamente gosta do Docker, porque é uma interface simples e legal para várias coisas do kernel. Você começa a usar o Docker em outro lugar e geralmente inicia mais de um contêiner. E descobre que muitas vezes os contêineres devem se comunicar entre si, o que leva à leitura sobre como gerenciar vários contêineres. 

    E você chega ao Kubernetes

    Uma opção para mudar rapidamente de docker-compose para Kubernetes é usar o kompose. Pessoalmente, prefiro simplesmente copiar os manifestos do Kubernetes dos manuais e, em seguida, editá-los, mas o kompose faz um bom trabalho ao concluir sua pequena tarefa:

    $ kompose convert -f docker-compose.yml
    INFO Kubernetes file "irisce-service.yaml" created
    INFO Kubernetes file "irisce-deployment.yaml" created

    Agora você tem os arquivos de implantação e serviço que podem ser enviados para algum cluster do Kubernetes. Você descobre que pode instalar um minikube, que permite executar um cluster Kubernetes de nó único e é exatamente o que você precisa neste estágio. Depois de um ou dois dias brincando com a sandbox do minikube, você está pronto para usar uma implantação real e ao vivo do Kubernetes em algum lugar da nuvem AWS.

    Preparação

    Então, vamos fazer isso juntos. Neste ponto, faremos algumas suposições:

    Primeiro, presumimos que você tenha uma conta no AWS, saiba seu ID e não use credenciais de root. Você criou um usuário IAM (vamos chamá-lo de “my-user”) com direitos de administrador e apenas acesso programático e armazenou suas credenciais. Você também criou outro usuário IAM, chamado “terraform”, com as mesmas permissões:

    Em seu nome, o Terraform irá para sua conta no AWS e criará e excluirá os recursos necessários. Os amplos direitos de ambos os usuários são explicados pelo fato de que se trata de uma demonstração. Você salva as credenciais localmente para os dois usuários IAM:

    $ cat ~/.aws/credentials
    [terraform]
    aws_access_key_id = ABCDEFGHIJKLMNOPQRST
    aws_secret_access_key = ABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890123
    [my-user]
    aws_access_key_id = TSRQPONMLKJIHGFEDCBA
    aws_secret_access_key = TSRQPONMLKJIHGFEDCBA01234567890123

    Observação: não copie e cole as credenciais acima. Eles são fornecidos aqui como um exemplo e não existem mais. Edite o arquivo ~/.aws/credentials e introduza seus próprios registros.

    Em segundo lugar, usaremos o ID do conta AWS fictícia (01234567890) para o artigo e a região AWS “eu-west-1.” Sinta-se à vontade para usar outra região.

    Terceiro, presumimos que você esteja ciente de que o AWS não é gratuito e que você terá que pagar pelos recursos usados.

    Em seguida, você instalou o utilitário AWS CLI para comunicação via linha de comando com o AWS. Você pode tentar usar o aws2, mas precisará definir especificamente o uso do aws2 em seu arquivo de configuração do kube, conforme descrito aqui.

    Você também instalou o utilitário kubectl para comunicação via linha de comando com AWS Kubernetes.

    E você instalou o utilitário kompose para docker-compose.yml para converter manifestos do Kubernetes.

    Finalmente, você criou um repositório GitHub vazio e clonou-o em seu host. Vamos nos referir ao seu diretório raiz como . Neste repositório, vamos criar e preencher três diretórios: .github/workflows/, k8s/, e terraform/.

    Observe que todo o código relevante é duplicado no repositório github-eks-samples-bi para simplificar a cópia e a colagem.

    Vamos continuar.

    Provisionamento AWS EKS

    Já conhecemos o EKS no artigo Implementando uma aplicação web simples baseado em IRIS usando o Amazon EKS. Naquela época, criamos um cluster semiautomático. Ou seja, descrevemos o cluster em um arquivo e, em seguida, iniciamos manualmente o utilitário eksctl de uma máquina local, que criou o cluster de acordo com nossa descrição. 

    O eksctl foi desenvolvido para a criação de clusters EKS e é bom para uma implementação de prova de conceito, mas para o uso diário é melhor usar algo mais universal, como o Terraform. Um ótimo recurso, Introdução ao AWS EKS, explica a configuração do Terraform necessária para criar um cluster EKS. Uma ou duas horas gastas para conhecê-lo não será uma perda de tempo.

    Você pode brincar com o Terraform localmente. Para fazer isso, você precisará de um binário (usaremos a versão mais recente para Linux no momento da redação do artigo, 0.12.20) e o usuário IAM “terraform” com direitos suficientes para o Terraform ir para o AWS. Crie o diretório /terraform/ para armazenar o código do Terraform:

    $ mkdir /terraform
    $ cd /terraform

    Você pode criar um ou mais arquivos .tf (eles são mesclados na inicialização). Basta copiar e colar os exemplos de código da Introdução ao AWS EKS e, em seguida, executar algo como:

    $ export AWS_PROFILE=terraform
    $ export AWS_REGION=eu-west-1
    $ terraform init
    $ terraform plan -out eks.plan

    Você pode encontrar alguns erros. Nesse caso, brinque um pouco com o modo de depuração, mas lembre-se de desligá-lo mais tarde:

    $ export TF_LOG=debug
    $ terraform plan -out eks.plan

    $ unset TF_LOG

    Esta experiência será útil, e muito provavelmente você terá um cluster EKS iniciado (use “terraform apply” para isso). Verifique no console do AWS:

    Limpe-o quando você ficar entediado:

    $ terraform destroy

    Em seguida, vá para o próximo nível e comece a usar o módulo Terraform EKS, especialmente porque ele é baseado na mesma introdução ao EKS. No diretório examples/ você verá como usá-lo. Você também  encontrará outros exemplos lá.

    Simplificamos um pouco os exemplos. Este é o arquivo principal em que os módulos de criação de VPC e de criação de EKS são chamados:

    $ cat /terraform/main.tf
    terraform {
      required_version = ">= 0.12.0"
      backend "s3" {
        bucket         = "eks-github-actions-terraform"
        key            = "terraform-dev.tfstate"
        region         = "eu-west-1"
        dynamodb_table = "eks-github-actions-terraform-lock"
      }
    }

    provider "kubernetes" {
      host                   = data.aws_eks_cluster.cluster.endpoint
      cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data)
      token                  = data.aws_eks_cluster_auth.cluster.token
      load_config_file       = false
      version                = "1.10.0"
    }

    locals {
      vpc_name             = "dev-vpc"
      vpc_cidr             = "10.42.0.0/16"
      private_subnets      = ["10.42.1.0/24", "10.42.2.0/24"]
      public_subnets       = ["10.42.11.0/24", "10.42.12.0/24"]
      cluster_name         = "dev-cluster"
      cluster_version      = "1.14"
      worker_group_name    = "worker-group-1"
      instance_type        = "t2.medium"
      asg_desired_capacity = 1
    }

    data "aws_eks_cluster" "cluster" {
      name = module.eks.cluster_id
    }

    data "aws_eks_cluster_auth" "cluster" {
      name = module.eks.cluster_id
    }

    data "aws_availability_zones" "available" {
    }

    module "vpc" {
      source               = "git::https://github.com/terraform-aws-modules/terraform-aws-vpc?ref=master"

      name                 = local.vpc_name
      cidr                 = local.vpc_cidr
      azs                  = data.aws_availability_zones.available.names
      private_subnets      = local.private_subnets
      public_subnets       = local.public_subnets
      enable_nat_gateway   = true
      single_nat_gateway   = true
      enable_dns_hostnames = true

      tags = {
        "kubernetes.io/cluster/${local.cluster_name}" = "shared"
      }

      public_subnet_tags = {
        "kubernetes.io/cluster/${local.cluster_name}" = "shared"
        "kubernetes.io/role/elb" = "1"
      }

      private_subnet_tags = {
        "kubernetes.io/cluster/${local.cluster_name}" = "shared"
        "kubernetes.io/role/internal-elb" = "1"
      }
    }

    module "eks" {
      source = "git::https://github.com/terraform-aws-modules/terraform-aws-eks?ref=master"
      cluster_name     = local.cluster_name
      cluster_version  = local.cluster_version
      vpc_id           = module.vpc.vpc_id
      subnets          = module.vpc.private_subnets
      write_kubeconfig = false

      worker_groups = [
        {
          name                 = local.worker_group_name
          instance_type        = local.instance_type
          asg_desired_capacity = local.asg_desired_capacity
        }
      ]

      map_accounts = var.map_accounts
      map_roles    = var.map_roles
      map_users    = var.map_users
    }

    Vejamos um pouco mais de perto o bloco "terraform" em main.tf:

    terraform {
      required_version = ">= 0.12.0"
      backend "s3" {
        bucket         = "eks-github-actions-terraform"
        key            = "terraform-dev.tfstate"
        region         = "eu-west-1"
        dynamodb_table = "eks-github-actions-terraform-lock"
      }
    }

    Aqui, indicamos que seguiremos a sintaxe não inferior ao Terraform 0.12 (muito mudou em comparação com as versões anteriores) e também que Terraform não deve armazenar seu estado localmente, mas sim remotamente, no S3 bucket. 

    É conveniente se o código do terraform possa ser atualizado de lugares diferentes por pessoas diferentes, o que significa que precisamos ser capazes de bloquear o estado de um usuário, então adicionamos um bloqueio usando uma tabela dynamodb. Leia mais sobre bloqueios na página State Locking (bloqueio de estado).

    Como o nome do bucket deve ser único em toda o AWS, o nome “eks-github-actions-terraform” não funcionará para você. Pense por conta própria e certifique-se de que ele ainda não foi usado (se sim, você receberá um erro NoSuchBucket):

    $ aws s3 ls s3://my-bucket
    An error occurred (AllAccessDisabled) when calling the ListObjectsV2 operation: All access to this object has been disabled
    $ aws s3 ls s3://my-bucket-with-name-that-impossible-to-remember
    An error occurred (NoSuchBucket) when calling the ListObjectsV2 operation: The specified bucket does not exist

    Tendo criado um nome, crie o bucket (usamos o usuário IAM “terraform” aqui. Ele tem direitos de administrador para que possa criar um bucket) e habilite o controle de versão para ele (o que lhe salvará em caso de um erro de configuração):

    $ aws s3 mb s3://eks-github-actions-terraform --region eu-west-1
    make_bucket: eks-github-actions-terraform
    $ aws s3api put-bucket-versioning --bucket eks-github-actions-terraform --versioning-configuration Status=Enabled
    $ aws s3api get-bucket-versioning --bucket eks-github-actions-terraform
    {
      "Status": "Enabled"
    }

    Com o DynamoDB, ser único não é necessário, mas você precisa criar uma tabela primeiro:

    $ aws dynamodb create-table                                                                                     \
      --region eu-west-1                                                                                                           \
      --table-name eks-github-actions-terraform-lock                                              \
      --attribute-definitions AttributeName=LockID,AttributeType=S                \
      --key-schema AttributeName=LockID,KeyType=HASH                                   \
      --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5

    Lembre-se de que, em caso de falha do Terraform, você pode precisar remover um bloqueio manualmente do console AWS. Mas tenha cuidado ao fazer isso.

    Com relação aos blocos do módulo eks/vpc em main.tf, a forma de referenciar o módulo disponível no GitHub é simples:

    git::https://github.com/terraform-aws-modules/terraform-aws-vpc?ref=master

    Agora vamos dar uma olhada em nossos outros dois arquivos Terraform (variables.tf e outputs.tf). O primeiro contém nossas variáveis Terraform:

    $ cat /terraform/variables.tf
    variable "region" {
      default = "eu-west-1"
    }

    variable "map_accounts" {
      description = "Additional AWS account numbers to add to the aws-auth configmap. See examples/basic/variables.tf for example format."
      type        = list(string)
      default     = []
    }

    variable "map_roles" {
      description = "Additional IAM roles to add to the aws-auth configmap."
      type = list(object({
        rolearn  = string
        username = string
        groups   = list(string)
      }))
      default = []
    }

    variable "map_users" {
      description = "Additional IAM users to add to the aws-auth configmap."
      type = list(object({
        userarn  = string
        username = string
        groups   = list(string)
      }))
      default = [
        {
          userarn  = "arn:aws:iam::01234567890:user/my-user"
          username = "my-user"
          groups   = ["system:masters"]
        }
      ]
    }

    A parte mais importante aqui é adicionar o usuário IAM “my-user” à variável map_users, mas você deve usar seu próprio ID de conta aqui no lugar de 01234567890.

    O que isto faz? Quando você se comunica com o EKS por meio do cliente kubectl local, ele envia solicitações ao servidor da API Kubernetes, e cada solicitação passa por processos de autenticação e autorização para que o Kubernetes possa entender quem enviou a solicitação e o que elas podem fazer. Portanto, a versão EKS do Kubernetes pede ajuda ao AWS IAM com a autenticação do usuário. Se o usuário que enviou a solicitação estiver listado no AWS IAM (apontamos seu ARN aqui), a solicitação vai para a fase de autorização, que o EKS processa sozinho, mas de acordo com nossas configurações. Aqui, indicamos que o usuário IAM “my-user” é muito legal (grupo “system: masters”).

    Por fim, o arquivo outputs.tf descreve o que o Terraform deve imprimir após concluir um job:

    $ cat /terraform/outputs.tf
    output "cluster_endpoint" {
      description = "Endpoint for EKS control plane."
      value       = module.eks.cluster_endpoint
    }

    output "cluster_security_group_id" {
      description = "Security group ids attached to the cluster control plane."
      value       = module.eks.cluster_security_group_id
    }

    output "config_map_aws_auth" {
      description = "A kubernetes configuration to authenticate to this EKS cluster."
      value       = module.eks.config_map_aws_auth
    }

    Isso completa a descrição da parte do Terraform. Voltaremos em breve para ver como vamos iniciar esses arquivos.

    Manifestos Kubernetes

    Até agora, cuidamos de onde iniciar a aplicação. Agora vamos ver o que executar. 

    Lembre-se de que temos o docker-compose.yml (renomeamos o serviço e adicionamos alguns rótulos que o kompose usará em breve) no diretório /k8s/:

    $ cat /k8s/docker-compose.yml
    version: "3.7"
    services:
      samples-bi:
        container_name: samples-bi
        image: intersystemsdc/iris-community:2019.4.0.383.0-zpm
        ports:
        - 52773:52773
        labels:
          kompose.service.type: loadbalancer
          kompose.image-pull-policy: IfNotPresent

    Execute o kompose e adicione o que está destacado abaixo. Exclua as anotações (para tornar as coisas mais inteligíveis):

    $ kompose convert -f docker-compose.yml --replicas=1
    $ cat /k8s/samples-bi-deployment.yaml
    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      labels:
        io.kompose.service: samples-bi
      name: samples-bi
    spec:
      replicas: 1
      strategy:
        type: Recreate
      template:
        metadata:
          labels:
            io.kompose.service: samples-bi
        spec:
          containers:
          - image: intersystemsdc/iris-community:2019.4.0.383.0-zpm
            imagePullPolicy: IfNotPresent
            name: samples-bi
            ports:
            - containerPort: 52773
            resources: {}
            lifecycle:
              postStart:
                exec:
                  command:
                  - /bin/bash
                  - -c
                  - |
                    echo -e "write\nhalt" > test
                    until iris session iris < test; do sleep 1; done
                    echo -e "zpm\ninstall samples-bi\nquit\nhalt" > samples_bi_install
                    iris session iris < samples_bi_install
                    rm test samples_bi_install

            restartPolicy: Always

    Usamos a estratégia de atualização de Recriar, o que significa que o pod será excluído primeiro e depois recriado. Isso é permitido para fins de demonstração e nos permite usar menos recursos.
    Também adicionamos o postStart hook, que será disparado imediatamente após o início do pod. Esperamos até o IRIS iniciar e instalar o pacote samples-bi do repositório zpm padrão.
    Agora adicionamos o serviço Kubernetes (também sem anotações):

    $ cat /k8s/samples-bi-service.yaml
    apiVersion: v1
    kind: Service
    metadata:
      labels:
        io.kompose.service: samples-bi
      name: samples-bi
    spec:
      ports:
      - name: "52773"
        port: 52773
        targetPort: 52773
      selector:
        io.kompose.service: samples-bi
      type: LoadBalancer

    Sim, vamos implantar no namespace "padrão", que funcionará para a demonstração.

    Ok, agora sabemos onde e o que queremos executar. Resta ver como.

    O fluxo de trabalho do GitHub Actions

    Em vez de fazer tudo do zero, criaremos um fluxo de trabalho semelhante ao descrito em Implantando uma solução InterSystems IRIS no GKE usando GitHub Actions. Desta vez, não precisamos nos preocupar em construir um contêiner. As partes específicas do GKE são substituídas pelas específicas do EKS. As partes em negrito estão relacionadas ao recebimento da mensagem de commit e ao uso dela em etapas condicionais:

    $ cat /.github/workflows/workflow.yaml
    name: Provision EKS cluster and deploy Samples BI there
    on:
      push:
        branches:
        - master

    # Environment variables.
    # ${{ secrets }} are taken from GitHub -> Settings -> Secrets
    # ${{ github.sha }} is the commit hash
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_REGION: ${{ secrets.AWS_REGION }}
      CLUSTER_NAME: dev-cluster
      DEPLOYMENT_NAME: samples-bi

    jobs:
      eks-provisioner:
        # Inspired by:
        ## https://www.terraform.io/docs/github-actions/getting-started.html
        ## https://github.com/hashicorp/terraform-github-actions
        name: Provision EKS cluster
        runs-on: ubuntu-18.04
        steps:
        - name: Checkout
          uses: actions/checkout@v2

        - name: Get commit message
          run: |
            echo ::set-env name=commit_msg::$(git log --format=%B -n 1 ${{ github.event.after }})

        - name: Show commit message
          run: echo $commit_msg

        - name: Terraform init
          uses: hashicorp/terraform-github-actions@master
          with:
            tf_actions_version: 0.12.20
            tf_actions_subcommand: 'init'
            tf_actions_working_dir: 'terraform'

        - name: Terraform validate
          uses: hashicorp/terraform-github-actions@master
          with:
            tf_actions_version: 0.12.20
            tf_actions_subcommand: 'validate'
            tf_actions_working_dir: 'terraform'

        - name: Terraform plan
          if: "!contains(env.commit_msg, '[destroy eks]')"
          uses: hashicorp/terraform-github-actions@master
          with:
            tf_actions_version: 0.12.20
            tf_actions_subcommand: 'plan'
            tf_actions_working_dir: 'terraform'

        - name: Terraform plan for destroy
          if: "contains(env.commit_msg, '[destroy eks]')"
          uses: hashicorp/terraform-github-actions@master
          with:
            tf_actions_version: 0.12.20
            tf_actions_subcommand: 'plan'
            args: '-destroy -out=./destroy-plan'
            tf_actions_working_dir: 'terraform'

        - name: Terraform apply
          if: "!contains(env.commit_msg, '[destroy eks]')"
          uses: hashicorp/terraform-github-actions@master
          with:
            tf_actions_version: 0.12.20
            tf_actions_subcommand: 'apply'
            tf_actions_working_dir: 'terraform'

        - name: Terraform apply for destroy
          if: "contains(env.commit_msg, '[destroy eks]')"
          uses: hashicorp/terraform-github-actions@master
          with:
            tf_actions_version: 0.12.20
            tf_actions_subcommand: 'apply'
            args: './destroy-plan'
            tf_actions_working_dir: 'terraform'

      kubernetes-deploy:
        name: Deploy Kubernetes manifests to EKS
        needs:
        - eks-provisioner
        runs-on: ubuntu-18.04
        steps:
        - name: Checkout
          uses: actions/checkout@v2

        - name: Get commit message
          run: |
            echo ::set-env name=commit_msg::$(git log --format=%B -n 1 ${{ github.event.after }})

        - name: Show commit message
          run: echo $commit_msg

        - name: Configure AWS Credentials
          if: "!contains(env.commit_msg, '[destroy eks]')"
          uses: aws-actions/configure-aws-credentials@v1
          with:
            aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
            aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
            aws-region: ${{ secrets.AWS_REGION }}

        - name: Apply Kubernetes manifests
          if: "!contains(env.commit_msg, '[destroy eks]')"
          working-directory: ./k8s/
          run: |
            aws eks update-kubeconfig --name ${CLUSTER_NAME}
            kubectl apply -f samples-bi-service.yaml
            kubectl apply -f samples-bi-deployment.yaml
            kubectl rollout status deployment/${DEPLOYMENT_NAME}

    É claro que, precisamos definir as credenciais do usuário "terraform" (retirá-las do arquivo ~/.aws/credentials), permitindo que o GitHub use seus segredos:

    Observe as partes destacadas do fluxo de trabalho. Elas nos permitirão destruir um cluster EKS empurrando uma mensagem de commit que contém a frase “[destroy eks]”. Observe que não executaremos "kubernetes apply" com essa mensagem de commit.
    Execute um pipeline, mas primeiro crie um arquivo .gitignore:

    $ cat /.gitignore
    .DS_Store
    terraform/.terraform/
    terraform/*.plan
    terraform/*.json

    $ cd
    $ git add .github/ k8s/ terraform/ .gitignore
    $ git commit -m "GitHub on EKS"
    $ git push

    Monitore o processo de implantação na aba "Actions" da página do repositório GitHub. Aguarde a conclusão com sucesso.

    Quando você executa um fluxo de trabalho pela primeira vez, leva cerca de 15 minutos na etapa “Terraform apply”, aproximadamente o mesmo tempo necessário para criar o cluster. Na próxima inicialização (se você não excluiu o cluster), o fluxo de trabalho será muito mais rápido. Você pode verificar isso:

    $ cd
    $ git commit -m "Trigger" --allow-empty
    $ git push

    É claro que seria bom verificar o que fizemos. Desta vez, você pode usar as credenciais do IAM “my-user” em seu computador:

    $ export AWS_PROFILE=my-user
    $ export AWS_REGION=eu-west-1
    $ aws sts get-caller-identity
    $ aws eks update-kubeconfig --region=eu-west-1 --name=dev-cluster --alias=dev-cluster
    $ kubectl config current-context
    dev-cluster

    $ kubectl get nodes
    NAME                                                                               STATUS   ROLES      AGE          VERSION
    ip-10-42-1-125.eu-west-1.compute.internal   Ready          6m20s     v1.14.8-eks-b8860f

    $ kubectl get po
    NAME                                                       READY        STATUS      RESTARTS   AGE
    samples-bi-756dddffdb-zd9nw    1/1               Running    0                      6m16s

    $ kubectl get svc
    NAME                   TYPE                        CLUSTER-IP        EXTERNAL-IP                                                                                                                                                         PORT(S)                    AGE
    kubernetes        ClusterIP               172.20.0.1                                                                                                                                                                                443/TCP                    11m
    samples-bi         LoadBalancer     172.20.33.235    a2c6f6733557511eab3c302618b2fae2-622862917.eu-west-1.elb.amazonaws.com    52773:31047/TCP  6m33s

    Vá para _http://a2c6f6733557511eab3c302618b2fae2-622862917.eu-west-1.elb.amazonaws.com:52773/csp/user/_DeepSee.UserPortal.Home.zen?$NAMESPACE=USER _(substitua o link pelo seu IP externo), então, digite “_system”, “SYS” e altere a senha padrão. Você deve ver vários painéis de Inteligência Empresarial (BI):

    Clique na seta de cada um para um mergulho mais profundo:

    Lembre-se, se você reiniciar um pod samples-bi, todas as suas alterações serão perdidas. Este é um comportamento intencional, pois esta é uma demonstração. Se você precisar de persistência, criei um exemplo no repositório github-gke-zpm-registry/k8s/statefulset.tpl.

    Quando terminar, basta remover tudo que você criou:

    $ git commit -m "Mr Proper [destroy eks]" --allow-empty
    $ git push

    Conclusão

    Neste artigo, substituímos o utilitário eksctl pelo Terraform para criar um cluster EKS. É um passo à frente para “codificar” toda a sua infraestrutura AWS.
    Mostramos como você pode facilmente implantar uma aplicação de demonstração com git push usando GitHub Actions e Terraform.
    Também adicionamos o kompose e um postStart hook de um pod à nossa caixa de ferramentas.
    Não mostramos a ativação do TLS neste momento. Essa é uma tarefa que realizaremos em um futuro próximo.

    0
    0 301