7.6 Definindo o chart utilizado para execução local e na nuvem

Vamos definir um chart para executarmos a aplicação, inicialmente localmente, e posteriormente na nuvem da Oracle. Para isso, vamos criar o diretório chart na raíz do nosso projeto, e acrescentar nele Chart.yaml, contendo o seguinte conteúdo:

apiVersion: v2
name: loja-virtual
description: Chart para implantação da loja virtual no Kubernetes

type: application
version: 1.0.0
appVersion: 1.0

Nele, definimos o nome do nosso chart (name: loja-virtual) e sua versão (version: 1.0.0), elaboramos uma descrição (description: Chart para implantação da loja virtual no Kubernetes), definimos seu tipo como aplicação (type: application) e informamos a versão da aplicação que ele instala (appVersion: 1.0).

Uma boa prática consiste em definir um arquivo chamado .helmignore dentro do diretório chart. Esse arquivo informa ao helm padrões de arquivos que devem ser ignorados. Cada linha desse arquivo deve conter um único padrão, tal como no arquivo .gitignore dos repositórios git. Vamos usar o seguinte arquivo:

# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

Nele podemos ver que diretórios e arquivos comuns às diversas IDEs existentes são ignorados, como por exemplo os diretório .vscode, referente à IDE Visual Studio Code, e o diretório .idea, referente à IDE IntelliJ, bem como arquivos comuns de backup (aqueles com extensão .bak e .swp) e arquivos de sistemas de controle de versão como o próprio .gitignore.

Na sequencia, é necessário criar o subdiretório templates dentro do diretório chart, que como visto anteriormente, deve conter os templates utilizados para gerar os arquivos de recursos do Kubernetes.

Vamos inicialmente criar nosso primeiro deployment, no arquivo loja-deployment.yaml. Ele terá o seguinte formato:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: loja
  labels:
    app.kubernetes.io/name: loja-virtual
    app.kubernetes.io/instance: loja-virtual
    app.kubernetes.io/version: '1.0'
    app.kubernetes.io/component: webserver
    app.kubernetes.io/part-of: loja-virtual
    app.kubernetes.io/managed-by: Helm 
    app.kubernetes.io/created-by: curso-devops
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: loja-virtual
      app.kubernetes.io/instance: loja-virtual
      app.kubernetes.io/version: '1.0'
      app.kubernetes.io/component: webserver
      app.kubernetes.io/part-of: loja-virtual
  template:
    metadata:
      labels:
        app.kubernetes.io/name: loja-virtual
        app.kubernetes.io/instance: loja-virtual
        app.kubernetes.io/version: '1.0'
        app.kubernetes.io/component: webserver
        app.kubernetes.io/part-of: loja-virtual
        app.kubernetes.io/managed-by: Helm 
        app.kubernetes.io/created-by: curso-devops
    spec:
      imagePullSecrets:
      containers:
      - name: loja
        image: registry-local:5005/loja-virtual
        imagePullPolicy: IfNotPresent
        ports:
          - name: http
            containerPort: 8080
            protocol: TCP   

É interessante observar que na linha 4 definimos o nome do deployment (loja-virtual), e entre as linhas 6 e 12 definimos os labels dele, usando os labels recomendados pela documentação do Kubernetes (https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/). Os labels, como dito anteriormente, ajudam a classificar e agrupar os recursos.

Os mesmos labels são utilizados nas linhas 25-31, e são aplicados nesse caso aos containers criados pelo deployment. No exemplo temos apenas um container, com o nome loja (linha 35), que usa a imagem registry-local:5005/loja-virtual (linha 36) e somente tenta baixar a imagem do registry se ela não estiver presente ainda no cluster Kubernetes (IfNotPresent, na linha 37).

Nosso container permite a conexão externa ao POD por meio da porta 8080 (linha 40), por meio do protocolo TCP (linha 41), e a essa porta foi atribuído o nome arbitrário de http (linha 39). Para o deployment saber quais são os containers que são gerenciados por ele, definimos nas linhas 17-21 os labels que serão utilizados para selecionar os respectivos containers. No nosso caso, estamos usando apenas 4 labels, em vez das 6 definidas para os containers. Poderíamos usar todas as 6, ou qualquer número entre 1 e 5. O importante nesse caso é selecionar as labels que identifiquem univocamente os containers que devem ser gerenciados pelo deployment.

Com a definição desse deployment, podemos testar o deploy no cluster Kubernetes usando o Helm. Mas primeiro, é necessário realizar o build da imagem da aplicação, e seu envio para o registry (registry-local:5005). Esses dois passos podem ser realizados pelo seguinte comando, o qual deve ser executado na raíz do projeto:

docker build -t registry-local:5005/loja-virtual-base . && \
docker push registry-local:5005/loja-virtual-base

Na linha 1 usamos o comando build do docker para criar a tag registry-local:5005/loja-virtual, por meio do parâmetro -t, e informamos que o contexto utilizado será o diretório atual (.).

Já na linha 2, o comando push é utilizado para enviar a imagem gerada para o registry. Com isso, nossa imagem poderá ser baixada pelo Kubernetes para gerar o container que definimos no deployment.

O arquivo Dockerfile presente na raiz é utilizado para formar a imagem base da nossa aplicação. Ela basicamente contém um servidor Tomcat, na versão 7.0, e o jdk8. A imagem que será efetivamente utilizada no cluster é gerada com base nela, e contém também a aplicação da loja virtual. Essa imagem é gerada pelo Jib.

Agora, é possível usar o Helm para realizar o deploy no kubernetes com o seguinte comando:

helm install chart --create-namespace -n loja-virtual --name-template loja-virtual

O comando install é responsável por instalar um chart no cluster Kubernetes. Deve ser informado o diretório no qual está localizado o chart. No nosso exemplo, o diretório tem o nome chart.

Vamos instalar o chart em um namespace chamado loja-virtual, definido pelo parâmetro -n. Esse namespace ainda não existe, por isso, utilizamos o comando --create-namespace que cria o namespace caso ele ainda não exista. O Helm criará uma nova release no Kubernetes, cujo nome loja-virtual, é definido por meio do parâmetro --name-template.

A saida desse comando será semelhante a essa:

WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /home/bruno/.kube/config
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /home/bruno/.kube/config
NAME: loja-virtual
LAST DEPLOYED: Mon Dec 13 10:49:43 2021
NAMESPACE: loja-virtual
STATUS: deployed
REVISION: 1
TEST SUITE: None

Podemos observar que o status é deployed, indicando que o foi realizado o deploy no cluster Kubernetes. Isso nos indica que nosso deployment está correto. Podemos verificar o log do container com o comando a seguir:

kubectl logs deployment/loja -c loja -n loja-virtual 

O kubectl, como mencionado anteriormente é uma aplicação de linha de comando que permite interagirmos com o cluster Kubernetes. Utilizamos o comando logs para informar que queremos obter os logs do deployment loja (deployment/loja), de maneira mais específica do container loja (-c loja), que está localizado no namespace loja-virtual (-n loja-virtual).

Para usar nossa aplicação no navegador, é necessário mapear a porta do contêiner em uma porta local da nossa máquina. Esse procedimento é realizado utilizando o comando a seguir:

kubectl port-forward deployment/loja 8080:8090

Com esse comando, estamos mapeando a porta 8080 do contêiner na porta 8090 da nosso máquina. Desse modo, no navegador, conseguimos acessar nossa aplicação com o endereço http://localhost:8090.

//TODO: Explicar que falta o banco de dados

Como podemos observar, nosso arquivo de template ainda está estático, assim como os arquivos do Kubernetes que criamos na seção 7.3. Vamos ver agora 3 modos de deixar o arquivo mais dinâmico, com a utilização de valores definidos conforme necessidade.

Valores definidos no arquivo values.yaml

Podemos melhorar isso com a criação do arquivo values.yaml dentro do diretório chart. Esse arquivo, como mencionamos, será utilizado para definir chaves com valores-padrão que serão utilizadas e substituídas nos templates.

Inicialmente ele será assim:

replicaCount: 1

image:
  repository: nginx
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: ""
imagePullPolicy: IfNotPresent
imagePullSecrets: 
  - name: gitlab-registry
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  # Specifies whether a service account should be created
  create: false
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: ""

podAnnotations: {}

podSecurityContext: {}
  # fsGroup: 2000

securityContext: {}
  # capabilities:
  #   drop:
  #   - ALL
  # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000

service:
  type: ClusterIP
  port: 8080

ingress:
  enabled: true
  className: ""
  annotations: 
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
  hosts:
    - host: loja-disciplina-devops.tk
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

resources: {}
  # We usually recommend not to specify default resources and to leave this as a conscious
  # choice for the user. This also increases chances charts run on environments with little
  # resources, such as Minikube. If you do want to specify resources, uncomment the following
  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
  # limits:
  #   cpu: 100m
  #   memory: 128Mi
  # requests:
  #   cpu: 100m
  #   memory: 128Mi

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80

nodeSelector: {}

tolerations: []

affinity: {}

Uma vez definido esses valores, podemos utilizá-los nos arquivos de templates. A utilização será realizada usando a seguinte sintaxe:

{{ .Values.CHAVE_DESEJADA }}

Por exemplo, podemos definir a porta utilizada no contêiner do seguinte modo:

containerPort: {{ .Values.service.port }}

Essa notação indica que deve ser utilizado o valor definido no arquivo values.yaml (.Values) por meio da sub-chave port, definida aninhada na chave service (service.port). Olhando novamente o arquivo values.yaml vemos que esse valor é 8080:

service:
  type: ClusterIP
  port: 8080

Portanto, o resultado do template será containerPort: 8080, que é exatamente o valor que queremos.

Valores definidos no arquivo charts.yaml

Outra substituição que podemos fazer, é utilizar informações do arquivo Chart.yaml. De modo análogo, podemos definir, por exemplo, o nome do continer do seguinte modo:

containers:
    - name: {{ .Chart.Name }}

Nesse caso, dada a definição a seguir presente no arquivo Chart.yaml:

name: loja-virtual

Teremos o seguinte arquivo resultante da substituição de valores no arquivo de template:

containers:
    name: loja-virtual

Valores definidos com named templates no arquivo _helpers.tpl

Um recurso interessante do Helm é a utilização de named templates. Named templates são templates definidos em um arquivo, aos quais é atribuído um nome, e que podem ser utilizados em outros templates, de modo semelhante a uma função ou método em programação. É um modo de reutilizar código.

Podemos definir named templates no arquivo _helpers.tpl, dentro do diretório templates. A seguir, temos a definição do named template chart.name:

{{/*
Expand the name of the chart.
*/}}
{{- define "chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

No arquivo _helpers.tpl, usamos sempre blocos que iniciam com {{ e são finalizados com }}. Podemos definir comentários entre os símbolos /* e */.

A definição de um named template pode ser realizada com a palavra define, tal como na linha 4 do código anterior, no qual definimos o named template chart.name. Podem ser usadas funções tal como na linha 5, onde são usadas as funções default, trunc e trimSuffix. As funções disponíveis no Helm podem ser conferidas na documentação.

Na linha 5, estamos definindo que vamos utilizar o valor definido pela chave nameOverride, definida no arquivo values.yaml. caso ela exista, caso contrário, o valor padrão (default) será o valor da chave Name, presente no arquivo chart.yaml. A seguir, o valor será utilizado como parâmetro para a função trunc, por meio da utilização do pipe (|).

A função trunc retorna somente os N primeiros caracteres de um texto. No exemplo anterior, o valor de N foi definido como 63. A seguir, o valor obtido é passado como parâmetro para a função trimSuffix, que remove o sufixo informado como parâmetro, no caso -. O valor resultante, pode ser utilizado nos arquivos de templates com a palavra include, do seguinte modo:

name: {{ include "chart.name" . }}

O nosso arquivo _helpers.tpl completo ficará assim:

{{/*
Expand the name of the chart.
*/}}
{{- define "chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "chart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "chart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels
*/}}
{{- define "chart.labels" -}}
helm.sh/chart: {{ include "chart.chart" . }}
{{ include "chart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels
*/}}
{{- define "chart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Create the name of the service account to use
*/}}
{{- define "chart.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "chart.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}


{{- define "db.labels" -}}
helm.sh/chart: {{ include "chart.chart" . }}
{{ include "db.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}


{{/*
Selector labels
*/}}
{{- define "db.selectorLabels" -}}
app.kubernetes.io/name: {{ .Values.db.name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

Podemos testar o template do deployment, com a substituição de variáveis com o seguinte comando:

helm upgrade loja-virtual ./chart -n loja-virtual

Esse comando vai atualizar a versão do chart de acordo com as alterações que realizamos. Para acesarmos pelo navegador teremos que repetir o comando para mapear a porta do contêiner em uma porta da nossa máquina:

kubectl port-forward deployment/loja 8080:8090

O próximo passo agora é definir o template para criar um serviço para nossa aplicação. Para isso, será criado o arquivo service.yaml dentro do diretório templates. Ele ficará do seguinte modo:

apiVersion: v1
kind: Service
metadata:
  name: {{ include "chart.fullname" . }}
  labels:
    {{- include "chart.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: http
  selector:
    {{- include "chart.selectorLabels" . | nindent 4 }}

Note que assim como fizemos no caso do deployment, estamos também usando valores provenientes do arquivo values.yaml, tal como em:

type: {{ .Values.service.type }}

Também são utilizados named templates como no caso a seguir:

selector:
    {{- include "chart.selectorLabels" . | nindent 4 }}

Observe que podemos utilizar o named template como entrada para uma função. No exemplo apresentado, o named template chart.selectorLabels é usado como parâmetro para a função nident, que realiza a identação colocando N número de espaços antes do valor do parâmetro. No nosso caso, são 4 espaços.

Podemos atualizar novamente nossa release, com o comando já apresentado, e na sequencia realizar o mapeamento agora de uma porta do serviço para uma porta da nossa máquina:

kubectl port-forward service/loja-virtual 8080:8090

Observe que o resultado prático foi o mesmo de fazer o mapeamento para o deployment, porém, se tivéssemos mais de uma réplica do deployment, ao acessarmos o endereço pelo navegador, poderíamos a cada momento ser atendidos por uma instância diferente da aplicação.

Essa é uma das vantagens de utilizar services, conforme explicado anteriormente.

Ainda falta uma peça nesse quebra-cabeça para que nossa aplicação funcione adequadamente, que é o banco de dados.

Como o banco de dados é uma dependência de nossa aplicação, podemos criar um subchart para implementar o banco. A vantagem de fazer isso é ter uma estrutura de arquivos mais organizada. Para criar o subchart do banco de dados, vamos criar o diretório charts/mysql dentro do diretório chart, e dentro dele vamos ter uma estrutura semelhante à do chart:

chart
├── charts
│   └── mysql
│       ├── Chart.yaml
│       ├── templates
│       │   ├── db-deployment.yaml
│       │   ├── db-service.yaml
│       │   └── _helpers.tpl
│       └── values.yam

O arquivo Chart.yaml ficará assim:

apiVersion: v2
name: mysql
description: Banco de dados da aplicação
type: application
version: 0.1.0
appVersion: "1.16.0"

Já o arquivo values.yaml será desse modo:

db:
  name: db
  labels:
    helm.sh/chart: chart-0.1.0
    app.kubernetes.io/name: chart
    app.kubernetes.io/instance: RELEASE-NAME
    app.kubernetes.io/version: "1.16.0"
    app.kubernetes.io/managed-by: Helm 
    test: true
  selectorLabels:
    test: true
  
  replicaCount: 1

  image: aurimrv/mysql-server-img
    # repository: nginx
    # pullPolicy: IfNotPresent
    # # Overrides the image tag whose default is the chart appVersion.
    # tag: ""
  imagePullPolicy: IfNotPresent
  imagePullSecrets: []
  nameOverride: ""
  fullnameOverride: ""

  serviceAccount:
    # Specifies whether a service account should be created
    create: "false"
    # Annotations to add to the service account
    annotations: {}
    # The name of the service account to use.
    # If not set and create is true, a name is generated using the fullname template
    name: ""

  podAnnotations: {}

  podSecurityContext: {}
    # fsGroup: 2000

  securityContext: {}
    # capabilities:
    #   drop:
    #   - ALL
    # readOnlyRootFilesystem: true
    # runAsNonRoot: true
    # runAsUser: 1000

  service:
    type: ClusterIP
    port: 3306

  
  resources:
    # We usually recommend not to specify default resources and to leave this as a conscious
    # choice for the user. This also increases chances charts run on environments with little
    # resources, such as Minikube. If you do want to specify resources, uncomment the following
    # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
    limits:
      cpu: 400m
      memory: 256Mi
    requests:
      cpu: 100m
      memory: 128Mi

  autoscaling:
    enabled: false
    minReplicas: 1
    maxReplicas: 100
    targetCPUUtilizationPercentage: 80
    # targetMemoryUtilizationPercentage: 80

  nodeSelector: {}


  tolerations: []

  affinity: {}

O arquivo _helpers.tpl terá o seguinte conteúdo:

{{- define "db.labels" -}}
helm.sh/chart: {{ include "chart.chart" . }}
{{ include "db.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}


{{/*
Selector labels
*/}}
{{- define "db.selectorLabels" -}}
app.kubernetes.io/name: {{ .Values.db.name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

E os arquivos db-deployment.yaml e db-service.yaml ficarão respectivamente assim:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.db.name }}
  labels:
{{- include "db.labels" . | nindent 4 }}
spec:
{{- if not .Values.db.autoscaling.enabled }}
  replicas: {{ .Values.db.replicaCount }}
{{- end }}
  selector:
    matchLabels:
{{- include "db.selectorLabels" . | nindent 6}}
  template:
    metadata:
      {{- with .Values.db.podAnnotations }}
      annotations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      labels:
{{- include "db.selectorLabels" . | nindent 8}}
    spec:
      {{- with .Values.db.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      securityContext:
        {{- toYaml .Values.db.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Values.db.name }}
          securityContext:
            {{- toYaml .Values.db.securityContext | nindent 12 }}
          image: "{{ .Values.db.image }}"
          imagePullPolicy: {{ .Values.db.imagePullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.db.service.port}}
              protocol: TCP
          # livenessProbe:
          #   httpGet:
          #     path: /
          #     port: http
          # readinessProbe:
          #   httpGet:
          #     path: /
          #     port: http
          resources:
            {{- toYaml .Values.db.resources | nindent 12 }}
      {{- with .Values.db.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.db.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.db.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.db.name }}
  labels:
{{- include "db.labels" . | nindent 4 }}
spec:
  type: {{ .Values.db.service.type }}
  ports:
    - port: {{ .Values.db.service.port }}
      targetPort: http
      protocol: TCP
      name: http
  selector:
    {{- include "db.selectorLabels" . | nindent 4 }}

Observe que nossa aplicação acessará o banco de dados por meio do nome do serviço, segundo nosso exemplo, esse nome será db. E a porta será a 3306.

Last updated