Учимся работать с Kubernetes через запуск приложения
- вторник, 18 июня 2024 г. в 00:00:05
Всем привет! Меня зовут Павел Агалецкий, я ведущий разработчик юнита Platform as a Service в Авито. В этой статье мы научимся запускать и отлаживать приложения в Kubernetes и познакомимся с двумя инструментами: утилитой kubectl и консольным дашбордом k9s.
Мы попытаемся запустить в Kubernetes два приложения, которые будут взаимодействовать друг с другом через вызовы API.
Первое приложение — app1 — отвечает фразой Hello World
и текущим значением времени. Время app1 получает из второго приложения app2. Для этого оно подключается к app2 с помощью env-переменной APP2_URL
— в ней должен быть указан адрес ко второму приложению.
func main() {
app2URL, ok := os.LookupEnv("APP2_URL")
if !ok {
slog.Error("missing APP2_URL")
os.Exit(1)
}
getTimeURL := fmt.Sprintf("%s/time", app2URL)
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
slog.Info("Received request to /hello endpoint")
app2Resp, err := http.DefaultClient.Get(getTimeURL)
if erp != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "App2 (%) is not available: %v", getTimeURL, err)
return
}
defer app2Resp.Body.Close()
respTime, _ := io.ReadAll(app2Resp.Body)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Hello World at %",, string(respTime))
})
slog.Info("Starting server on port 8890")
err := http.ListenAndServe(":8890", nil)
if err != nil {
slog.Error("Application 1 finished with an error", "error", err)
}
}
Второе приложение — app2 — выводит текущее время в ответ на вызов своего API/time.
Вот код app2:
func main() {
http.HandleFunc("/time", func(w http.ResponseWriter, r *http.Request) {
slog.Info("Received request to /time endpoint")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, time.Now().String())
})
slog.Info("Starting server 2 on port 8891")
err := http.ListenAndServe(":8891", nil)
if err != nil {
slog.Error("Application 2 finished with an error", "error", err)
}
}
Нам надо собрать контейнеры приложений. Для этого используем следующие Dockerfiles:
Для первого приложения:
FROM golang:1.21-alpine
COPY . /app
WORKDIR /app
RUN ls -la . && \
go install . && \
which kubeapp1
ENTRYPOINT ["/go/bin/kubeapp1"]
Для второго приложения:
FROM golang:1.21-alpine
COPY . /app
WORKDIR /app
RUN ls -la . && \
go install . && \
which kubeapp2
ENTRYPOINT ["/go/bin/kubeapp2"]
Теперь соберём их с помощью команды docker build
:
# в директории app1:
$ docker build -t kubeapp1 .
# в директории app2:
$ docker build -t kubeapp2 .
Подробнее вы можете прочитать в нашей предыдущей статье о работе с Kubernetes.
Убедимся, что они присутствуют в нашем docker-хосте, для этого воспользуемся командой docker images
, она позволяет посмотреть существующие контейнеры в Docker.
Задеплоим контейнеры в кластер. Ниже показаны deployments.yaml
для приложений, в которые намеренно внесены некоторые ошибки. Позже попробуем их устранить.
Для app1:
apiVersion: v1
kind: Namespace
metadata:
name: app1
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubeapp1
namespace: app1
labels:
app: kubeapp1
spec:
selector:
matchLabels:
app: kubeapp1
replicas: 10
strategy:
type: RollingUpdate
template:
metadata:
namespace: app1
labels:
app: kubeapp1
spec:
containers:
- name: app
image: kubeapp1
imagePullPolicy: Never
ports:
- containerPort: 8890
Для app2:
apiVersion: v1
kind: Namespace
metadata:
name: app2
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubeapp2
namespace: app2
labels:
app: kubeapp2
spec:
selector:
matchLabels:
app: kubeapp2
replicas: 18
strategy:
type: RollingUpdate
template:
metadata:
namespace: app2
labels:
app: kubeapp2
spec:
containers:
- name: app
image: kubeapp22
imagePullPolicy: Never
ports:
- containerPort: 8891
Теперь выполним деплой командой kubectl apply
:
Проверим, работает ли первое приложение. Для этого посмотрим список подов с помощью команды kubectl get pods -n app1
:
Ошибка в app1
Выясним причину ошибки с помощью команды kubectl describe -n app1 pod/kubeapp1-5fcd8b8d9f-brwhh
, где kubeapp1-5fcd8b8d9f-brwhh
— название пода.
Здесь видно, что контейнер запускается и тут же падает. На это указывает сообщение Back-off restarting failed container app in pod...
Найдём причину падения в логах. Посмотреть их можно с помощью команды kubectl logs -n app1 pod/kubeapp1-5fcd8b8d9f-brwhh
Как мы видим, ошибка в том, что мы не указали env-переменную APP2_URL
.
Ошибка в app2
Посмотрим на поды второго приложения через команду kubectl get pods -n app2.
Видим, что в app2 тоже плохо, но у его подов другой статус — ErrImageNeverPull
.
Выясним причину такого статуса помощью команды kubectl describe -n app1 pod/kubeapp1-5fcd8b8d9f-brwhh
Ошибка сообщает о том, что невозможно спуллить под kubeapp22
. Так и есть: наш образ называется kubeapp2
. Ошибка сообщает, что образ с названием kubeapp22
отсутствует и не может быть скачен из registry.
Это из-за ошибки в манифесте — вместо kubeapp22
надо написать kubeapp2
.
В реальной ситуации стоило бы исправить описание ресурса в исходном файле, который мы использовали в Kubernetes, но в целях тренировки мы поправим ситуацию прямо в кластере куба.
Поды не являются самостоятельными единицами — они объединены в деплоймент, и именно в нём неверно указано название образа. Давайте это проверим c помощью команды kubectl get deployments -n app2
Так и есть. Деплоймент существует, но ни одна из опрошенных 18 реплик не работает. Чтобы увидеть полный манифест ресурса, добавим флаг -o yaml
.
Как мы видим, в манифесте неправильно указано название образа. Отредактируем название с помощью команды kubectl edit -n app2 deployment/kubeapp2
Она откроет редактор по умолчанию. После этого нам надо будет поправить значение kubeapp22 на kubeapp2 и сохранить изменение. Такое изменение будет автоматически применено в кластере Kubernetes.
Теперь посмотрим на статусы подов еще раз:
Видим, что все поды теперь запустились и работают.
Давайте вернёмся к нашему первому приложению. Оно не работает, потому что мы не указали адрес второго приложения в env-переменной. Чтобы это исправить, нужно определить, какой будет адрес.
Пока адрес не доступен: мы не создали никакого специального ресурса, чтобы можно было получить доступ ко второму приложению. Нужный нам тип ресурса — так называемый сервис, или сокращённо svc.
Ресурс svc позволяет указать, что определённое приложение, запущенное в Kubernetes, должно быть доступно по таким-то адресам. Мы создавали такой тип ресурса в этом видео, чтобы получить доступ к нему снаружи кластера.
Давайте создадим ресурс сервис, чтобы приложение app1 могло получить доступ к приложению app1. Для этого используем команду kubectl expose -n app2 deployment/kubeapp2
Теперь сервис должен быть доступен. Выясним, по какому адресу, — с помощью команды kubectl get -n app2 svc
Мы видим IP-адрес кластера, но ориентироваться на него не стоит — он может поменяться. Внутри нашего кластера работает специальный DNS. Это значит, что любой созданный сервис будет доступен по DNS-адресу.
Проверим, что мы можем использовать специальный DNS-адрес кластера. Для этого подключимся к пока единственно работающему в нашем кластере приложению app2. Посмотрим на список подов и подключимся к одному из них с помощью команды kubectl exec -n app2 pod/kubeapp2-64486b645f-gqklp -it -- sh
Флаг -it – sh
запустит внутри терминал и сделает его доступным в интерактивным режиме.
Так как наш образ основан на Alpine, мы можем установить в него curl с помощью команды apk add curl.
Теперь попробуем обратиться к сервису по его имени kubeapp2. В консоль действительно выводится время — как и мы ожидаем от приложения app2.
Однако приложение app1 находится в другом неймспейсе и сможет получить доступ к app2 только по полному имени сервиса с указанием неймспейса. В нашем случае полное имя выглядит так: kubeapp2.app2.svc.cluster.local.
Полный адрес можно указать в переменную APP2_URL, отредактировав манифест. В этот раз давайте сделаем это через другой инструмент — утилиту k9s.
k9s — это CLI-инструмент, который предоставляет удобный интерфейс для взаимодействия с кластерами Kubernetes и позволяет легко управлять ресурсами, подами, службами и другими объектами Kubernetes.
K9s обычно используется для управления и отладки приложений, развернутых на кластерах Kubernetes, а также для мониторинга состояния кластера в целом.
Установим k9s:
brew install k9s
Запустим k9s:
k9s
Внутри k9s — дашборд, на котором видны различные ресурсы кластера: поды, неймспейсы, сервисы, деплойменты.
Наша задача — поменять значение env-переменной APP2_URL на полный DNS-адрес кластера.
Перейдем в нужный неймспейс.
Нажмем клавишу :
.
Введём в появившейся строке команду namespace.
С помощью Enter можно выбрать нужный неймспейс — нам интересен app1. Внутри мы увидим список подов приложения:
Чтобы посмотреть deployment приложения , нужно также нажать на :
и ввести deployments. У нас один деплоймент в этом неймспейсе — kubeapp1
.
Отредактировать deployment можно с помощью клавиши e
. Добавим env-переменную APP2_URL
После сохранения все внесённые изменения применятся в кластере Kubernetes, как и в предыдущем примере с kubeapp1.
Теперь проверим состояние приложения. В k9s для этого достаточно нажать на название деплоймента.
Как мы видим, все образы находятся в состоянии Running — всё работает, приложение запущено.
Зайдём в один из подов и вызовем оттуда app2. Чтобы попасть в Shell, нужно нажать клавишу S
на поде.
Попробуем вызвать /time
у app2:
curl http://kubeapp2.app2.svc.cluster.local:8891/time
В разделе выше мы попробовали вызвать /time
у app2 внутри кластера. А что будет, если сделать то же самое, но снаружи кластера?
Для этого для начала выставим приложение app1 наружу. Не забудьте выйти из k9s с помощью Ctrl+D и Ctrl+C.
kubectl expose --type=NodePort -n app1 deployment/kubeapp1
service/kubeapp1 exposed
--type=NodePort
указывает, что мы хотим получать доступ к сервису снаружи.
Попробуем сделать запрос на порт, который был присвоен нашему приложению снаружи. А сначала узнаем этот порт, для этого посмотрим список сервисов в неймспейсе app1.
kubectl get svc -n app1
Как видно на скриншоте ниже, присвоенный порт — 30136. Давайте сделаем запрос на него. Узнать IP машины можно с помощью команды colima status.
curl 192.168.106:2:30136/hello
Указываем, что мы хотим получать доступ к сервису снаружи.
Узнаём присвоенный приложению порт.
Узнаём IP машины.
Пробуем сделать запрос снаружи.
Как вы видите, API успешно отрабатывает и при запросе снаружи.
В этой статье мы изучили несколько команд kubectl
и познакомились с инструментом k9s, который позволяет работать с кластером в консоли в визуальном режиме.
Команды kubectl
kubectl apply -f <файл>
— применить конфигурацию к кластеру из указанного файла или каталога. Команда создает или обновляет ресурсы в кластере.
kubectl get pods
— показать список всех подов в текущем пространстве имен. Можно добавить флаги для просмотра подов во всех пространствах имен или с определенными критериями.
kubectl describe <ресурс> <имя>
— показать подробную информацию о конкретном ресурсе, например о поде, включая состояние, переменные окружения, связанные ресурсы и другие важные сведения.
kubectl logs <имя пода>
— получить логи для конкретного пода. Это полезно для отладки и мониторинга работы приложений.
kubectl get deployments
— отобразить список всех деплойментов, которые управляют подами.
kubectl edit <ресурс> <имя>
— открыть редактор по умолчанию, позволяющий вносить изменения в спецификацию ресурса непосредственно из командной строки.
kubectl expose
— создать новый сервис (svc), который обеспечивает доступ к подам через сеть.
kubectl get svc
— показать список всех сервисов, доступных в кластере или неймспейсе .
kubectl exec -it <имя поля> -- <команда>
— выполнить команду внутри контейнера в поде. Опция -it позволяет интерактивно работать с контейнером через терминал. Это может быть полезно для отладки и управления системой изнутри контейнера.
Горячие клавиши в k9s
:
— ввести команду для выполнения.
s
— попасть в Shell на поде.
e
– отредактировать ресурс
Ctrl+D, Ctrl+C — выйти из k9s.