golang

Учимся работать с Kubernetes через запуск приложения

  • вторник, 18 июня 2024 г. в 00:00:05
https://habr.com/ru/companies/avito/articles/820953/

Всем привет! Меня зовут Павел Агалецкий, я ведущий разработчик юнита Platform as a Service в Авито. В этой статье мы научимся запускать и отлаживать приложения в Kubernetes и познакомимся с двумя инструментами: утилитой kubectl и консольным дашбордом k9s.

Задача: запустить два приложения в Kubernetes

Мы попытаемся запустить в 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.

В докере лежат контейнеры обоих приложений: kuberapp2 и kubeapp1
В докере лежат контейнеры обоих приложений: kuberapp2 и kubeapp1

Задеплоим контейнеры в кластер. Ниже показаны 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
  1. Теперь выполним деплой командой kubectl apply:

Деплоим контейнеры в кластер
Деплоим контейнеры в кластер
  1. Проверим, работает ли первое приложение. Для этого посмотрим список подов с помощью команды kubectl get pods -n app1:

Видим, что поды запущены, но висят в статусе Error
Видим, что поды запущены, но висят в статусе Error

Выясняем, почему поды висят в ошибке

Ошибка в app1

  1. Выясним причину ошибки с помощью команды kubectl describe -n app1 pod/kubeapp1-5fcd8b8d9f-brwhh, где kubeapp1-5fcd8b8d9f-brwhh — название пода.

Команда describe показывает все ивенты и сообщения об ошибках
Команда describe показывает все ивенты и сообщения об ошибках

Здесь видно, что контейнер запускается и тут же падает. На это указывает сообщение Back-off restarting failed container app in pod...

  1. Найдём причину падения в логах. Посмотреть их можно с помощью команды kubectl logs -n app1 pod/kubeapp1-5fcd8b8d9f-brwhh

Как мы видим, ошибка в том, что мы не указали env-переменную APP2_URL.

Ошибка в app2

  1. Посмотрим на поды второго приложения через команду kubectl get pods -n app2. Видим, что в app2 тоже плохо, но у его подов другой статус — ErrImageNeverPull.

Поды app2
Поды app2
  1. Выясним причину такого статуса  помощью команды kubectl describe -n app1 pod/kubeapp1-5fcd8b8d9f-brwhh

Команда describe показывает все ивенты и сообщения об ошибках
Команда describe показывает все ивенты и сообщения об ошибках

Ошибка сообщает о том, что невозможно спуллить под kubeapp22. Так и есть: наш образ называется kubeapp2.  Ошибка сообщает, что образ с названием kubeapp22 отсутствует и не может быть скачен из registry.

Это из-за ошибки в манифесте — вместо kubeapp22 надо написать kubeapp2.

Чиним проблему: исправляем ошибку в app2

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

Поды не являются самостоятельными единицами — они объединены в деплоймент, и именно в нём неверно указано название образа. Давайте это проверим c помощью команды kubectl get deployments -n app2

Так и есть. Деплоймент существует, но ни одна из опрошенных 18 реплик не работает. Чтобы увидеть полный манифест ресурса, добавим флаг -o yaml.

Полное состояние манифеста со всеми параметрами. Действительно неправильно указано название образа
Полное состояние манифеста со всеми параметрами. Действительно неправильно указано название образа

Как мы видим, в манифесте неправильно указано название образа. Отредактируем название с помощью команды kubectl edit -n app2 deployment/kubeapp2

Она откроет редактор по умолчанию. После этого нам надо будет поправить значение kubeapp22 на kubeapp2 и сохранить изменение. Такое изменение будет автоматически применено в кластере Kubernetes.

Теперь посмотрим на статусы подов еще раз:

Все поды в статусе Running
Все поды в статусе Running

Видим, что все поды теперь запустились и работают.

Чиним проблему: исправляем ошибку в app1

Давайте вернёмся к нашему первому приложению. Оно не работает, потому что мы не указали адрес второго приложения в env-переменной. Чтобы это исправить, нужно определить, какой будет адрес. 

Пока адрес не доступен: мы не создали никакого специального ресурса, чтобы можно было получить доступ ко второму приложению. Нужный нам тип ресурса — так называемый сервис, или сокращённо svc. 

Ресурс svc позволяет указать, что определённое приложение, запущенное в Kubernetes, должно быть доступно по таким-то адресам. Мы создавали такой тип ресурса в этом видео, чтобы получить доступ к нему снаружи кластера.

Давайте создадим ресурс сервис, чтобы приложение app1 могло получить доступ к приложению app1. Для этого используем команду kubectl expose -n app2 deployment/kubeapp2

Теперь сервис должен быть доступен. Выясним, по какому адресу, — с помощью команды kubectl get -n app2 svc

Мы видим IP-адрес кластера, но ориентироваться на него не стоит — он может поменяться. Внутри нашего кластера работает специальный DNS. Это значит, что любой созданный сервис будет доступен по DNS-адресу.

Проверяем работу DNS в app2

Смотрим список подов и подключаемся к первому
Смотрим список подов и подключаемся к первому

Проверим, что мы можем использовать специальный 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

k9s — это CLI-инструмент, который предоставляет удобный интерфейс для взаимодействия с кластерами Kubernetes и позволяет легко управлять ресурсами, подами, службами и другими объектами Kubernetes.

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

  1. Установим k9s:

brew install k9s
  1. Запустим k9s:

k9s

Внутри k9s — дашборд, на котором видны различные ресурсы кластера: поды, неймспейсы, сервисы, деплойменты. 

Дашборд k9s
Дашборд k9s

Редактируем deployment в k9s

Наша задача — поменять значение env-переменной APP2_URL на полный DNS-адрес кластера.

  1. Перейдем в нужный неймспейс.

  2. Нажмем клавишу :.

  3. Введём в появившейся строке команду namespace.

Теперь видим все неймспейсы кластера
Теперь видим все неймспейсы кластера
  1. С помощью Enter можно выбрать нужный неймспейс — нам интересен app1. Внутри мы увидим список подов приложения:

  1. Чтобы посмотреть deployment приложения , нужно также нажать на : и ввести deployments. У нас один деплоймент в этом неймспейсе — kubeapp1.

  2. Отредактировать deployment можно с помощью клавиши e. Добавим env-переменную APP2_URL

Добавили в манифест env-переменную APP2_URL с полным DNS-адресом app2
Добавили в манифест env-переменную APP2_URL с полным DNS-адресом app2

После сохранения все внесённые изменения применятся в кластере Kubernetes, как и в предыдущем примере с kubeapp1.

Проверяем приложения после изменений

Теперь проверим состояние приложения. В k9s для этого достаточно нажать на название деплоймента. 

Как мы видим, все образы находятся в состоянии Running — всё работает, приложение запущено.

Зайдём в один из подов и вызовем оттуда app2. Чтобы попасть в Shell, нужно нажать клавишу S на поде. 

Попробуем вызвать /time у app2:

curl http://kubeapp2.app2.svc.cluster.local:8891/time
Приложение доступно и успешно отвечает текущим временем
Приложение доступно и успешно отвечает текущим временем

Вызываем API app1 снаружи кластера

В разделе выше мы попробовали вызвать /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
  1. Указываем, что мы хотим получать доступ к сервису снаружи.

  2. Узнаём присвоенный приложению порт.

  3. Узнаём IP машины.

  4. Пробуем сделать запрос снаружи.

Как вы видите, 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. 

Полезные ссылки