LoadBalance
В данном практическом занятии предлагаю опробовать работу Ingress используя Nginx Ingress Controller и посмотреть на процесс балансировки трафика в момент обновления версии приложения.
Kind
Для корректной работы Ingress Controller в kind необходимы дополнительные настройки кластера, для этого проще всего создать новый кластер с дополнительным блоком конфигурации, который позволит получить доступ к подам Ingress Controller с нашей машины.
Можно создать новый кластер с другим именем, либо сначала удалить старый:
$ kind delete cluster
Дополнительная конфигурация выставляет наружу порты 80 и 443, а также добавляет лейбл к ноде.
cat <<EOF | kind create cluster --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
EOF
Nginx Ingress
Для установки Nginx Ingress Controller достаточно выполнить команду:
$ k apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
После которой в неймспейсе ingress-nginx создадутся необходимые ресурсы и запустится под контроллера.
$ k get po -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-admission-create-hkt6j 0/1 Completed 0 5h57m
ingress-nginx-admission-patch-k2wmt 0/1 Completed 0 5h57m
ingress-nginx-controller-69dfcc796b-zm5pt 1/1 Running 1 (5h ago) 5h57m
Предупреждение
Существует как минимум две реализации Ingress Controller с использованием Nginx: от компании Nginx Inc и от kubernetes сообщества. В данном руководстве используется реализация от сообщества!
Application
Для проверки работы Ingress сделаем простое приложение, которое будет отвечать на HTTP запросы:
GET / - будет возвращать HTTP код 200 и сообщение «ok»
GET /version - будет возвращать HTTP код 200 и сообщение с текущей версией приложения, а в лог также выводить количество запросов, которое было обработано по данному пути
Вы можете написать его на своем любимом языке, в качестве примера приведу простую реализацию на Golang:
package main
import (
"fmt"
"io"
"net/http"
"os"
)
var (
Version string
reqCount uint
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "ok\n")
})
http.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) {
reqCount++
fmt.Printf("GET /version - %s, count - %d\n", Version, reqCount)
io.WriteString(w, fmt.Sprintf("version: %s\n", Version))
})
fmt.Println("start server")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Printf("error serve http: %s\n", err)
os.Exit(1)
}
}
Версию приложения предлагаю указывать при сборке образа в Dockerfile:
FROM golang:1.20-alpine as build
ARG BUILD_VERSION
WORKDIR /build
COPY . ./
RUN GOOS=linux GOARCH=amd64 go build \
-ldflags="-X main.Version=${BUILD_VERSION}" \
-a -o app main.go
FROM alpine:3.17
COPY --from=build /build/app /app
CMD ["/app"]
В данном примере версия приложения будет указана в итоговом бинарном файле, переданная через
аргумент BUILD_VERSION
.
$ docker build --build-arg BUILD_VERSION=1.0 -t app:1.0 .
Sending build context to Docker daemon 5.632kB
Step 1/8 : FROM golang:1.20-alpine as build
---> 818ca3531f99
Step 2/8 : ARG BUILD_VERSION
---> Using cache
---> 9a3dd54335f8
Step 3/8 : WORKDIR /build
---> Using cache
---> 9c28030abe9e
Step 4/8 : COPY . ./
---> 3c00293d730e
Step 5/8 : RUN GOOS=linux GOARCH=amd64 go build -ldflags="-X main.Version=${BUILD_VERSION}" -a -o app main.go
---> Running in dfb517e386a7
Removing intermediate container dfb517e386a7
---> 42fe6c7a47c5
Step 6/8 : FROM alpine:3.17
---> b2aa39c304c2
Step 7/8 : COPY --from=build /build/app /app
---> 68d71f56dd53
Step 8/8 : CMD ["/app"]
---> Running in 448b41e615d3
Removing intermediate container 448b41e615d3
---> 8f797e1b1ccb
Successfully built 8f797e1b1ccb
Successfully tagged app:1.0
Также соберем версию 2.0:
$ docker build --build-arg BUILD_VERSION=2.0 -t app:2.0 .
...
Successfully built 5b5feec48cb7
Successfully tagged app:2.0
И загрузим в наш кластер:
$ kind load docker-image app:1.0
Image: "" with ID "sha256:8f797e1b1ccb95ef25b846714a5162ec80aee72b102c78f7d188e52bf119a8c1" not yet present on node "kind-control-plane", loading...
$ kind load docker-image app:2.0
Image: "" with ID "sha256:5b5feec48cb7b016637584224e59c64c5cb8e107226384f5bdc3c587da5b0fb9" not yet present on node "kind-control-plane", loading...
Deploy
Создадим ресурс Deployment с нашим приложением в трех репликах:
$ k create deployment app --image=app:1.0 --replicas 3
deployment.apps/app created
Создадим ресурс Service, указав его порт и порт, который слушает приложение:
$ k expose deployment app --port 80 --target-port 8080
service/app exposed
Создадим ресурс Ingress, указав в качестве хоста - localhost и сервис app с портом 80:
$ k create ingress app --rule=localhost/*=app:80
ingress.networking.k8s.io/app created
Убедимся, что все ресурсы созданы и поды запущены:
$ k get deploy,svc,ingress,pod
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/app 3/3 3 3 17m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/app ClusterIP 10.96.219.251 <none> 80/TCP 14m
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 6h57m
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/app <none> localhost localhost 80 5m5s
NAME READY STATUS RESTARTS AGE
pod/app-64885bbddf-5f5g7 1/1 Running 0 17m
pod/app-64885bbddf-gtnt6 1/1 Running 0 17m
pod/app-64885bbddf-mdnhs 1/1 Running 0 17m
Теперь доступ к приложению возможен через localhost:
$ curl localhost
ok
$ curl localhost/version
version: 1.0
LoadBalance
Убедимся, что трафик распределяется на все поды с приложением. Для этого удалим старые поды и убедимся, что новые запущены:
$ k delete po -l app=app
pod "app-64885bbddf-5f5g7" deleted
pod "app-64885bbddf-gtnt6" deleted
pod "app-64885bbddf-mdnhs" deleted
$ k get po
NAME READY STATUS RESTARTS AGE
app-64885bbddf-886x2 1/1 Running 0 10s
app-64885bbddf-9rrh9 1/1 Running 0 10s
app-64885bbddf-lfjdz 1/1 Running 0 10s
Сделаем в цикле 100 запросов к нашему Ingress и посмотрим в логах каждого пода, сколько запросов он обработал:
$ for i in {1..100};do curl localhost/version -so /dev/null;done
$ k logs -l app=app --tail=1
GET /version - 1.0, count - 34
GET /version - 1.0, count - 33
GET /version - 1.0, count - 33
Как видно из вывода трафик равномерно распределился между подами. Nginx Ingress Controller по-умолчанию использует алгоритм балансировки round robin, изменить это поведение можно с помощью аннотаций на ресурсе Ingress.
RollingUpdate
Посмотрим, как в процессе обновлении будут выглядеть ответы от приложения.
Для начала увеличим количество реплик, к примеру, до 20, чтобы процесс обновления занял более продолжительное время:
$ k scale deploy/app --replicas 20
deployment.apps/app scaled
Для наблюдения за процессом обновления в отдельном окне терминала запустим цикл, который раз в секунду будет делать запрос к нашему приложению:
$ while sleep 1;do echo -n "$(date '+%H:%M:%S') ";curl localhost/version;done
И обновим образ на версию 2.0:
$ k set image deploy/app app=app:2.0
deployment.apps/app image updated
В нашем цикле будет видно как постепенно версия 1.0 сменяется на 2.0:
21:07:01 version: 1.0
21:07:02 version: 1.0
21:07:03 version: 1.0
21:07:04 version: 1.0
21:07:05 version: 1.0
21:07:06 version: 1.0
21:07:07 version: 1.0
21:07:08 version: 2.0
21:07:09 version: 1.0
21:07:10 version: 2.0
21:07:11 version: 1.0
21:07:12 version: 2.0
21:07:13 version: 2.0
21:07:14 version: 2.0
21:07:20 version: 2.0
21:07:21 version: 2.0
Пока все поды полностью не обновятся и останется только версия 2.0.
Попробуем также откатиться обратно:
$ k rollout undo deploy/app
deployment.apps/app rolled back
В нашем цикле мы также увидим процесс смены версии в обратную сторону:
21:19:14 version: 2.0
21:19:15 version: 2.0
21:19:16 version: 2.0
21:19:17 version: 2.0
21:19:23 version: 1.0
21:19:29 version: 2.0
21:19:30 version: 2.0
21:19:31 version: 1.0
21:19:37 version: 1.0
21:19:38 version: 1.0
21:19:39 version: 1.0
Все примеры, используемые здесь, можно также посмотреть на github.