Найти тему
VK Cloud

Как расширить приложение в Kubernetes при помощи мультиконтейнерных подов: основные рекомендации

Оглавление

Запустить облачные микросервисы или 12-факторные приложения в Kubernetes относительно просто. Но как насчет запуска приложений, которые явно не предназначены для работы в контейнерной среде?

Перевели статью об одном из самых мощных инструментов в Kubernetes — мультиконтейнерном поде. Он позволяет менять поведение приложения, не изменяя его кода. Эта функция удобна для приложений, которые изначально не предназначены для работы в контейнерах.

Итак, посмотрим на примере.

Защита HTTP

Elasticsearch разработали до того, как контейнеры стали популярны (хотя сейчас его можно запустить в Kubernetes). И его можно рассматривать в качестве замены для устаревшего Java-приложения, которое предназначено для работы на виртуальной машине.

В этой статье я буду использовать Elasticsearch в качестве примера приложения, которое нужно улучшить с помощью мультиконтейнерных подов.

Вот очень простой, но не для продакшена, деплой и сервис для Elasticsearch:

apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
spec:
selector:
matchLabels:
app.kubernetes.io/name: elasticsearch
template:
metadata:
labels:
app.kubernetes.io/name: elasticsearch
spec:
containers:
- name: elasticsearch
image: elasticsearch:7.9.3
env:
- name: discovery.type
value: single-node
ports:
- name: http
containerPort: 9200
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
spec:
selector:
app.kubernetes.io/name: elasticsearch
ports:
- port: 9200
targetPort: 9200

Примечание

Переменная окружения dicsovery.type нужна, чтобы запустить одиночную реплику.

Elasticsearch слушает HTTP-порт 9200 по умолчанию.

Вы можете убедиться, что под работает, запустив в кластере другой под и подключившись к службе elasticsearch:

$ kubectl run -it --rm --image=curlimages/curl curl \
-- curl http://elasticsearch:9200
{
"name" : "elasticsearch-77d857c8cf-mk2dv",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "z98oL-w-SLKJBhh5KVG4kg",
"version" : {
"number" : "7.9.3",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "c4138e51121ef06a6404866cddc601906fe5c868",
"build_date" : "2020-10-16T10:36:16.141335Z",
"build_snapshot" : false,
"lucene_version" : "8.6.2",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}

Теперь предположим, что вы хотите получить модель безопасности с нулевым доверием и зашифровать весь трафик в сети. Как поступить, если в приложении нет встроенной поддержки TLS?

Примечание

Последние версии Elasticsearch поддерживают TLS, раньше эта функция была платной.

Первая мысль — выполнить терминацию TLS при помощи Nginx Ingress, поскольку Ingress маршрутизирует внешний трафик в кластере. Это не соответствует требованиям: трафик между подом входящего трафика и подом Elasticsearch может проходить по сети в незашифрованном виде.

Внешний трафик проходит через Ingress и направляется к подам
Внешний трафик проходит через Ingress и направляется к подам
Если вы завершите TLS на входе, остальной трафик не будет зашифрован
Если вы завершите TLS на входе, остальной трафик не будет зашифрован

Решение, которое соответствует требованиям, — прикрепить прокси-контейнер Nginx к поду, доступному по TLS. Тогда трафик будет зашифрован на всем пути от пользователя к поду.

Если вы включаете прокси-контейнер в модуль, то можете завершить TLS в модуле Nginx
Если вы включаете прокси-контейнер в модуль, то можете завершить TLS в модуле Nginx
До контейнера Elasticsearch трафик идет в зашифрованном виде
До контейнера Elasticsearch трафик идет в зашифрованном виде

Вот как выглядит деплоймент:

apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
spec:
selector:
matchLabels:
app.kubernetes.io/name: elasticsearch
template:
metadata:
labels:
app.kubernetes.io/name: elasticsearch
spec:
containers:
- name: elasticsearch
image: elasticsearch:7.9.3
env:
- name: discovery.type
value: single-node
- name: network.host
value: 127.0.0.1
- name: http.port
value: '9201'
- name: nginx-proxy
image: nginx:1.19.5
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d
readOnly: true
- name: certs
mountPath: /certs
readOnly: true
ports:
- name: https
containerPort: 9200
volumes:
- name: nginx-config
configMap:
name: elasticsearch-nginx
- name: certs
secret:
secretName: elasticsearch-tls
---
apiVersion: v1
kind: ConfigMap
metadata:
name: elasticsearch-nginx
data:
elasticsearch.conf: |
server {
listen 9200 ssl;
server_name elasticsearch;
ssl_certificate /certs/tls.crt;
ssl_certificate_key /certs/tls.key;
location / {
proxy_pass http://localhost:9201;
}
}

Давайте немного поясню:

  • Elasticsearch слушает localhost на порту 9201 вместо значения по умолчанию 0.0.0.0:9200. Для этого предусмотрены переменные среды network.host и http.port.
  • Новый контейнер nginx-proxy слушает порт 9200 HTTPS и передает запросы в Elasticsearch в порт 9201. Секрет elasticsearch-tls содержит сертификат и ключ TLS, их можно сгенерировать с помощью cert-manager.

Таким образом, запросы извне пода поступают в Nginx через порт 9200 HTTPS, а затем попадают в Elasticsearch через порт 9201.

-6

Вы можете проверить, что все работает, отправив HTTPS-запрос из кластера:

kubectl run -it --rm --image=curlimages/curl curl \
-- curl -k https://elasticsearch:9200
{
"name" : "elasticsearch-5469857795-nddbn",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "XPW9Z8XGTxa7snoUYzeqgg",
"version" : {
"number" : "7.9.3",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "c4138e51121ef06a6404866cddc601906fe5c868",
"build_date" : "2020-10-16T10:36:16.141335Z",
"build_snapshot" : false,
"lucene_version" : "8.6.2",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}

Примечание

Ключ -k нужен для самоподписанных сертификатов TLS. В продакшене необходимо использовать доверенный сертификат.

Беглый взгляд на логи показывает, что запрос прошел через прокси Nginx:

kubectl logs elasticsearch-5469857795-nddbn nginx-proxy | grep curl
10.88.4.127 - - [26/Nov/2020:02:37:07 +0000] "GET / HTTP/1.1" 200 559 "-" "curl/7.73.0-DEV" "-"

Вы также можете убедиться: нельзя подключиться к Elasticsearch через незашифрованные соединения.

kubectl run -it --rm --image=curlimages/curl curl \
-- curl http://elasticsearch:9200
<html>
<head><title>400 The plain HTTP request was sent to HTTPS port</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The plain HTTP request was sent to HTTPS port</center>
<hr><center>nginx/1.19.5</center>
</body>
</html>

Поздравляю, вы включили TLS, не меняя кода Elasticsearch или образа в контейнере!

Практика добавления прокси-контейнера в под распространена, поэтому у нее есть свое имя — шаблон Ambassador.

Примечание

Все шаблоны в этом посте подробно описаны в отличной статье от Google.

Добавление базовой поддержки TLS — только начало. Вот еще несколько вещей, которые можно сделать с шаблоном Ambassador:

  1. Чтобы зашифровать весь трафик в кластере с помощью сертификата TLS, устанавливают прокси-сервер Nginx (или другой) в каждом поде кластера. Можно пойти еще дальше и использовать взаимный TLS, он гарантирует — все запросы аутентифицированы и зашифрованы. Это основной подход, в котором используют сервисные сети, например, Istio или Linkerd.
  2. Можно использовать прокси, он гарантирует, что центр OAuth аутентифицирует все запросы, проверяя jwt. Одним из примеров — gcp-iap-auth, он проверяет, что запросы аутентифицированы GCP Identity-Aware Proxy.
  3. Можно подключиться к внешней базе данных через безопасный туннель. Это особенно удобно для баз данных без встроенной поддержки TLS, например для старых версий Redis. Другой пример — Google Cloud SQL Proxy.

Принцип работы мультиконтейнерных подов

Давайте вернемся назад и посмотрим, в чем разница между подами и контейнерами в Kubernetes. Это поможет лучше понять происходящее под капотом.

Традиционный контейнер, например запущенный при помощи Docker, обеспечивает несколько форм изоляции:

  • Изоляция ресурсов, например ограничение памяти.
  • Изоляция процессов.
  • Файловая система и изоляция монтирования.
  • Сетевая изоляция.

Примечание

Docker позволяет настраивать и другие вещи, но эти — наиболее важные.

Инструменты под капотом — пространства имен Linux и контрольные группы (cgroups).

Контрольные группы — удобный способ ограничить ресурсы, которые может использовать конкретный процесс, например CPU или память. Допустим, вы указываете: процесс может использовать только 2 ГБ памяти и одно из четырех ядер CPU.

В то же время пространства имен отвечают за изоляцию процесса и ограничивают то, что он может видеть. Например, процесс может видеть только сетевые пакеты, непосредственно с ним связанные. И не может видеть все сетевые пакеты, проходящие через сетевой адаптер.

Другой пример — вы можете изолировать файловую систему и заставить процесс поверить, что у него есть доступ ко всей системе.

Начиная с версии ядра 5.6, есть восемь видов пространств имен, и mount — одно из них
Начиная с версии ядра 5.6, есть восемь видов пространств имен, и mount — одно из них
Используя пространство имен mount, вы заставляете процесс поверить, что ему доступны все каталоги на хосте, хотя это не так
Используя пространство имен mount, вы заставляете процесс поверить, что ему доступны все каталоги на хосте, хотя это не так
Пространство имен mount предназначено для изоляции ресурсов, в данном случае — файловой системы
Пространство имен mount предназначено для изоляции ресурсов, в данном случае — файловой системы
Каждый процесс видит одну и ту же файловую систему, но при этом изолирован от других
Каждый процесс видит одну и ту же файловую систему, но при этом изолирован от других

Примечание

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

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

Изоляция сети происходит на уровне пода. Другими словами: у каждого контейнера в поде — своя файловая система, таблица процессов и так далее. Но все они совместно используют одно и то же сетевое пространство имен.

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

apiVersion: v1
kind: Pod
metadata:
name: podtest
spec:
containers:
- name: c1
image: busybox
command: ['sleep', '5000']
volumeMounts:
- name: shared
mountPath: /shared
- name: c2
image: busybox
command: ['sleep', '5000']
volumeMounts:
- name: shared
mountPath: /shared
volumes:
- name: shared
emptyDir: {}

Немного разберем:

  • Есть два контейнера, какое-то время оба просто спят.
  • Есть том emptyDir, по сути, это временный локальный том, он действует в течение всего срока службы пода.
  • Том emptyDir монтируется в каждом контейнере в каталоге /shared.

Вы можете убедиться, что том смонтирован в первом контейнере, используя kubectl exec:

$ kubectl exec -it podtest --container c1 -- sh

Команда подключает терминальный сеанс к контейнеру c1 в поде podtest. Вы можете проверить прикрепленные тома с1 с помощью команды:

$ mount | grep shared
/dev/vda1 on /shared type ext4 (rw,relatime)

Как видите, том смонтирован на /shared — общем томе, который мы создали ранее. Теперь создадим несколько файлов:

$ echo "foo" > /tmp/foo
$ echo "bar" > /shared/bar

Проверим те же файлы из второго контейнера. Сначала подключимся к нему с помощью команды:

$ kubectl exec -it podtest --container c2 -- sh

$ cat /shared/bar
bar
cat /tmp/foo
cat: can't open '/tmp/foo': No such file or directory

Как видите, файл, созданный в общем каталоге, доступен в обоих контейнерах, а файл в /tmp — нет. Так случилось, поскольку за исключением тома, файловые системы контейнеров полностью изолированы друг от друга.

Теперь посмотрим на сеть и изоляцию процессов. Простой способ понять, как настроена сеть, — использовать команду ip link, она показывает сетевые устройства системы Linux.

Выполним команду в первом контейнере:

$ kubectl exec -it podtest -c c1 -- ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
178: eth0@if179: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
link/ether 46:4c:58:6c:da:37 brd ff:ff:ff:ff:ff:ff

А теперь ту же команду в другом контейнере:

$ kubectl exec -it podtest -c c2 -- ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
178: eth0@if179: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
link/ether 46:4c:58:6c:da:37 brd ff:ff:ff:ff:ff:ff

Вы можете видеть, что в обоих контейнерах есть:

  • Один и тот же девайс eth0.
  • Один и тот же MAC-адрес 46:4c:58:6c:da:37.

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

Теперь давайте посмотрим на совместное использование сети в действии. Подключимся к первому контейнеру с помощью команды:

$ kubectl exec -it podtest -c c1 -- sh

Запустим очень простой сетевой листенер nc:

$ nc -lk -p 5000 127.0.0.1 -e 'date'

Команда запускает листенер на локальном хосте на порту 5000 и показывает дату любому подключенному TCP-клиенту. Может ли к нему подключиться второй контейнер? Откроем терминал во втором контейнере с помощью команды:

$ kubectl exec -it podtest -c c2 -- sh

Посмотрите сами — второй контейнер может подключиться к сетевому порту, но не видит процесс nc:

$ telnet localhost 5000
Connected to localhost
Sun Nov 29 00:57:37 UTC 2020
Connection closed by foreign host
$ ps aux
PID USER TIME COMMAND
1 root 0:00 sleep 5000
73 root 0:00 sh
81 root 0:00 ps aux

Подключившись через telnet, вы можете увидеть дату, это доказывает, что листенер nc работает. Но ps aux, показывающий все процессы в контейнере, вообще не отображает nc. Это связано с тем, что у контейнеров внутри модуля есть изоляция процессов, но нет изоляции сети.

И это объясняет, как работает паттерн Ambassador:

  • Поскольку все контейнеры используют одно и то же сетевое пространство имен, то один контейнер может прослушивать все подключения — даже внешние.
  • Остальные контейнеры принимают соединения только от localhost, отклоняя любые внешние соединения.

Контейнер, который получает внешний трафик, называют Ambassador — отсюда и название шаблона.

-11

Примечание

Поскольку сетевое пространство имен общее, то несколько контейнеров в поде не могут прослушивать один и тот же порт!

Давайте посмотрим на другие варианты использования многоконтейнерных подов.

Предоставление метрик по стандартному интерфейсу

Допустим, вы используете Prometheus для мониторинга всех сервисов в вашем кластере Kubernetes. Но есть приложения, которые изначально не экспортируют метрики Prometheus, например Elasticsearch.

Можете ли вы добавить метрики Prometheus в свои поды, не меняя код приложения? Можете, если используете шаблон адаптера.

В примере с Elasticsearch давайте добавим в под контейнер «экспортер», он предоставляет метрики Elasticsearch в формате Prometheus. Это легко сделать, поскольку для Elasticsearch есть экспортер с открытым исходным кодом (вам нужно добавить соответствующий порт в сервис).

apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
spec:
selector:
matchLabels:
app.kubernetes.io/name: elasticsearch
template:
metadata:
labels:
app.kubernetes.io/name: elasticsearch
spec:
containers:
- name: elasticsearch
image: elasticsearch:7.9.3
env:
- name: discovery.type
value: single-node
ports:
- name: http
containerPort: 9200
- name: prometheus-exporter
image: justwatch/elasticsearch_exporter:1.1.0
args:
- '--es.uri=http://localhost:9200'
ports:
- name: http-prometheus
containerPort: 9114
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
spec:
selector:
app.kubernetes.io/name: elasticsearch
ports:
- name: http
port: 9200
targetPort: http
- name: http-prometheus
port: 9114
targetPort: http-prometheus

Как только вы примените этот манифест, метрики станут доступны на порту 9114.

$ kubectl run -it --rm --image=curlimages/curl curl \
-- curl -s elasticsearch:9114/metrics | head
# HELP elasticsearch_breakers_estimated_size_bytes Estimated size in bytes of breaker
# TYPE elasticsearch_breakers_estimated_size_bytes gauge
elasticsearch_breakers_estimated_size_bytes{breaker="accounting",name="elasticsearch-ss86j"} 0
elasticsearch_breakers_estimated_size_bytes{breaker="fielddata",name="elasticsearch-ss86j"} 0
elasticsearch_breakers_estimated_size_bytes{breaker="in_flight_requests",name="elasticsearch-ss86j"} 0
elasticsearch_breakers_estimated_size_bytes{breaker="model_inference",name="elasticsearch-ss86j"} 0
elasticsearch_breakers_estimated_size_bytes{breaker="parent",name="elasticsearch-ss86j"} 1.61106136e+08
elasticsearch_breakers_estimated_size_bytes{breaker="request",name="elasticsearch-ss86j"} 16440
# HELP elasticsearch_breakers_limit_size_bytes Limit size in bytes for breaker
# TYPE elasticsearch_breakers_limit_size_bytes gauge

Еще раз — вы можете изменить поведение приложения, фактически не меняя код или образы контейнеров. В данном случае мы предоставили стандартизированные метрики Prometheus, которые используют кластерные инструменты (такие, как оператор Prometheus). И таким образом добились хорошего разделения между приложением и базовой инфраструктурой.

Просмотр логов

Давайте посмотрим на паттерн Sidecar, с его помощью в под добавляют улучшающий приложение контейнер.

Шаблон Sidecar — общий и применяется в различных вариантах использования. Так, вы можете услышать, что любые контейнеры в модуле, кроме первого, называют «sidecars».

Сначала рассмотрим один из классических вариантов использования — log tailing sidecar.

В контейнерной среде рекомендуют всегда писать в стандартный вывод, чтобы собирать и агрегировать журналы логов централизованно. Но многие старые приложения разработаны для логирования в файлы, и изменить это иногда нетривиальная задача. Благодаря log tailing sidecar вам, возможно, не придется ничего менять!

Вернемся к примеру с Elasticsearch. Он немного надуманный, поскольку контейнер Elasticsearch по умолчанию ведет журнал в стандартном режиме, и нетривиально заставить его писать в файл.

Вот как выглядит деплой:

apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
labels:
app.kubernetes.io/name: elasticsearch
spec:
selector:
matchLabels:
app.kubernetes.io/name: elasticsearch
template:
metadata:
labels:
app.kubernetes.io/name: elasticsearch
spec:
containers:
- name: elasticsearch
image: elasticsearch:7.9.3
env:
- name: discovery.type
value: single-node
- name: path.logs
value: /var/log/elasticsearch
volumeMounts:
- name: logs
mountPath: /var/log/elasticsearch
- name: logging-config
mountPath: /usr/share/elasticsearch/config/log4j2.properties
subPath: log4j2.properties
readOnly: true
ports:
- name: http
containerPort: 9200
- name: logs
image: alpine:3.12
command:
- tail
- -f
- /logs/docker-cluster_server.json
volumeMounts:
- name: logs
mountPath: /logs
readOnly: true
volumes:
- name: logging-config
configMap:
name: elasticsearch-logging
- name: logs
emptyDir: {}

Примечание

Файл конфигурации ведения журнала логов — отдельный файл ConfigMap, он слишком большой, чтобы включать его сюда.

У обоих контейнеров есть общий том с именем logs. Контейнер Elasticsearch записывает журналы на этот том, а контейнер журналов логов — читает из соответствующего файла и выводит его в стандартный формат.

Вы можете получить поток журнала логов, указав нужный контейнер в kubectl logs:

$ kubectl logs elasticsearch-6f88d74475-jxdhl logs | head
{
"type": "server",
"timestamp": "2020-11-29T23:01:42,849Z",
"level": "INFO",
"component": "o.e.n.Node",
"cluster.name": "docker-cluster",
"node.name": "elasticsearch-6f88d74475-jxdhl",
"message": "version[7.9.3], pid[7], OS[Linux/5.4.0-52-generic/amd64], JVM"
}
{
"type": "server",
"timestamp": "2020-11-29T23:01:42,855Z",
"level": "INFO",
"component": "o.e.n.Node",
"cluster.name": "docker-cluster",
"node.name": "elasticsearch-6f88d74475-jxdhl",
"message": "JVM home [/usr/share/elasticsearch/jdk]"
}
{
"type": "server",
"timestamp": "2020-11-29T23:01:42,856Z",
"level": "INFO",
"component": "o.e.n.Node",
"cluster.name": "docker-cluster",
"node.name": "elasticsearch-6f88d74475-jxdhl",
"message": "JVM arguments [...]"
}

Самое прекрасное в Sidecar: потоковая передача в стандартный вывод — не единственный вариант. Если нужно переключиться на настраиваемую службу агрегации журналов логов, то можно просто изменить контейнер Sidecar, ничего не меняя в приложении.

Другие примеры использования Sidecars

Есть множество вариантов использования Sidecars, контейнер логов — только один и довольно простой пример.

Вот еще несколько вариантов, которые могут вам пригодиться:

  • Перезагрузка ConfigMaps в реальном времени без перезапуска модуля.
  • Внедрение секретов из Hashicorp Vault в ваше приложение.
  • Добавление локального экземпляра Redis в приложение для кэширования в памяти с малой задержкой.

Подготовка пода к запуску

Во всех примерах многоконтейнерных подов, которые мы рассмотрели выше, несколько контейнеров работают одновременно.

Еще в Kubernetes можно запускать контейнеры инициализации — контейнеры, которые выполняются и завершаются до запуска «обычных» контейнеров. Это позволяет запустить сценарий инициализации до того, как под запустится полностью.

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

Давайте посмотрим на примере Elasticsearch. В документации Elasticsearch рекомендуют устанавливать параметр sysctl vm.max_map_count в продакшен развертываниях. В контейнерных средах это проблематично — для sysctl нет изоляции на уровне контейнера, и любые изменения происходят на уровне узла.

Что делать с этим, если вы не можете настраивать узлы Kubernetes? Один из способов — запустить Elasticsearch в привилегированном контейнере. Это позволяет Elasticsearch изменять системные настройки на своем хост-узле и сценарий точки входа для добавления sysctl.

Но это чрезвычайно опасно с точки зрения безопасности! Если служба Elasticsearch будет скомпрометирована, то злоумышленник получил бы root-доступ к своему хост-узлу.

Чтобы снизить этот риск, можно использовать контейнер инициализации:

apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
spec:
selector:
matchLabels:
app.kubernetes.io/name: elasticsearch
template:
metadata:
labels:
app.kubernetes.io/name: elasticsearch
spec:
initContainers:
- name: update-sysctl
image: alpine:3.12
command: ['/bin/sh']
args:
- -c
- |
sysctl -w vm.max_map_count=262144
securityContext:
privileged: true
containers:
- name: elasticsearch
image: elasticsearch:7.9.3
env:
- name: discovery.type
value: single-node
ports:
- name: http
containerPort: 9200

Под устанавливает sysctl в привилегированном контейнере инициализации, после чего контейнер Elasticsearch запускается, как и ожидалось. Вы по-прежнему используете привилегированный контейнер, что не идеально. Но он, по крайней мере, маленький и работает недолго, так что поверхность атаки намного ниже.

Примечание

Этот подход рекомендуют в Elastic Cloud Operator.

Использование привилегированного контейнера инициализации для подготовки узла к запуску пода — распространенный шаблон. Например, Istio использует контейнеры инициализации для настройки правил iptables при каждом запуске пода.

Еще одна причина использовать контейнер инициализации — подготовить файловую систему пода. Один из распространенных вариантов использования — управление секретами.

Еще варианты использования контейнера инициализации

Если для управления секретами вместо секретов Kubernetes вы используете что-то вроде HashicCorp Vault, то можете получить секреты в контейнере инициализации и сохранить их на общем томе emptyDir.

apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app.kubernetes.io/name: myapp
spec:
selector:
matchLabels:
app.kubernetes.io/name: myapp
template:
metadata:
labels:
app.kubernetes.io/name: myapp
spec:
initContainers:
- name: get-secret
image: vault
volumeMounts:
- name: secrets
mountPath: /secrets
command: ['/bin/sh']
args:
- -c
- |
vault read secret/my-secret > /secrets/my-secret
containers:
- name: myapp
image: myapp
volumeMounts:
- name: secrets
mountPath: /secrets
volumes:
- name: secrets
emptyDir: {}

Теперь секрет secret/my-secret доступен в файловой системе для контейнера myapp.

Это основная идея того, как работают системы вроде инжектора Sidecar Vault Agent. Однако на практике они намного сложнее: комбинируют изменяющие веб-хуки, контейнеры инициализации и вспомогательные Sidecar.

Вот еще несколько причин использовать контейнер инициализации:

  • Вы хотите, чтобы сценарий миграции базы данных запускался перед вашим приложением. Обычно это можно сделать в сценарии точки входа, но с помощью специального контейнера иногда проще.
  • Вы хотите получить большой файл из S3 или GCS, от которого зависит ваше приложение. В данному случае контейнер инициализации помогает избежать раздувания контейнера вашего приложения.

Заключение

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

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

-12

Автор: Денис Романенко

Источник: https://mcs.mail.ru/blog/multkontejnernye-pody-v-kubernetes

Читайте также статьи по теме:
11 факапов PRO-уровня при внедрении Kubernetes и как их избежать
Как жили до Kubernetes: сравниваем самый популярный оркестратор с другими решениями
Как предоставить доступ к кластеру Kubernetes с помощью клиентского сертификата: простое руководство