Despliegue continuo con Terraform y Kubernetes


travis

Introducción

En este artículo continuaremos donde lo dejamos la última vez Integración continua con Go usando Travis CI y Docker. Los archivos utilizados aquí pueden encontrarse AQUÍ. Crearemos nuestro clúster en DigitalOcean con un balanceador de carga, generaremos el archivo kubeconfig basado en los certificados proporcionados por Terraform en Travis, y finalmente crearemos un despliegue básico para validar que todo funciona.


DigitalOcean

Necesitamos crear un token para que Terraform pueda crear recursos utilizando la API de DO. Dirígete a tu cuenta, luego en el menú de la izquierda haz clic en API, deberías ver algo como esto: image Una vez allí, haz clic en “generate token” (dale un nombre significativo para ti), y asegúrate de que tenga permisos de escritura. image


Terraform

El siguiente paso es configurar el token para Terraform. Examinemos todos los archivos y veamos qué harán, pero primero proporcionaremos los secretos a nuestra aplicación a través de variables de entorno. Yo he encontrado útil usar direnv en muchos proyectos, por lo que el contenido del primer archivo .envrc se vería algo así:

export TF_VAR_DO_TOKEN=insertar_tu_token_aquí

Después de eso, necesitarás permitir su ejecución ejecutando direnv allow.


El primer archivo de Terraform que vamos a revisar es provider.tf:

# Configura el proveedor de DigitalOcean con su token
variable "DO_TOKEN" {}

provider "digitalocean" {
  token = "${var.DO_TOKEN}"
}

Como estamos utilizando variables de entorno, necesitamos declararlo y luego establecerlo en el proveedor. Por ahora, solo necesitamos el token.


Luego, el archivo kubernetes.tf:

# Crear el clúster
resource "digitalocean_kubernetes_cluster" "dev-k8s" {
  name    = "dev-k8s"
  region  = "nyc1"
  version = "1.14.1-do.2"

  node_pool {
    name       = "dev-k8s-nodes"
    size       = "s-1vcpu-2gb"
    node_count = 1
    tags       = ["dev-k8s-nodes"]
  }
}

Este archivo será el responsable de crear el clúster de Kubernetes. Como es nuestro clúster de desarrollo, solo necesitamos un nodo.


El siguiente archivo es lb.tf:

# Crear un balanceador de carga asociado a nuestro clúster
resource "digitalocean_loadbalancer" "public" {
  name   = "loadbalancer-1"
  region = "nyc1"

  forwarding_rule {
    entry_port     = 80
    entry_protocol = "http"

    target_port     = 30000
    target_protocol = "http"
  }

  healthcheck {
    port     = 30000
    protocol = "tcp"
  }

  droplet_tag = "dev-k8s-nodes"
}

Este archivo es particularmente interesante porque proporcionará un punto de acceso a nuestras aplicaciones (puerto 80 en su dirección IP pública), y también usa una verificación de estado básica.


Y finalmente, el archivo output.tf:

# Exportar el archivo de configuración de kubectl
resource "local_file" "kubernetes_config" {
  content  = "${digitalocean_kubernetes_cluster.dev-k8s.kube_config.0.raw_config}"
  filename = "kubeconfig.yaml"
}

# Imprimir la IP del balanceador de carga
output "digitalocean_loadbalancer" {
  value       = "${digitalocean_loadbalancer.public.ip}"
  description = "La dirección IP pública del balanceador de carga."
}

Este archivo imprimirá el archivo de configuración de Kubernetes que necesitamos para poder usar kubectl, y también la dirección IP del balanceador de carga.


¿Qué hacemos con todo esto? Primero necesitarás ejecutar terraform init dentro de la carpeta de terraform para descargar los plugins y proveedores. Una vez hecho esto, puedes ejecutar terraform plan para ver qué cambios desea hacer terraform o terraform apply para aplicarlos. ¿Cómo se verá esto?:

# Inicializar terraform
$ terraform init
...

# Aplicar
$ terraform apply
...

Esto creará nuestro clúster en DigitalOcean. Recuerda destruirlo después de usarlo con terraform destroy. Si no usas un plan, se te pedirá confirmación cuando ejecutes terraform apply; revisa y di .


Travis

Hicimos algunas adiciones a nuestro archivo .travis.yml, que son principalmente para preparar kubectl y también para desencadenar un despliegue si la compilación tiene éxito.

language: go

services:
  - docker

before_install:
- docker build --no-cache -t ${TRAVIS_REPO_SLUG}:${TRAVIS_COMMIT} .
- docker run ${TRAVIS_REPO_SLUG}:${TRAVIS_COMMIT} /go/src/github.com/kainlite/whatismyip-go/whatismyip-go.test
- docker run -d -p 127.0.0.1:8000:8000 ${TRAVIS_REPO_SLUG}:${TRAVIS_COMMIT}

script:
  - curl 127.0.0.1:8000
  - echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
  - docker push ${TRAVIS_REPO_SLUG}:${TRAVIS_COMMIT}

after_success:
  - echo ${KUBERNETES_CA} | base64 -d > k8s-ca.pem
  - echo ${KUBERNETES_CLIENT_CA} | base64 -d > k8s-client-ca.pem
  - echo ${KUBERNETES_CLIENT_KEY} | base64 -d > k8s-key.pem
  - curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
  - envsubst < ./kubernetes/manifest.yml.template > ./kubernetes/manifest.yml
  - chmod u+x ./kubectl
  - ./kubectl config set-cluster dev-k8s --server=${KUBERNETES_ENDPOINT} --certificate-authority=k8s-ca.pem
  - ./kubectl config set-credentials dev-k8s --client-certificate=k8s-client-ca.pem --client-key=k8s-key.pem
  - ./kubectl config set-context dev-k8s --cluster=dev-k8s --namespace=default --user=dev-k8s
  - ./kubectl config use-context dev-k8s
  - ./kubectl apply -f ./kubernetes/manifest.yml

Como se muestra en la imagen, tomamos los certificados codificados en base64 y los cargamos en Travis como variables de entorno (KUBERNETES_CA, KUBERNETES_CLIENT_CA, KUBERNETES_CLIENT_KEY, KUBERNETES_ENDPOINT), luego los decodificamos en archivos, creamos la configuración usando kubectl y la activamos, y luego aplicamos el despliegue con el hash generado.


Así es como debería verse en Travis: image

Veamos la configuración generada de Kubernetes y qué valores debes tener en cuenta:

apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...
...
  server: https://592d58c9-98c1-4285-b098-cbc9378e9f89.k8s.ondigitalocean.com
  name: do-nyc1-dev-k8s
contexts:
- context:
    cluster: do-nyc1-dev-k8s
    user: do-nyc1-dev-k8s-admin
  name: do-nyc1-dev-k8s
current-context: do-nyc1-dev-k8s
kind: Config
preferences: {}
users:
- name: do-nyc1-dev-k8s-admin
  user:
    client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...
...

Nunca hagas esto, no compartas tu configuración o cualquiera podrá usar tu clúster. También ten cuidado de no comprometerlo en tu repositorio. En este ejemplo ya no es válido porque, después de ejecutar los ejemplos, destruí el clúster con terraform destroy. Ahora, hay cuatro valores de interés para nosotros: certificate-authority-data: KUBERNETES_CA, client-certificate-data: KUBERNETES_CLIENT_CA, client-key-data: KUBERNETES_CLIENT_KEY, y server: KUBERNETES_ENDPOINT. Con estas variables, podemos recrear fácilmente nuestra configuración de Kubernetes usando kubectl. Ten en cuenta que no estamos dec

odificando para guardarlo en Travis; lo hacemos en el archivo de configuración de Travis (.travis.yml).


Kubernetes

Después de todo eso, aún necesitamos tener una plantilla de despliegue para desplegar nuestra aplicación, y es una plantilla porque necesitamos reemplazar el SHA de la compilación actual en el manifiesto antes de enviarlo a la API de Kubernetes. Revisemos el archivo manifest.yml.template:

---
apiVersion: v1
kind: Service
metadata:
  name: whatismyip-go-service
spec:
  type: NodePort
  ports:
  - port: 8000
    targetPort: 8000
    nodePort: 30000
  selector:
    app: whatismyip-go
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whatismyip-go-deployment
  namespace: default
  labels:
    app: whatismyip-go
spec:
  replicas: 1
  selector:
    matchLabels:
      app: whatismyip-go
  template:
    metadata:
      labels:
        app: whatismyip-go
    spec:
      containers:
        - name: whatismyip-go
          image: kainlite/whatismyip-go:${TRAVIS_COMMIT}
          ports:
            - containerPort: 8000
              name: http

Aquí exponemos nuestro servicio en el puerto 30000 como un NodePort y desplegamos el SHA actual (reemplazado durante la ejecución por Travis).


Probando todo

Valida que el despliegue se haya realizado correctamente verificando nuestro clúster de Kubernetes:

$ kubectl get pods --kubeconfig=./kubeconfig.yaml
NAME                                        READY   STATUS    RESTARTS   AGE
whatismyip-go-deployment-5ff9894d8c-f48nx   1/1     Running   0          9s

Primero probamos el balanceador de carga, y como veremos, la IP no es correcta, es la IP interna del balanceador de carga y no nuestra dirección IP pública.

$ curl -v 159.203.156.153
*   Trying 159.203.156.153...
* TCP_NODELAY set
* Connected to 159.203.156.153 (159.203.156.153) port 80 (#0)
> GET / HTTP/1.1
> Host: 159.203.156.153
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 05 May 2019 21:14:28 GMT
< Content-Length: 12
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host 159.203.156.153 left intact
10.136.5.237* Closing connection 0 

Pero si accedemos directamente a nuestro servicio, podemos ver la dirección IP correcta, esto podría mejorarse pero lo dejamos como ejercicio para el lector ◕_◕.

$ curl -v 142.93.207.200:30000
*   Trying 142.93.207.200...
* TCP_NODELAY set
* Connected to 142.93.207.200 (142.93.207.200) port 30000 (#0)
> GET / HTTP/1.1
> Host: 142.93.207.200:30000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 05 May 2019 21:35:13 GMT
< Content-Length: 12
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host 142.93.207.200 left intact
111.138.53.63* Closing connection 0  

Finalmente, verifiquemos lo que deberíamos ver en Travis: image


Como podemos ver, todo salió bien y nuestro despliegue se aplicó con éxito en nuestro clúster. image


Notas finales

Publicaré algunos artículos sobre CI y CD y buenas prácticas que los DevOps/SRE deben tener en cuenta, consejos, trucos y ejemplos completos de despliegue. Este es la segunda parte de una posible serie de tres artículos (el siguiente debería tratar lo mismo pero usando Jenkins) con un ejemplo básico pero completo de CI y luego CD. Esto, por supuesto, puede cambiar y cualquier comentario será muy apreciado :).

En este ejemplo, muchas cosas podrían mejorarse. Por ejemplo, usamos un puerto de nodo y no hay un firewall, por lo que podemos acceder a nuestra aplicación directamente a través del NodePort o usando el balanceador de carga. Deberíamos agregar algunas reglas de firewall para que solo el balanceador de carga pueda comunicarse con el rango de puertos del NodePort (30000-32767).

También ten en cuenta que para producción, esta configuración no será suficiente, pero para un entorno de desarrollo sería suficiente inicialmente.

Algunos enlaces útiles para travis y terraform.


Errata

Si encuentras algún error o tienes alguna sugerencia, por favor envíame un mensaje para que se corrija.



No tienes cuenta? Regístrate aqui

Ya registrado? Iniciar sesión a tu cuenta ahora.

Iniciar session con GitHub
Iniciar sesion con Google
  • Comentarios

    Online: 0

Por favor inicie sesión para poder escribir comentarios.

by Gabriel Garrido