Crie uma API TODO para Golang com Kubernetes

Olá a todos! Antes do lançamento do curso da Plataforma de Infraestrutura baseado em Kubernetes, preparamos uma tradução de outro material interessante.



Este artigo é destinado a recém-chegados ao Kubernetes, interessados ​​em entender um exemplo prático de como escrever uma API Golang para gerenciar uma lista TODO e, em seguida, como implantá-la no Kubernetes.

Todo desenvolvedor adora uma boa lista de tarefas, certo? De que outra forma poderíamos nos organizar de outra maneira?


Todo desenvolvedor adora um bom aplicativo TODO, certo?

Começaremos analisando a lista de componentes necessários, depois configurando o Kubernetes, fornecendo o banco de dados Postgresql e instalando uma estrutura de aplicativos que nos ajudará a implantar facilmente a API Go no Kubernetes, sem focar nos detalhes.

Criaremos dois pontos de extremidade na API - um para criar um novo registro TODO, o outro para selecionar todos os registros TODO.

Todo o código deste tutorial está disponível no GitHub.

A lista de componentes necessários:


  • Docker instalado localmente

O Kubernetes executa o código nas imagens do contêiner, portanto, você precisa instalar o Docker no seu computador.

Instale o Docker: https://www.docker.com
Registre uma conta do Docker Hub para armazenar suas imagens do Docker: https://hub.docker.com/

  • Cluster Kubernetes


Você pode escolher um cluster local ou remoto, mas qual é o melhor? Opções simplificadas, como o k3d, funcionam em qualquer computador em que o Docker possa ser executado, portanto, a execução de um cluster local não exige mais muita RAM. Um cluster remoto também pode ser uma solução muito eficiente, mas lembre-se de que todas as suas imagens do Docker precisarão ser carregadas e carregadas para cada alteração.

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

k3d não instalará o kubectl (esta é a CLI do Kubernetes); portanto, instale-o separadamente daqui: https://kubernetes.io/docs/tasks/tools / install-kubectl

  • Golang


Você também precisa instalar o Golang com o IDE no seu computador. O Go é gratuito e você pode baixá-lo para MacOS, Windows ou Linux no seguinte link: https://golang.org/dl

  • IDE


Eu recomendaria usar o Visual Studio Code, é gratuito e possui um conjunto de plugins para Go que você pode adicionar. Alguns colegas preferem Goland da Jetbrains. Se você é um programador Java, provavelmente prefere pagar pelo Goland, pois ele lembrará seus outros produtos.

Instale o VSCode ou Golang .

Crie um cluster


Você precisa instalar todo o software especificado na seção anterior.

Crie um novo cluster usando o k3d:


k3d create

Configure o kubectl para que aponte para um novo cluster. Lembre-se de que vários clusters podem ser usados ​​em um computador, por isso é extremamente importante apontar para o correto:

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


Verifique se o cluster possui pelo menos um nó. Aqui você pode ver que eu tenho o Kubernetes 1.17 instalado - uma versão relativamente nova:

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


Vamos armazenar nossos registros TODO na tabela do banco de dados, agora precisamos instalá-lo. O Postgresql é um banco de dados relacional popular que podemos instalar em um cluster usando o gráfico Helm.

Instalar o Arkade


O arkade é um CLI Go, semelhante ao "brew" ou "apt-get", mas para aplicativos Kubernetes. Ele usa um projeto Helm, kubectl ou CLI para instalar um projeto ou produto em seu cluster.

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


Agora instale o Postgresql

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


Você também verá informações sobre a cadeia de conexão e como iniciar a CLI do Postgresql através da imagem do Docker dentro do cluster.

Você pode obter essas informações a qualquer momento usando arkade info postgresql.

Vamos projetar o layout da tabela

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


Execute arkade info postgresqlpara obter informações de conexão novamente. Deve ser algo como isto:

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


Agora você tem no prompt de comando :, postgres = #você pode criar a tabela, copiando-a para dentro e, em seguida, execute \dtpara mostrar a tabela:

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


Instale a estrutura do aplicativo


Assim como os desenvolvedores de PHP aceleraram seu fluxo de trabalho usando os desenvolvedores LAMP (Linux Apache + Mysql + PHP) e Rails usando uma pilha pré-preparada, os desenvolvedores do Kubernetes podem usar estruturas de aplicativos.
A pilha PLONK significa Prometheus, Linux, OpenFaaS, NATS e Kubernetes.
  • O Prometheus fornece métricas, dimensionamento automático e observabilidade para testar a integridade do seu sistema, permite responder a picos de demanda e reduzir custos, fornecendo métricas para tomar decisões sobre o dimensionamento para zero.
  • O Linux, embora não seja a única opção para executar cargas de trabalho no Kubernetes, é padrão e mais fácil de usar.
  • Inicialmente, o OpenFaaS forneceu funções portáteis aos desenvolvedores, mas funciona muito bem ao implantar APIs e microsserviços. Sua versatilidade significa que qualquer contêiner Docker com um servidor HTTP pode ser implantado e gerenciado.
  • NATS é um projeto CNCF popular usado para mensagens e pub / sub. Na pilha PLONK, fornece a capacidade de executar solicitações de forma assíncrona e para enfileiramento.
  • Kubernetes é a razão de estarmos aqui. Ele fornece uma infraestrutura escalonável, auto-reparável e declarativa. E se você não precisar mais de toda essa grandeza, sua API será simplificada com o OpenFaaS.


Instale a pilha PLONK via arkade:

arkade install openfaas


Leia a mensagem informativa e execute cada comando:

  • Instale o faas-cli.
  • Obtenha sua senha.
  • Redirecione a interface do usuário do gateway OpenFaaS (através da porta 8080)
  • E faça login no sistema através da CLI.


Como antes, você pode receber uma mensagem informativa usando info openfaas.

Implante sua API First Go


Existem várias maneiras de criar uma API Go usando o PLONK. A primeira opção é usar o Dockerfile e determinar manualmente a porta TCP, a verificação de integridade, o servidor HTTP e assim por diante. Isso pode ser feito usando faas-cli new --lang dockerfile API_NAME, mas existe uma maneira mais simples e automatizada.

A segunda maneira é usar os modelos internos oferecidos pelo 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


Como queremos criar uma API tradicional no estilo HTTP, o modelo golang-middleware seria o mais apropriado.

No início do tutorial, você se registrou na sua conta do Docker Hub para armazenar suas imagens do Docker. Cada carga de trabalho do Kubernetes deve ser incorporada em uma imagem do Docker antes de poder ser implantada.

Puxe o padrão especial:

faas-cli template store pull golang-middleware


Use o Scaffold para sua API com golang-middleware e seu nome de usuário no Docker Hub:

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


Você verá dois arquivos gerados:

  • ./todo.yml - fornece uma maneira de configurar, implantar e instalar o modelo e o nome
  • ./todo/handler.go - aqui você escreve o código e adiciona outros arquivos ou pacotes necessários


Vamos editar e expandir o 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))
}


Se você não usar o VScode e seus plug-ins para editar e formatar o código, execute-o após cada alteração para garantir que o arquivo esteja formatado corretamente.

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


Agora implante o código - primeiro crie uma nova imagem, inicie-a no Docker Hub e implante-a no cluster usando a API OpenFaaS:

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


Permitir que os usuários criem novos registros TODO


Agora vamos permitir que os usuários criem novas entradas na lista TODO. Primeiro, você precisa adicionar o link ou "dependência" para Ir para a biblioteca do Postgresql.

Podemos obtê-lo usando os módulos de venda automática ou Go que foram introduzidos no Go 1.11 e instalados por padrão no Go 1.13.

Edite handler.goe adicione o módulo que precisamos para acessar o Postgresql:

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


Para que os módulos Go detectem dependências, precisamos declarar algo dentro do arquivo, que usaremos posteriormente. Caso contrário, o VSCode excluirá essas linhas quando salvas.

Adicione isso sob as importações no arquivo

var db *sql.DB


Sempre execute esses comandos na todopasta em que está localizado handler.goe não no nível raiz c todo.yml.

Inicialize o novo módulo Go:

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


Agora atualize o arquivo usando a 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


O que estiver dentro go.mod, copie seu conteúdo paraGO_REPLACE.txt

cat go.mod > GO_REPLACE.txt


Agora, verifique se o assembly ainda funciona antes de adicionar o código de inserção.

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


Você pode perceber que agora passamos --build-arga relatar um padrão para usar os módulos Go.

Durante a montagem, você verá que os módulos são baixados conforme necessário da 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


Configurando segredos para acesso ao Postgresql


Podemos criar um pool de conexões init (), um método que será executado apenas uma vez quando o programa for iniciado.

// 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 você notou, algumas informações são extraídas do que é os.Getenvlido no ambiente. Eu consideraria esses valores não confidenciais, eles estão definidos no arquivo todo.y.

O restante, como a senha e o host, que são confidenciais, são mantidos em segredo do Kubernetes .

Você pode criá-los completamente faas-cli secret createou completamente kubectl create secret generic -n openfaas-fn.

A string sdk.ReadSecretvem de OpenFaaS Nuvem SDK, com a seguinte importação: github.com/openfaas/openfaas-cloud/sdk. Ele lê um arquivo secreto do disco e retorna um valor ou erro.

Obtenha os valores secretos de arkade info postgresql.

Agora, para cada senha, faça o seguinte:

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 os segredos quanto à disponibilidade e conformidade:

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 nosso arquivo YAML e adicione o seguinte:

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


Em seguida, atualize os módulos Go e execute a compilação novamente:

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


A montagem funcionou como esperado, então vamos executá-la faas-clicom os mesmos argumentos para iniciar e implantar a imagem. Se as credenciais e a configuração do SQL estiverem corretas, não veremos erros nos logs; no entanto, se estiverem incorretos, obteremos o código de pânico em init ().

Verifique os logs:

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


Enquanto tudo parece bom, agora vamos tentar chamar o terminal:

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


Portanto, agora estabelecemos uma conexão bem-sucedida ao banco de dados e podemos executar a inserção. Como sabemos disso? Como db.Ping ()retorna um erro, caso contrário, isso causaria pânico:

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


Siga o link para obter mais detalhes sobre o pacote database / sql .

Escrevendo o código de inserção


Esse código insere uma nova linha na tabela todoe usa sintaxe especial na qual o valor não é colocado entre aspas, mas é substituído pelo código db.Query. Nos "velhos tempos" da programação LAMP, um erro comum que levou muitos sistemas a se tornarem inseguros foi a falta de saneamento dos dados de entrada e a concatenação da entrada do usuário diretamente na instrução SQL.

Imagine alguém digitando uma descrição; drop table todoNão seria muito divertido.

Portanto, corremos db.Query, em seguida, passar a instrução SQL usando $1, $2etc. para cada valor, e então nós podemos obter o resultado e / ou de erro. Também devemos fechar esse resultado, portanto, use adiar para isso.

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
}


Agora vamos conectar isso ao 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 implantá-lo e executá-lo.

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 os logs da API:

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


Verifique o conteúdo da tabela usando o 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 |


Parabéns, agora você tem uma API TODO que pode aceitar solicitações recebidas por meio de curlqualquer outro cliente HTTP e gravá-las na tabela do banco de dados.

Solicitação de Registro


Vamos criar uma nova função para consultar registros TODO de uma tabela:

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


Não podemos nomear esse método de seleção porque é uma palavra-chave reservada para trabalhar com goroutines.

Agora conecte o método ao manipulador 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)
}


Agora que existem campos adicionais para datas em nosso esquema de dados, atualize a estrutura Todo:

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


Agora vamos adicionar o selectTodos()código de solicitação ao nosso 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, precisamos atrasar o fechamento das linhas da solicitação. Cada valor é inserido na nova estrutura usando o método lines.Scan. No final do método, temos uma parte do conteúdo Todo.

Vamos tentar:

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


Aqui está o 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 remover os valores, podemos atualizar as anotações da estrutura adicionando omitempty:

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


Para resumir o trabalho realizado


Ainda não terminamos, mas este é um bom momento para parar e revisar o que alcançamos até agora. Nós:

  • Go, Docker, kubectl e VSCode (IDE) instalados
  • Implantou o Kubernetes em nosso computador local
  • Postgresql instalado usando arkade e helm3
  • OpenFaaS instalado e a pilha PLONK para desenvolvedores de aplicativos Kubernetes
  • Criou a API REST estática inicial usando Go e golang-middlewareo modelo OpenFaaS
  • Adicionada funcionalidade "inserir" à nossa API TODO
  • Adicionada funcionalidade de seleção à nossa API TODO


O exemplo de código completo que criamos até agora está disponível na minha conta do GitHub: alexellis / kubernetes-todo-go-app

Então, podemos fazer muito mais, por exemplo:

  • Adicionando autenticação usando um token de portador estático
  • Crie uma página da Web usando um HTML estático ou um modelo React para renderizar uma lista TODO e criar novos elementos.
  • Adicionando suporte multiusuário à API


E muito mais. Também podemos nos aprofundar na pilha PLONK e implantar o painel Grafana para começar a monitorar nossa API e entender quantos recursos estão sendo usados ​​com o painel ou servidor métrico Kubernetes (instalado usando arkade install).

Saiba mais sobre o arkade: https://get-arkade.dev

Participe do workshop do OpenFaaS para saber mais sobre o acima: https://github.com/openfaas/workshop/

Você pode se inscrever no meu boletim informativo premium de atualizações de insiders em
https: / /www.alexellis.io/
e no meu twitter Alex Ellis



Saiba mais sobre o curso



All Articles