Хабрахабр

[Перевод] Как этот sidecar-контейнер оказался здесь [в Kubernetes]?

Прим. перев.: Этой статьёй, написанной Scott Rahner — инженером в Dow Jones, мы продолжаем цикл многочисленных материалов, доступно рассказывающих о том, как устроен Kubernetes, как работают, взаимосвязаны и используются его базовые компоненты. На сей раз это практическая заметка с примером кода для создания хука в Kubernetes, демонстрируемого автором «под предлогом» автоматического создания sidecar-контейнеров.

Maxwell, найдено на просторах интернета.)
(Автор фото — Gordon A.

Ведь в случае использования систем вроде Istio или Consul, при деплое контейнера с приложением внезапно в его pod'е появляется и уже настроенный контейнер Envoy (схожая ситуация происходит и у Conduit, о котором мы писали в начале года — прим. Когда я начал изучать sidecar-контейнеры и service mesh'и, мне потребовалось разобраться в том, как работает ключевой механизм — автоматическая вставка sidecar-контейнера. Что? перев.). Так начались мои исследования… Как?

Примером такого использования может служить прокси для управления трафиком и завершения TLS-сессий, контейнер для стриминга логов и метрик, контейнер для сканирования проблем в безопасности… Идея в том, чтобы изолировать различные аспекты всего приложения от бизнес-логики с помощью применения отдельных контейнеров для каждой функции. Для тех, кто не знает, sidecar-контейнер — контейнер, который деплоится рядом с контейнерами приложения, чтобы каким-либо образом «помогать» этому приложению.

Цель этой статьи — не объяснить хитросплетения и сценарии использования Docker, Kubernetes, service mesh'ей и т.п., а наглядно показать один мощный подход к расширению возможностей этих технологий. Перед тем, как продолжить, обозначу свои ожидания. Чтобы попробовать практическую часть в действии, потребуется машина с уже настроенными Docker и Kubernetes. Статья — для тех, кто уже знаком с применением данных технологий или, по крайней мере, немало о них прочитал. (Прим. Простейший способ для этого — https://docs.docker.com/docker-for-windows/kubernetes/ (инструкция для Windows, которая работает и в Docker for Mac). перев.: В качестве альтернативы пользователям Linux и *nix-систем можем предложить Minikube.)

Общая картина

Для начала давайте немного разберёмся с Kubernetes:

0
Kube Arch, лицензированная под CC BY 4.

Чаще всего это делают передачей аргументов или YAML-файла в kubectl. Когда вы собираетесь задеплоить что-либо в Kubernetes, необходимо отправить объект в kube-apiserver. В таком случае сервер API перед тем, как непосредственно помещать данные в etcd и планировать соответствующие задания, проходит через несколько этапов:

В частности, нужно обратить внимание на Admission Control, в рамках которого Kubernetes валидирует и, если необходимо, модифицирует объекты перед тем, как сохранять их (подробнее об этом этапе см. Эта последовательность важна, чтобы разобраться, как работает вставка sidecar-контейнеров. перев.). в главе «Контроль допуска» этой статьи — прим. Kubernetes также позволяет регистрировать webhooks, которые могут выполнять определяемую пользователем валидацию и изменения (mutations).

Мне пришлось потратить несколько дней на чтение и перечитывание документации, а также на анализ кода Istio и Consul. Однако процесс создания и регистрации своих хуков не так-то уж прост и хорошо документирован. А когда дело дошло до кода для некоторых из ответов API, я провёл не менее половины дня на выполнение случайных проб и ошибок.

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

Код

Название webhook говорит само за себя ­— это HTTP endpoint, реализующий API, определённый в Kubernetes. Вы создаёте API-сервер, который Kubernetes может вызывать перед тем, как разбираться с Deployment'ами. Здесь мне пришлось столкнуться со сложностями, поскольку доступны всего несколько примеров, некоторые из которых — просто unit-тесты Kubernetes, другие — спрятаны посреди огромной кодовой базы… и все написаны на Go. Но я выбрал более доступный вариант —  Node.js:

const app = express();
app.use(bodyParser.json()); app.post('/mutate', (req, res) => ]").toString('base64'), patchType: "JSONPatch", }} console.log(adminResp) res.send(adminResp)
}) const server = https.createServer(options, app);

(index.js)

Для него важно видеть и понимать JSON, получаемый от API-сервера. Путь к API — в данном случае это /mutate — может быть произвольным (должен лишь в дальнейшем соответствовать YAML, передаваемому в Kubernetes) . В приведённом же выше коде мы обновляем JSON. В данном случае мы не вытаскиваем ничего из JSON, но это может пригодиться в других сценариях. Для этого нужно две вещи:

  1. Изучить и понять JSON Patch.
  2. Правильно сконвертировать выражение JSON Patch в массив байтов, закодированный с base64.

Как только это сделано, достаточно лишь передать API-серверу ответ с очень простым объектом. В данном случае мы добавляем лейбл foo=bar любому попадающему к нам pod'у.

Deployment

Хорошо, у нас есть код, который принимает запросы от API-сервера Kubernetes и отвечает на них, но как его задеплоить? И как заставить Kubernetes перенаправлять нам эти запросы? Развернуть такой endpoint можно везде, до чего может «достучаться» API-сервер Kubernetes. Простейшим способом является деплой кода в сам кластер Kubernetes, что мы и сделаем в данного примере. Я постарался сделать пример максимально простым, поэтому для всех действий использую лишь Docker и kubectl. Начнём с создания контейнера, в котором будет запускаться код:

FROM node:8 USER node
WORKDIR /home/node COPY index.js .
COPY package.json . RUN npm install # позже сюда добавятся дополнительные команды для TLS CMD node index.js

(Dockerfile)

Возьмите образ с node от сообщества и забросьте в него код. Как видно, тут всё очень просто. Теперь можно выполнить простую сборку:

docker build . -t localserver

Следующим шагом создадим Deployment в Kubernetes:

apiVersion: apps/v1
kind: Deployment
metadata: name: webhook-server
spec: replicas: 1 selector: matchLabels: component: webhook-server template: metadata: labels: component: webhook-server spec: containers: - name: webhook-server imagePullPolicy: Never
image: localserver

(deployment.yaml)

Так же просто тут мог быть и pod, и что-либо иное, к чему мы можем подключить сервис в Kubernetes. Заметили, как мы сослались на только что созданный образ? Теперь определим этот Service:

apiVersion: v1
kind: Service
metadata: name: webhook-service
spec: ports: - port: 443 targetPort: 8443 selector:
component: webhook-server

Так в Kubernetes появится endpoint с внутренним именем, который указывает на наш контейнер. Финальный шаг — сообщить Kubernetes'у, что мы хотим, чтобы API-сервер вызывал этот сервис, когда он готов производить изменения (mutations):

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata: name: webhook
webhooks: - name: webhook-service.default.svc failurePolicy: Fail clientConfig: service: name: webhook-service namespace: default path: "/mutate" # далее записан результат base64-кодирования файла rootCA.crt # с помощью команды `cat rootCA.crt | base64 | tr -d '\n'` # подробнее об этом см. ниже caBundle: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUdHekNDQkFPZ0F3SUJBZ0lKQU1jcTN6UHZDQUd0TUEwR0NTcUdTSWIzRFFFQkN3VUFNSUdqTVFzd0NRWUQKVlFRR0V3SlZVekVUTUJFR0ExVUVDQXdLVG1WM0lFcGxjbk5sZVRFVE1CRUdBMVVFQnd3S1VISnBibU5sZEc5dQpJREVTTUJBR0ExVUVDZ3dKUkc5M0lFcHZibVZ6TVF3d0NnWURWUVFMREFOUVNVSXhIakFjQmdOVkJBTU1GWGRsClltaHZiMnN0S2k1a1pXWmhkV3gwTG5OMll6RW9NQ1lHQ1NxR1NJYjNEUUVKQVJZWmMyTnZkSFF1Y21Gb2JtVnkKUUdSdmQycHZibVZ6TG1OdmJUQWVGdzB4T0RFd016RXhOalU1TURWYUZ3MHlNVEE0TWpBeE5qVTVNRFZhTUlHagpNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1RtVjNJRXBsY25ObGVURVRNQkVHQTFVRUJ3d0tVSEpwCmJtTmxkRzl1SURFU01CQUdBMVVFQ2d3SlJHOTNJRXB2Ym1Wek1Rd3dDZ1lEVlFRTERBTlFTVUl4SGpBY0JnTlYKQkFNTUZYZGxZbWh2YjJzdEtpNWtaV1poZFd4MExuTjJZekVvTUNZR0NTcUdTSWIzRFFFSkFSWVpjMk52ZEhRdQpjbUZvYm1WeVFHUnZkMnB2Ym1WekxtTnZiVENDQWlJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dJUEFEQ0NBZ29DCmdnSUJBTHRpTU5mL1l3d0RkcHlPSUhja2FQK3J6NmdxYXBhWmZ2a0JndHVZK3BYQVZnNWc5M1RISmlPdlJYUnAKeG9UZ1o0RlA4N0V3R0NXRUZxZTRFRjh5UUxCK1NvWHBxUmRrWlVLYlM3eDVJNnNDb0h1dFJXaURpd3piV3lGawp3UnppeXpyMTQzN2wzYWxadU9VNkl5bU9mVDlETzdRaDNnY01HOEprQ09aVlVOelVIN3J4WmtieGg3M1lXNW5ZCjhSMU5tZDJ3cm1IWkVWc2JmS21GTlhvZjFueWtRcXMyMUQxT1FwQ3A1VDB5QU9penZlaW9OS3VsQVVpcjNVQ0EKSmNYYWpMMGZVS1ZIcGVTbGlhWXdKZmZNSDFqOElqSDZTdm5TdG9qQWlWdnJHb1ZKUlFqRXFLQkpYVGMyaHZCWQpCcjJqdGdQb25WWnBBTFphbktha0JTV1cyZ25oZVFKaHpKOGhkMXlEU0x6dFFKb2JkOHZUMEZ5bHZaQzY3aURnCmROb1NWbHBaQlpDSVIxTldaRVdGbTlTWWtKLzZ6emVqMFZpWnp2aFBYdm9GelZEVGZoMEwzQWljUTZlWTNzcEMKV0Fmb2VTcFUxaEVJeG92SmdwVkpMbnRaWkhyN1RJQ05CNlV5QnFVUzhEa0lTMkhnWkh2MTd1VjA3bTFzZDZDMApDUnV5YmZHQ0l2RGNwMCtzMjF6TENXemJuS3BzaFo5UkYvYWhXMW11cVN2dGt0WXlYOFVySlpKT1h3Z0NKenhLCmdwZGs3YlA4Y3ZkRWxUZDduQXRJbjZPcm42VWlVUnFpSXY1VSt0bmIvOVlrNDIxVzdlT2NxZ3JqTEY4eUo5ckIKN0hBYlhGRjM5OW5NMlBtYkZIV2FROG1xeWo0L0kxNm9tTHVsUGZvekVWK0xvMXVwQWdNQkFBR2pVREJPTUIwRwpBMVVkRGdRV0JCUnVKaTcyS0U5bWhpejZvYVhkSXlpbGpTeXhkVEFmQmdOVkhTTUVHREFXZ0JSdUppNzJLRTltCmhpejZvYVhkSXlpbGpTeXhkVEFNQmdOVkhSTUVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJDd1VBQTRJQ0FRQlQKS28wczJTTWZkSzdkRS9ZdFBwQ2lQNDVBK0xJSjVKd0l2dWdiUlNGeVRUSEU0akhVRTdQdWc3VHdGNC93YnJFZwpNN1F3OWUxbDA1M2lheWRFOS9sUlVDbzN4TnVVcU5jU2lCK3RIOE54dURHUUw5NHBuWTdTR3FuRjBDMlZ2d2x2CmxaYUQxNU41cVdvTVJrQU54VXRPRGFaWEdLcS94VVBSQWdNMHFtbXc5ZnIwaXAvQzFjVGMyVVhlejlGNTMvV2cKV1FNempWbUNTNGlnckR1a1FBNWxodFRlYUlzK3pxNk9ZeWNiN01KR1JBL0NhcnpDL1VuZExMbmhsdEtITkJhMwp0TDFVVUJCTzBMdmdMaE8zVk9nRENOazJYVmZzVHFueEUrTGp6R2dmUnRqYjE5L0p1d2V2OW00Y3ZzUlZESGVMCk9oQ0lvenorUHRLWHBwVDFWd1VRbFZlOG5ic2RiVnNZWmt4Q3llcGpMUTJ5TXNUUXdoa2NncGRiTnYzbTMvRC8Kc3N5ZS9iZnphUGFXVEE1R0d5emhXdXlENDZPT1lCUFlhZzd0aFFneXRvOWRpSWNDSHNMQ3BVZm1FQ1d6TERBYgozK2NadnZnYXZybFJCZjN2cVhrVlZxT1NLNGxna25iUEZJc0YvbnFIanM2WXI5Tktiai9sRGlBalRYaVdQdFRmClJzd0JodndveDJnK21zd0prQytId0cvckZ1RXFDdklTaFJGWlEvMDgyL0F5ekpYRlE3SlV3eHluL0dTQXlGZUsKL1Y3T01XTEhUeVd4Vkg4eVBCZ1JSVE1CK3NrOEVQQndveFRLSjZnLytTbmdkNXM1ZEx6ZDhpSTlsVHdxWDZBTApzNU1OY2NobFRWVU9RYnFGWXBKc3FTUTlIVlB2bjZDckRlTGlxTlNKQVE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==" rules: - operations: [ "CREATE" ] apiGroups: [""] apiVersions: ["v1"] resources: ["pods"]

(hook.yaml)

Изменение пути будет означать необходимость модификации соответствующего кода в JavaScript. Название и путь здесь могут быть любыми, но я постарался сделать их настолько осмысленными, насколько возможно. Мы в данном случае говорим Kubernetes'у не продолжать обработку. Важен и webhook failurePolicy — он определяет, должен ли объект сохраняться, если хук возвращает ошибку или не срабатывает. В данном случае, поскольку мы пытаемся эмулировать вставку sidecar-контейнера, нам требуется перехват запросов на создание pod'а. Наконец, правила (rules): они будут меняться в зависимости от того, на какие вызовы API вы ожидаете действий от Kubernetes.

Так просто… но что насчёт безопасности? Вот и всё! Я предполагаю, что вы запускаете пример в Minikube или же в Kubernetes, что идёт в поставке Docker for Windows/Mac. RBAC — это один из аспектов, который не затронут в статье. API-сервер Kubernetes обращается только к endpoint'ам с HTTPS, поэтому для приложения потребуется наличие SSL-сертификатов. Однако расскажу ещё об одном необходимом элементе. Также потребуется сообщить Kubernetes'у, кто является удостоверяющим центром корневого сертификата.

TLS

Только для демонстрационных целей(!!!) я добавил в Dockerfile немного кода, чтобы создать root CA и воспользоваться им для подписи сертификата:

RUN openssl genrsa -out rootCA.key 4096
RUN openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt \ -subj "/C=US/ST=New Jersey/L=Princeton /O=Dow Jones/OU=PIB/CN=*.default.svc/emailAddress=scott.rahner@dowjones.com"
RUN openssl genrsa -out webhook.key 4096
RUN openssl req -new -key webhook.key -out webhook.csr \ -subj "/C=US/ST=New Jersey/L=Princeton /O=Dow Jones/OU=PIB/CN=webhook-service.default.svc/emailAddress=scott.rahner@dowjones.com"
RUN openssl x509 -req -in webhook.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out webhook.crt -days 1024 -sha256
RUN cat rootCA.crt | base64 | tr -d '\n'

(Dockerfile)

Именно это требуется для конфигурации хука, так что в своих дальнейших тестах убедитесь, что скопировали эту строку в поле caBundle файла hook.yaml. Обратите внимание: последний этап — выводит единственную строку с root CA, закодированным в base64. Dockerfile забрасывает сертификаты прямо в WORKDIR, так что JavaScript просто забирает их оттуда и использует для сервера:

const privateKey = fs.readFileSync('webhook.key').toString();
const certificate = fs.readFileSync('webhook.crt').toString();
//…
const options = {key: privateKey, cert: certificate};
const server = https.createServer(options, app);

Теперь код поддерживает запуск HTTPS, а также сообщил Kubernetes'у, где найти нас и какому удостоверяющему центру доверять. Осталось лишь задеплоить всё это в кластер:

kubectl create -f deployment.yaml
kubectl create -f service.yaml
kubectl create -f hook.yaml

Резюмируем

  • Deployment.yaml запускает контейнер, который обслуживает hook API по HTTPS и возвращает JSON Patch для изменения объекта.
  • Service.yaml обеспечивает для контейнера endpoint — webhook-service.default.svc.
  • Hook.yaml говорит API-серверу, где нас найти: https://webhook-service.default.svc/mutate.

Попробуем в деле!

Всё развёрнуто в кластере — время попробовать код в действии, что мы сделаем добавлением нового pod/Deployment. Если всё работает правильно, хук должен будет добавить дополнительный лейбл foo:

apiVersion: apps/v1
kind: Deployment
metadata: name: test
spec: replicas: 1 selector: matchLabels: component: test template: metadata: labels: component: test spec: containers: - name: test image: node:8 command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]

(test.yaml)

kubectl create -f test.yaml

Ок, мы увидели deployment.apps test created… но всё ли получилось?

kubectl describe pods test Name: test-6f79f9f8bd-r7tbd
Namespace: default
Node: docker-for-desktop/192.168.65.3
Start Time: Sat, 10 Nov 2018 16:08:47 -0500
Labels: component=test foo=bar

Замечательно! Хотя у test.yaml был задан единственный лейбл (component), результирующий pod получил два: component и foo.

Домашнее задание

Но подождите! Разве мы собирались использовать этот код, чтобы создать sidecar-контейнер? Я предупреждал, что покажу, как добавить sidecar… А теперь, с полученным знанием и кодом: https://github.com/dowjones/k8s-webhook — смело экспериментируйте и разбирайтесь в том, как сделать свой автоматически вставляемый sidecar. Это довольно просто: необходимо лишь подготовить правильный JSON Patch, который будет добавлять дополнительный контейнер в тестовом Deployment'е. Счастливой оркестровки!

P.S. от переводчика

Читайте также в нашем блоге:

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть