Cree una API TODO para Golang con Kubernetes

¡Hola a todos! Antes del lanzamiento del curso de la Plataforma de Infraestructura basada en Kubernetes, preparamos una traducción de otro material interesante.



Este artículo está dirigido a los recién llegados a Kubernetes, que estarán interesados ​​en comprender un ejemplo práctico de cómo escribir una API de Golang para administrar la lista TODO, y luego cómo implementarla en Kubernetes.

Cada desarrollador ama una buena lista de tareas pendientes, ¿verdad? ¿De qué otra manera podríamos organizarnos de otra manera?


Todo desarrollador ama una buena aplicación TODO, ¿verdad?

Comenzaremos revisando la lista de componentes necesarios, luego configuraremos Kubernetes, proporcionaremos una base de datos Postgresql y luego instalaremos un marco de aplicación que nos ayudará a implementar fácilmente la API Go en Kubernetes, sin centrarnos en los detalles.

Crearemos dos puntos finales en la API: uno para crear un nuevo registro TODO y el otro para seleccionar todos los registros TODO.

Todo el código de este tutorial está disponible en GitHub.

La lista de componentes necesarios:


  • Docker instalado localmente

Kubernetes ejecuta código en imágenes de contenedor, por lo que debe instalar Docker en su computadora.

Instale Docker: https://www.docker.com
Registre una cuenta de Docker Hub para almacenar sus imágenes de Docker: https://hub.docker.com/

  • Cluster Kubernetes


Puede elegir un clúster local o remoto, pero ¿cuál es mejor? Las opciones simplificadas, como k3d, funcionan en cualquier computadora en la que Docker pueda ejecutarse, por lo que ejecutar un clúster local ya no requiere mucha RAM. Un clúster remoto también puede ser una solución muy eficiente, pero tenga en cuenta que todas sus imágenes de Docker deberán cargarse y cargarse para cada cambio.

Instale k3d: https://github.com/rancher/k3d

k3d no instalará kubectl (esta es la CLI para Kubernetes), así que instálelo por separado desde aquí: https://kubernetes.io/docs/tasks/tools / install-kubectl

  • Golang


También necesita instalar Golang con el IDE en su computadora. Go es gratuito y puede descargarlo para MacOS, Windows o Linux desde el siguiente enlace: https://golang.org/dl

  • IDE


Recomendaría usar Visual Studio Code, es gratis y tiene un conjunto de complementos para Go que puede agregar. Algunos colegas prefieren Goland de Jetbrains. Si eres un programador de Java, probablemente prefieras pagar por Goland, ya que te recordará a sus otros productos.

Instale VSCode o Golang .

Crea un clúster


Debe instalar todo el software especificado en la sección anterior.

Cree un nuevo clúster con k3d:


k3d create

Configure kubectl para que apunte a un nuevo clúster. Recuerde que se pueden usar varios clústeres desde una computadora, por lo que es extremadamente importante señalar la correcta:

export KUBECONFIG="$(k3d get-kubeconfig --name='k3s-default')"


Asegúrese de que el clúster tenga al menos un nodo. Aquí puede ver que tengo instalado Kubernetes 1.17, una versión relativamente nueva:

kubectl get node
NAME                     STATUS   ROLES    AGE   VERSION
k3d-k3s-default-server   Ready    master   48s   v1.17.0+k3s.1


Almacenaremos nuestros registros TODO en la tabla de la base de datos, por lo que ahora tenemos que instalarlo. Postgresql es una base de datos relacional popular que podemos instalar en un clúster usando el diagrama Helm.

Instalar arkade


arkade es un CLI Go, similar a "brew" o "apt-get", pero para aplicaciones de Kubernetes. Utiliza un proyecto Helm, kubectl o CLI para instalar un proyecto o producto en su clúster.

curl -sLS https://dl.get-arkade.dev | sudo sh


Ahora instale Postgresql

arkade install postgresql
===================================================================== = PostgreSQL has been installed.                                    =
=====================================================================


También verá información sobre la cadena de conexión y cómo iniciar la CLI de Postgresql a través de la imagen de Docker dentro del clúster.

Puede obtener esta información en cualquier momento utilizando arkade info postgresql.

Vamos a diseñar el diseño de la mesa.

CREATE TABLE todo (
 id              INT GENERATED ALWAYS AS IDENTITY,
 description     text NOT NULL,
 created_date    timestamp NOT NULL,
 completed_date  timestamp NOT NULL
);


Ejecute arkade info postgresqlpara obtener información de conexión nuevamente. Debería verse más o menos así:

export POSTGRES_PASSWORD=$(kubectl get secret --namespace default postgresql -o jsonpath="{.data.postgresql-password}" | base64 --decode)
kubectl run postgresql-client --rm --tty -i --restart='Never' --namespace default --image docker.io/bitnami/postgresql:11.6.0-debian-9-r0 --env="PGPASSWORD=$POSTGRES_PASSWORD" --command -- psql --host postgresql -U postgres -d postgres -p 5432


Ahora tiene en el símbolo del sistema : postgres = #, puede crear la tabla copiándola adentro, luego ejecutar \dtpara mostrar la tabla:

postgres=# \dt
List of relations
Schema | Name | Type  |  Owner
--------+------+-------+----------
public | todo | table | postgres
(1 row)


Instalar el marco de la aplicación


Así como los desarrolladores de PHP aceleraron su flujo de trabajo usando LAMP (Linux Apache + Mysql + PHP) y los desarrolladores de Rails usando una pila preparada, los desarrolladores de Kubernetes pueden usar marcos de aplicaciones.
La pila PLONK significa Prometheus, Linux, OpenFaaS, NATS y Kubernetes.
  • Prometheus proporciona métricas, escalado automático y observabilidad para probar el estado de su sistema, le permite responder a los aumentos repentinos de la demanda y reducir los costos al proporcionar métricas para tomar decisiones sobre el escalado a cero.
  • Linux, aunque no es la única opción para ejecutar cargas de trabajo en Kubernetes, es estándar y más fácil de usar.
  • Inicialmente, OpenFaaS proporcionó funciones portátiles a los desarrolladores, pero funciona muy bien al implementar API y microservicios. Su versatilidad significa que cualquier contenedor Docker con un servidor HTTP se puede implementar y administrar.
  • NATS es un popular proyecto CNCF utilizado para mensajería y pub / sub. En la pila PLONK, proporciona la capacidad de ejecutar solicitudes de forma asincrónica y para hacer cola.
  • Kubernetes es la razón por la que estamos aquí. Proporciona una infraestructura escalable, autorreparable y declarativa. Y si ya no necesita parte de toda esta grandeza, su API se simplifica con OpenFaaS.


Instale la pila PLONK a través de arkade:

arkade install openfaas


Lea el mensaje informativo y ejecute cada comando:

  • Instalar faas-cli.
  • Obtenga su contraseña
  • Redireccionar la interfaz de usuario de la puerta de enlace OpenFaaS (a través del puerto 8080)
  • E inicie sesión en el sistema a través de la CLI.


Como antes, puede recibir un mensaje informativo usando info openfaas.

Implemente su API First Go


Hay varias formas de crear una API Go con PLONK. La primera opción es utilizar el Dockerfile y determinar manualmente el puerto TCP, la comprobación de estado, el servidor HTTP, etc. Esto se puede hacer usando faas-cli new --lang dockerfile API_NAME, pero hay una manera más simple y más automatizada.

La segunda forma es usar las plantillas integradas que ofrece Function Store:

faas-cli template store list | grep go
go                       openfaas           Classic Golang template
golang-http              openfaas-incubator Golang HTTP template
golang-middleware        openfaas-incubator Golang Middleware template


Dado que queremos crear una API tradicional de estilo HTTP, la plantilla golang-middleware sería la más adecuada.

Al comienzo del tutorial, se registró con su cuenta de Docker Hub para almacenar sus imágenes de Docker. Cada carga de trabajo de Kubernetes debe integrarse en una imagen de Docker antes de poder desplegarse.

Levante el patrón especial:

faas-cli template store pull golang-middleware


Use Scaffold para su API con golang-middleware y su nombre de usuario en Docker Hub:

export PREFIX=alexellis2
export LANG=golang-middleware
export API_NAME=todo
faas-cli new --lang $LANG --prefix $PREFIX $API_NAME


Verá dos archivos generados:

  • ./todo.yml - proporciona una forma de configurar, implementar e instalar la plantilla y el nombre
  • ./todo/handler.go - aquí escribe el código y agrega cualquier otro archivo o paquete que necesite


Vamos a editar un poco y luego expandir el código.

package function
import (
   "net/http"
   "encoding/json"
)
type Todo struct {
   Description string `json:"description"`
}
func Handle(w http.ResponseWriter, r *http.Request) {
   todos := []Todo{}
   todos = append(todos, Todo{Description: "Run faas-cli up"})
   res, _ := json.Marshal(todos)
   w.WriteHeader(http.StatusOK)
   w.Header().Set("Content-Type", "application/json")
   w.Write([]byte(res))
}


Si no usa VScode y sus complementos para editar y formatear el código, ejecútelo después de cada cambio para asegurarse de que el archivo esté formateado correctamente.

gofmt -w -s ./todo/handler.go


Ahora implemente el código: primero cree una nueva imagen, inícielo en Docker Hub e impleméntelo en el clúster utilizando la API OpenFaaS:

Invoke your endpoint when ready:
curl http://127.0.0.1:8080/function/todo
{
"description": "Run faas-cli up"
}


Permitir a los usuarios crear nuevos registros TODO


Ahora permitamos que los usuarios creen nuevas entradas en su lista TODO. Primero debe agregar el enlace o "dependencia" para Ir a la biblioteca Postgresql.

Podemos obtenerlo utilizando los módulos vending o Go que se introdujeron en Go 1.11 y se instalaron de forma predeterminada en Go 1.13.

Edite handler.goy agregue el módulo que necesitamos para acceder a Postgresql:

import (
   "database/sql"
   _ "github.com/lib/pq"
...


Para que los módulos Go detecten dependencias, debemos declarar algo dentro del archivo, que usaremos más adelante. Si no lo hacemos, VSCode eliminará estas líneas cuando se guarden.

Agregue esto debajo de las importaciones en el archivo

var db *sql.DB


Siempre ejecute estos comandos en la todocarpeta donde se encuentra handler.go, y no en el nivel raíz c todo.yml.

Inicialice el nuevo módulo Go:

cd todo/
ls
handler.go
export GO111MODULE=on
go mod init


Ahora actualice el archivo usando la biblioteca pq:

go get
go mod tidy
cat go.mod
module github.com/alexellis/todo1/todo
go 1.13
require github.com/lib/pq v1.3.0


Lo que sea que esté adentro go.mod, copie su contenido aGO_REPLACE.txt

cat go.mod > GO_REPLACE.txt


Ahora asegurémonos de que el ensamblaje aún funcione antes de agregar el código de inserción.

faas-cli build -f todo.yml --build-arg GO111MODULE=on


Puede notar que ahora pasamos --build-arga informar un patrón para usar los módulos Go.

Durante el ensamblaje, verá que los módulos se descargan según sea necesario desde Internet.

Step 16/29 : RUN go test ./... -cover
---> Running in 9a4017438500
go: downloading github.com/lib/pq v1.3.0
go: extracting github.com/lib/pq v1.3.0
go: finding github.com/lib/pq v1.3.0
?       github.com/alexellis/todo1/todo [no test files]
Removing intermediate container 9a4017438500


Configurar secretos para el acceso Postgresql


Podemos crear un grupo de conexiones init (), un método que solo se ejecutará una vez cuando se inicie el programa.

// init       .   ,     ,   /   /

func init() {
       if _, err := os.Stat("/var/openfaas/secrets/password"); err == nil {
               password, _ := sdk.ReadSecret("password")
               user, _ := sdk.ReadSecret("username")
               host, _ := sdk.ReadSecret("host")
               dbName := os.Getenv("postgres_db")
               port := os.Getenv("postgres_port")
               sslmode := os.Getenv("postgres_sslmode")
               connStr := "postgres://" + user + ":" + password + "@" + host + ":" + port + "/" + dbName + "?sslmode=" + sslmode
var err error
               db, err = sql.Open("postgres", connStr)
               if err != nil {
                       panic(err.Error())
               }
               err = db.Ping()
               if err != nil {
                       panic(err.Error())
               }
       }
}


Como notó, se extrae información de lo que se os.Getenvlee del entorno. Consideraría que estos valores no son confidenciales, se establecen en el archivo todo.y.

El resto, como la contraseña y el host, que son confidenciales, se guardan en los secretos de Kubernetes .

Puede crearlos a través faas-cli secret createo a través kubectl create secret generic -n openfaas-fn.

La cadena sdk.ReadSecretviene de OpenFaaS Nube SDK, con la siguiente importación: github.com/openfaas/openfaas-cloud/sdk. Lee un archivo secreto del disco y devuelve un valor o error.

Obtenga los valores secretos de arkade info postgresql.

Ahora para cada contraseña, haga lo siguiente:

export POSTGRES_PASSWORD=$(kubectl get secret --namespace default postgresql -o jsonpath="{.data.postgresql-password}" | base64 --decode)
export USERNAME="postgres"
export PASSWORD=$POSTGRES_PASSWORD
export HOST="postgresql.default"
faas-cli secret create username --from-literal $USERNAME
faas-cli secret create password --from-literal $PASSWORD
faas-cli secret create host --from-literal $HOST


Verifique los secretos de disponibilidad y cumplimiento:

faas-cli secret ls
NAME
username
password
host
# And via kubectl:
kubectl get secret -n openfaas-fn
NAME                  TYPE                                  DATA   AGE
username              Opaque                                1      13s
password              Opaque                                1      13s
host                  Opaque                                1      12s


Edite nuestro archivo YAML y agregue lo siguiente:

secrets:
   - host
   - password
   - username
   environment:
     postgres_db: postgres
     postgres_sslmode: "disable"
     postgres_port: 5432


A continuación, actualice los módulos Go y ejecute la compilación nuevamente:

cd todo
go get
go mod tidy
cd ..
faas-cli build -f todo.yml --build-arg GO111MODULE=on
Successfully built d2c609f8f559
Successfully tagged alexellis2/todo:latest
Image: alexellis2/todo:latest built.
[0] < Building todo done in 22.50s.
[0] Worker done.
Total build time: 22.50s


El ensamblaje funcionó como se esperaba, así que vamos a ejecutarlo faas-clicon los mismos argumentos para comenzar y desplegar la imagen. Si las credenciales y la configuración de SQL son correctas, no veremos errores en los registros, sin embargo, si son incorrectos, obtendremos el código de pánico en init ().

Verifique los registros:

faas-cli logs todo
2020-03-26T14:10:03Z Forking - ./handler []
2020-03-26T14:10:03Z 2020/03/26 14:10:03 Started logging stderr from function.
2020-03-26T14:10:03Z 2020/03/26 14:10:03 Started logging stdout from function.
2020-03-26T14:10:03Z 2020/03/26 14:10:03 OperationalMode: http
2020-03-26T14:10:03Z 2020/03/26 14:10:03 Timeouts: read: 10s, write: 10s hard: 10s.
2020-03-26T14:10:03Z 2020/03/26 14:10:03 Listening on port: 8080
2020-03-26T14:10:03Z 2020/03/26 14:10:03 Metrics listening on port: 8081
2020-03-26T14:10:03Z 2020/03/26 14:10:03 Writing lock-file to: /tmp/.lock


Si bien todo se ve bien, ahora intentemos llamar al punto final:

echo | faas-cli invoke todo -f todo.yml
2020-03-26T14:11:02Z 2020/03/26 14:11:02 POST / - 200 OK - ContentLength: 35


Entonces, ahora hemos establecido una conexión exitosa a la base de datos, y podemos realizar la inserción. ¿Cómo lo sabemos? Debido a que db.Ping ()devuelve un error, de lo contrario habría generado pánico:

err = db.Ping()
if err != nil {
   panic(err.Error())
}


Siga el enlace para obtener más detalles sobre el paquete de base de datos / sql .

Escribiendo el código de inserción


Este código inserta una nueva fila en la tabla todoy utiliza una sintaxis especial en la que el valor no está entre comillas, sino que se reemplaza por el código db.Query. En los "viejos tiempos" de la programación de LAMP, un error común que llevó a muchos sistemas a volverse inseguros fue la falta de saneamiento de los datos de entrada y la concatenación de la entrada del usuario directamente en la instrucción SQL.

Imagina a alguien ingresando una descripción; drop table todoNo sería muy divertido

Por lo tanto, ejecutamos db.Query, luego pasamos la instrucción SQL usando $1, $2etc. para cada valor, y luego podemos obtener el resultado y / o error. También debemos cerrar este resultado, así que use diferir para esto.

func insert(description string) error {
       res, err := db.Query(`insert into todo (id, description, created_date) values (DEFAULT, $1, now());`,
               description)
       if err != nil {
               return err
       }
       defer res.Close()
       return nil
}


Ahora conectemos esto al código.

func Handle(w http.ResponseWriter, r *http.Request) {
       if r.Method == http.MethodPost && r.URL.Path == "/create" {
               defer r.Body.Close()
               body, _ := ioutil.ReadAll(r.Body)
               if err := insert(string(body)); err != nil {
                       http.Error(w, fmt.Sprintf("unable to insert todo: %s", err.Error()), http.StatusInternalServerError)
               }
       }
}


Vamos a implementarlo y ejecutarlo.

echo | faas-cli invoke todo -f todo.yml
curl http://127.0.0.1:8080/function/todo/create --data "faas-cli build"
curl http://127.0.0.1:8080/function/todo/create --data "faas-cli push"
curl http://127.0.0.1:8080/function/todo/create --data "faas-cli deploy"


Verifique los registros de API:

faas-cli logs todo
2020-03-26T14:35:29Z 2020/03/26 14:35:29 POST /create - 200 OK - ContentLength: 0


Verifique el contenido de la tabla usando pgsql:

export POSTGRES_PASSWORD=$(kubectl get secret --namespace default postgresql -o jsonpath="{.data.postgresql-password}" | base64 --decode)
kubectl run postgresql-client --rm --tty -i --restart='Never' --namespace default --image docker.io/bitnami/postgresql:11.6.0-debian-9-r0 --env="PGPASSWORD=$POSTGRES_PASSWORD" --command -- psql --host postgresql -U postgres -d postgres -p 5432
postgres=# select * from todo;
id |   description   |        created_date        | completed_date
----+-----------------+----------------------------+----------------
1 | faas-cli build  | 2020-03-26 14:36:03.367789 |
2 | faas-cli push   | 2020-03-26 14:36:03.389656 |
3 | faas-cli deploy | 2020-03-26 14:36:03.797881 |


Felicitaciones, ahora tiene una API TODO que puede aceptar solicitudes entrantes a través de curlcualquier otro cliente HTTP y escribirlas en la tabla de la base de datos.

Solicitud de registro


Creemos una nueva función para consultar registros TODO desde una tabla:

func selectTodos() ([]Todo, error) {
   var error err
   var todos []Todo
   return todos, err
}


No podemos nombrar este método de selección porque es una palabra clave reservada para trabajar con goroutines.

Ahora conecte el método al controlador principal:

} else if r.Method == http.MethodGet && r.URL.Path == "/list" {
   todos, err := selectTodos()
   if err != nil {
       http.Error(w, fmt.Sprintf("unable to get todos: %s", err.Error()), http.StatusInternalServerError)
   }
   out, _ := json.Marshal(todos)
   w.Header().Set("Content-Type", "application/json")
   w.Write(out)
}


Ahora que hay campos adicionales para fechas en nuestro esquema de datos, actualice la estructura Todo:

type Todo struct {
   ID int `json:"id"`
   Description   string `json:"description"`
   CreatedDate   *time.Time `json:"created_date"`
   CompletedDate *time.Time `json:"completed_date"`
}


Ahora agreguemos el selectTodos()código de solicitud a nuestro método :

func selectTodos() ([]Todo, error) {
       rows, getErr := db.Query(`select id, description, created_date, completed_date from todo;`)
   if getErr != nil {
       return []Todo{}, errors.Wrap(getErr, "unable to get from todo table")
   }
   todos := []Todo{}
   defer rows.Close()
   for rows.Next() {
       result := Todo{}
       scanErr := rows.Scan(&result.ID, &result.Description, &result.CreatedDate, &result.CompletedDate)
       if scanErr != nil {
           log.Println("scan err:", scanErr)
       }
       todos = append(todos, result)
   }
   return todos, nil
}


Como antes, debemos retrasar el cierre de las filas de la solicitud. Cada valor se inserta en la nueva estructura utilizando el método rows.Scan. Al final del método, tenemos una parte del contenido de Todo.

Intentemos:

faas-cli up -f todo.yml --build-arg GO111MODULE=on
curl http://127.0.0.1:8080/function/todo/list


Aquí está el resultado:

[
 {
   "id": 2,
   "description": "faas-cli build",
   "created_date": "2020-03-26T14:36:03.367789Z",
   "completed_date": null
 },
 {
   "id": 3,
   "description": "faas-cli push",
   "created_date": "2020-03-26T14:36:03.389656Z",
   "completed_date": null
 },
 {
   "id": 4,
   "description": "faas-cli deploy",
   "created_date": "2020-03-26T14:36:03.797881Z",
   "completed_date": null
 }
]


Para eliminar los valores, podemos actualizar las anotaciones de estructura agregando omitempty:

CompletedDate *time.Time `json:"completed_date,omitempty"`


Para resumir el trabajo realizado


Todavía no hemos terminado, pero este es un buen momento para detenerse y revisar lo que hemos logrado hasta ahora. Nosotros:

  • Instalado Go, Docker, kubectl y VSCode (IDE)
  • Implementado Kubernetes en nuestra computadora local
  • Postgresql instalado usando arkade y helm3
  • Instalé OpenFaaS y la pila PLONK para desarrolladores de aplicaciones Kubernetes
  • Creó la API REST estática inicial usando Go y golang-middlewarela plantilla OpenFaaS
  • Se agregó la funcionalidad de "inserción" a nuestra API TODO
  • Se agregó funcionalidad selecta a nuestra API TODO


El ejemplo de código completo que hemos creado hasta ahora está disponible en mi cuenta de GitHub: alexellis / kubernetes-todo-go-app.

Entonces podemos hacer mucho más, por ejemplo:

  • Agregar autenticación mediante un token de portador estático
  • Cree una página web utilizando una plantilla HTML estática o React para representar una lista TODO y crear nuevos elementos.
  • Agregar soporte multiusuario a la API


Y mucho más. También podríamos profundizar en la pila PLONK e implementar el panel de Grafana para comenzar a monitorear nuestra API y comprender cuántos recursos se están utilizando con el panel de Kubernetes o el servidor métrico (instalado usando arkade install).

Obtenga más información sobre arkade: https://get-arkade.dev

Asista al taller de OpenFaaS para obtener más información sobre lo anterior: https://github.com/openfaas/workshop/

Puede suscribirse a mi boletín de actualizaciones de información privilegiada premium en
https: / /www.alexellis.io/
y en mi twitter Alex Ellis



Aprende más sobre el curso



All Articles