Всем привет! В преддверии старта курса «Инфраструктурная платформа на основе Kubernetes» подготовили перевод еще одного интересного материала.



Эта статья рассчитана на новичков в Kubernetes, которым будет интересно разобраться на практическом примере, как написать Golang API для управления TODO list-ом, и затем как развернуть его в Kubernetes.

Каждый разработчик любит хороший TODO list, не правда ли? Как бы еще мы могли себя организовать иначе?


Каждый разработчик любит хорошее TODO приложение, не так ли?

Мы начнем с обзора перечня необходимых компонентов, затем перейдем к настройке Kubernetes, предоставим базу данных Postgresql, а затем установим фреймворк приложения, который поможет нам легко развернуть Go API в Kubernetes, не зацикливаясь на деталях.

Мы создадим два эндпоинта в API — один для создания новой TODO записи, другой для выбора всех TODO записей.

Весь код из этого туториала доступен на GitHub

Перечень необходимых компонентов:


  • Локально установленный Docker

Kubernetes запускает код в образах контейнеров, поэтому вам необходимо установить Docker на свой компьютер.

Установите Docker: https://www.docker.com
Зарегистрируйте учетную запись Docker Hub для хранения ваших образов Docker: https://hub.docker.com/

  • Кластер Kubernetes


Вы можете выбрать либо локальный, либо удаленный кластер, но какой лучше? Упрощенные варианты, такие как k3d, работают на любом компьютере, на котором может работать Docker, поэтому для запуска локального кластера больше не требуется много оперативной памяти. Удаленный кластер также может быть очень эффективным решением, но имейте в виду, что все ваши образы Docker нужно будет выгружать и загружать для каждого изменения.

Установите k3d: https://github.com/rancher/k3d

k3d не установит kubectl (это CLI для Kubernetes), поэтому установите его отдельно отсюда: https://kubernetes.io/docs/tasks/tools / install-kubectl

  • Golang


Так же вам необходимо установить на ваш компьютер Golang вместе с IDE. Go бесплатен, и вы можете скачать его для MacOS, Windows или Linux по следующей ссылке: https://golang.org/dl

  • IDE


Я бы порекомендовал использовать Visual Studio Code, она бесплатно и имеет набор плагинов для Go, которые вы можете добавить. Некоторые коллеги предпочитают Goland от Jetbrains. Если вы Java программист, вы, вероятно, предпочтете заплатить за Goland, поскольку он будет напоминать вам о других их продуктах.

Установите VSCode или Golang.

Создаем кластер


Вам необходимо установить все программное обеспечение, указанное в предыдущем разделе.

Создайте новый кластер с помощью k3d:


k3d create

Настройте kubectl таким образом, чтобы он указывал на новый кластер. Помните, что с одного компьютера могут использоваться несколько кластеров, поэтому крайне важно указывать на правильный:

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


Убедитесь, что в кластере есть хотя бы один узел. Здесь вы можете видеть, что у меня установлен ??Kubernetes 1.17 — относительно новая версия:

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


Мы будем хранить наши TODO записи в таблице базы данных, поэтому теперь нам нужно ее установить. Postgresql — это популярная реляционная база данных, которую мы можем установить в кластер с помощью диаграммы Хелма.

Установка arkade


arkade — это CLI Go, похожий на «brew» или «apt-get», но для приложений Kubernetes. Он использует Helm, kubectl или CLI проекта для установки проекта или продукта в ваш кластер.

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


Теперь установите Postgresql

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


Вы также увидите информацию о строке подключения и о том, как запустить CLI Postgresql через образ Docker внутри кластера.

Вы можете получить эту информацию в любое время с помощью arkade info postgresql.

Спроектируем схему таблицы

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


Запустите arkade info postgresql, чтобы снова получить информацию о соединении. Она должна выглядеть примерно так:

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 = #, вы можете создать таблицу, скопировав это внутрь, затем запустите \dt, чтобы показать таблицу:

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


Установка фреймворка приложения


Так же, как PHP разработчики ускорили свой рабочий процесс с помощью LAMP (Linux Apache + Mysql + PHP) и Rails разработчики с помощью предварительно подготовленного стека, разработчики Kubernetes могут использовать фреймворки приложений.
Стек PLONK расшифровывается как Prometheus, Linux, OpenFaaS, NATS и Kubernetes.
  • Prometheus предоставляет метрики, автоматическое масштабирование и наблюдаемость для проверки работоспособности вашей системы, позволяет ей реагировать на всплески спроса и сокращать расходы, предоставляя метрики для принятия решений о масштабировании до нуля.
  • Linux, хотя и не единственный вариант запуска рабочих нагрузок в Kubernetes, является стандартным и наиболее простым в использовании.
  • Изначально OpenFaaS предоставлял переносимые функции разработчикам, но отлично работает и при развертывании API и микросервисов. Его универсальность означает, что могут быть развернуты и управляемы любые контейнеры Docker с HTTP-сервером.
  • NATS — это популярный проект CNCF, используемый для обмена сообщениями и pub/sub. В стеке PLONK он предоставляет возможность выполнять запросы асинхронно и для организации очередей.
  • Kubernetes — причина, по которой мы здесь собрались. Он обеспечивает масштабируемую, самовосстанавливающуюся и декларативную инфраструктуру. И если вам не нужна больше часть всего этого великолепия, его API упрощается с помощью OpenFaaS.


Установите стек PLONK через arkade:

arkade install openfaas


Прочитайте информационное сообщение и выполните каждую команду:

  • Установите faas-cli.
  • Получите ваш пароль.
  • Перенаправьте UI шлюза OpenFaaS (через порт 8080)
  • И залогиньтесь в систему через CLI.


Как и раньше, вы можете получить информационное сообщение с помощью аркады info openfaas.

Развертывание вашего первого API Go


Существует несколько способов создания API Go с помощью PLONK. Первый вариант — использовать Dockerfile и вручную определить TCP порт, проверку работоспособности, HTTP-сервер и так далее. Это можно сделать с помощью faas-cli new --lang dockerfile API_NAME, однако есть более простой и автоматизированный способ.

Второй способ — воспользоваться встроенными шаблонами, предлагаемыми 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


Поскольку мы хотим создать традиционный API в HTTP стиле, шаблон golang-middleware будет наиболее подходящим.

В начале туториала вы зарегистрировали в учетную запись Docker Hub для хранения ваших образов Docker. Каждая рабочая нагрузка в Kubernetes должна быть встроена в образ Docker, прежде чем ее можно будет развернуть.

Подтяните специальный шаблон:

faas-cli template store pull golang-middleware


Используйте Scaffold для вашего API с golang-middleware и вашим юзернеймом в Docker Hub:

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


Вы увидите два сгенерированных файла:

  • ./todo.yml — предоставляет способ настройки, развертывания и установки шаблона и имени
  • ./todo/handler.go — здесь вы пишете код и добавляете любые другие файлы или пакеты, которые вам требуются


Давайте проведем небольшое редактирование и затем развернем код.

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))
}


Если вы не используете VScode и его плагины для редактирования и форматирования кода, то запускайте это после каждого изменения, чтобы убедиться, что файл правильно отформатирован.

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


Теперь развернем код — сначала создадим новый образ, запушим его в Docker Hub и развернем в кластере с помощью OpenFaaS API:

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


Разрешаем пользователям создавать новые TODO записи


Теперь разрешим пользователям создавать новые записи в своем TODO list-е. Сначала вам нужно добавить ссылку или «зависимость» для Go в библиотеку Postgresql.

Мы можем получить ее с помощью вендинга или модулей Go, которые были введены в Go 1.11 и установлены по умолчанию в Go 1.13.

Отредактируйте handler.go и добавьте модуль, который нам необходим для доступа к Postgresql:

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


Чтобы модули Go могли обнаружить зависимость, мы должны объявить кое-что внутри файла, который мы будем использовать позже. Если мы не сделаем этого, то VSCode удалит эти строки при сохранении.

Добавьте это под импортами в файле

var db *sql.DB


Всегда запускайте эти команды в todo папке, где находится handler.go, а не на корневом уровне с todo.yml.

Инициализируйте новый модуль Go:

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


Теперь обновите файл с помощью библиотеки 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


Что бы ни было внутри go.mod, скопируйте его содержимое в GO_REPLACE.txt

cat go.mod > GO_REPLACE.txt


Теперь давайте убедимся, что сборка все еще работает до добавления кода insert.

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


Вы можете заметить, что теперь мы передаем --build-arg, чтобы сообщить шаблон для использования модулей Go.

Во время сборки вы увидите, что модули загружаются по мере необходимости из интернета.

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


Настройка секретов для доступа к Postgresql


Мы можем создать пул соединений в init (), методекоторый будет запускаться только один раз при запуске программы.

// 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())
               }
       }
}


Как вы заметили, некоторая информация извлекается из os.Getenv читаемого из среды. Эти значения я бы счел неконфиденциальными, они установлены в файле todo.y.

Остальные, такие как пароль и хост, которые являются конфиденциальными, хранятся в секретах Kubernetes.

Вы можете создать их через faas-cli secret create или через kubectl create secret generic -n openfaas-fn.

Строка sdk.ReadSecret происходит из OpenFaaS Cloud SDK со следующим импортом: github.com/openfaas/openfaas-cloud/sdk. Она читает секретный файл с диска и возвращает значение или ошибку.

Получим секретные значения из arkade info postgresql.

Теперь для каждого пароля выполним следующее:

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


Проверим секреты на предмет наличия и соответствия:

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


Отредактируем наш YAML файл и добавим следующее:

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


Далее обновим модули Go и снова запустим сборку:

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


Сборка отработала как положено, поэтому запустим faas-cli с теми же аргументами, чтобы запушить и развернуть образ. Если учетные данные и конфигурация SQL верны, мы не увидим ошибки в логах, однако, если они неверны, мы получим код паники в init().

Проверим журналы:

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


Пока все выглядит хорошо, теперь попробуем вызвать эндпоинт:

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


Итак, теперь у нас установлено успешное соединение с БД, и мы можем выполнять insert. Откуда мы это знаем? Потому что db.Ping () возвращает ошибку, а в противном случае пробросил бы панику:

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


Перейдите по ссылке для более подробной информации о пакете database/sql.

Пишем код insert


Этот код вставляет новую строку в таблицу todo и использует специальный синтаксис, в котором значение не заключено в кавычки, а вместо этого заменяется кодом db.Query. В «старые времена» LAMP программирования распространенной ошибкой, которая приводила к тому, что многие системы становились небезопасными, было отсутствие санации входных данных и конкатенация пользовательского ввода непосредственно в оператор SQL.

Представьте, что кто-то введет описание; drop table todo, было бы не очень весело.

Поэтому мы запускаем db.Query, затем передаем SQL инструкцию, используя $1, $2 и т. д. для каждого значения, а затем можем получить результат и/или ошибку. Мы также должны закрыть этот результат, поэтому используйте для этого defer.

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
}


Теперь давайте подключим это к коду.

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)
               }
       }
}


Давайте развернем это и запустим.

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"


Проверим логи API:

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


Проверим содержимое таблицы с помощью 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 |


Поздравляем, теперь у вас есть API TODO, которое может принимать входящие запросы через curl или любой другой HTTP-клиент и записывать их в таблицу базы данных.

Запрос записей


Давайте создадим новую функцию для запроса TODO записей из таблицы:

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


Мы не можем назвать этот метод select, потому что это зарезервированное ключевое слово для работы с горутинами.

Теперь подключим метод к основному обработчику:

} 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)
}


Теперь, когда в нашей схеме данных есть дополнительные поля для дат, обновите структуру Todo:

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


Теперь давайте добавим в наш метод selectTodos() код запроса:

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
}


Как и раньше, нам нужно отложить закрытие строк для запроса. Каждое значение вставляется в новую структуру с помощью метода rows.Scan. В конце метода у нас есть кусочек содержимого Todo.

Попробуем:

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


Вот результат:

[
 {
   "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
 }
]


Чтобы удалить значения, мы можем обновить аннотации структуры, добавив omitempty:

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


Подытожим проделанную работу


Мы еще не закончили, но это хороший момент, чтобы остановиться и пересмотреть то, чего мы достигли на данный момент. Мы:

  • Установили Go, Docker, kubectl и VSCode (IDE)
  • Развернули Kubernetes на нашем локальном компьютере
  • Установили Postgresql с помощью arkade и helm3
  • Установили OpenFaaS и стек PLONK для разработчиков приложений Kubernetes
  • Создали начальное статическое REST API с помощью Go и golang-middleware шаблона OpenFaaS
  • Добавили функциональность «insert» в наш TODO API
  • Добавили ??функциональность «select» в наш TODO API


Полный пример кода, который мы создали до сих пор, доступен на моем GitHub акаунте: alexellis/kubernetes-todo-go-app

Дальше мы можем сделать гораздо больше, например:

  • Добавление аутентификации с использованием статического токена на предъявителя
  • Создание веб-страницы с использованием статического шаблона HTML или React для рендеринга TODO list-а и создания новых элементов.
  • Добавление многопользовательской поддержки в API


И многое другое. Мы также могли бы покопаться в стеке PLONK и развернуть дашборд Grafana, чтобы начать наблюдать за нашим API и понять, сколько ресурсов используется с дашбордом Kubernetes или сервером метрик (установленным с помощью arkade install).

Узнайте больше об arkade: https://get-arkade.dev

Посетите семинар OpenFaaS, чтобы подробнее изучить вышесказанное: https://github.com/openfaas/workshop/

Вы можете подписаться на мою премиальную рассылку «Insiders Updates» на
https://www.alexellis.io/
и на мой твиттер Alex Ellis



Узнать подробнее о курсе