Прим. перев.: Этой статьёй, написанной Scott Rahner — инженером в Dow Jones, мы продолжаем цикл многочисленных материалов, доступно рассказывающих о том, как устроен Kubernetes, как работают, взаимосвязаны и используются его базовые компоненты. На сей раз это практическая заметка с примером кода для создания хука в Kubernetes, демонстрируемого автором «под предлогом» автоматического создания sidecar-контейнеров.
(Автор фото — Gordon A. Maxwell, найдено на просторах интернета.)
Когда я начал изучать sidecar-контейнеры и service mesh'и, мне потребовалось разобраться в том, как работает ключевой механизм — автоматическая вставка sidecar-контейнера. Ведь в случае использования систем вроде Istio или Consul, при деплое контейнера с приложением внезапно в его pod'е появляется и уже настроенный контейнер Envoy (схожая ситуация происходит и у Conduit, о котором мы писали в начале года — прим. перев.). Что? Как? Так начались мои исследования…
Для тех, кто не знает, sidecar-контейнер — контейнер, который деплоится рядом с контейнерами приложения, чтобы каким-либо образом «помогать» этому приложению. Примером такого использования может служить прокси для управления трафиком и завершения TLS-сессий, контейнер для стриминга логов и метрик, контейнер для сканирования проблем в безопасности… Идея в том, чтобы изолировать различные аспекты всего приложения от бизнес-логики с помощью применения отдельных контейнеров для каждой функции.
Перед тем, как продолжить, обозначу свои ожидания. Цель этой статьи — не объяснить хитросплетения и сценарии использования Docker, Kubernetes, service mesh'ей и т.п., а наглядно показать один мощный подход к расширению возможностей этих технологий. Статья — для тех, кто уже знаком с применением данных технологий или, по крайней мере, немало о них прочитал. Чтобы попробовать практическую часть в действии, потребуется машина с уже настроенными Docker и Kubernetes. Простейший способ для этого — https://docs.docker.com/docker-for-windows/kubernetes/ (инструкция для Windows, которая работает и в Docker for Mac). (Прим. перев.: В качестве альтернативы пользователям Linux и *nix-систем можем предложить Minikube.)
Общая картина
Для начала давайте немного разберёмся с Kubernetes:
Kube Arch, лицензированная под CC BY 4.0
Когда вы собираетесь задеплоить что-либо в Kubernetes, необходимо отправить объект в kube-apiserver. Чаще всего это делают передачей аргументов или YAML-файла в kubectl. В таком случае сервер API перед тем, как непосредственно помещать данные в etcd и планировать соответствующие задания, проходит через несколько этапов:
Эта последовательность важна, чтобы разобраться, как работает вставка sidecar-контейнеров. В частности, нужно обратить внимание на Admission Control, в рамках которого Kubernetes валидирует и, если необходимо, модифицирует объекты перед тем, как сохранять их (подробнее об этом этапе см. в главе «Контроль допуска» этой статьи — прим. перев.). 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) => {
console.log(req.body)
console.log(req.body.request.object)
let adminResp = {response:{
allowed: true,
patch: Buffer.from("[{ "op": "add", "path": "/metadata/labels/foo", "value": "bar" }]").toString('base64'),
patchType: "JSONPatch",
}}
console.log(adminResp)
res.send(adminResp)
})
const server = https.createServer(options, app);
(index.js)
Путь к API — в данном случае это /mutate
— может быть произвольным (должен лишь в дальнейшем соответствовать YAML, передаваемому в Kubernetes) . Для него важно видеть и понимать JSON, получаемый от API-сервера. В данном случае мы не вытаскиваем ничего из JSON, но это может пригодиться в других сценариях. В приведённом же выше коде мы обновляем JSON. Для этого нужно две вещи:
- Изучить и понять JSON Patch.
- Правильно сконвертировать выражение 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
Как видно, тут всё очень просто. Возьмите образ с 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
Заметили, как мы сослались на только что созданный образ? Так же просто тут мог быть и 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"]
Название и путь здесь могут быть любыми, но я постарался сделать их настолько осмысленными, насколько возможно. Изменение пути будет означать необходимость модификации соответствующего кода в JavaScript. Важен и webhook failurePolicy
— он определяет, должен ли объект сохраняться, если хук возвращает ошибку или не срабатывает. Мы в данном случае говорим Kubernetes'у не продолжать обработку. Наконец, правила (rules
): они будут меняться в зависимости от того, на какие вызовы API вы ожидаете действий от Kubernetes. В данном случае, поскольку мы пытаемся эмулировать вставку sidecar-контейнера, нам требуется перехват запросов на создание pod'а.
Вот и всё! Так просто… но что насчёт безопасности? RBAC — это один из аспектов, который не затронут в статье. Я предполагаю, что вы запускаете пример в Minikube или же в Kubernetes, что идёт в поставке Docker for Windows/Mac. Однако расскажу ещё об одном необходимом элементе. 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'
Обратите внимание: последний этап — выводит единственную строку с root CA, закодированным в base64. Именно это требуется для конфигурации хука, так что в своих дальнейших тестах убедитесь, что скопировали эту строку в поле caBundle
файла hook.yaml
. 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;" ]
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. от переводчика
Читайте также в нашем блоге:
- «Так что же такое pod в Kubernetes?»;
- «Как обеспечивается высокая доступность в Kubernetes»;
- «Как на самом деле работает планировщик Kubernetes?»;
- «Что происходит в Kubernetes при запуске kubectl run?» Часть 1 и часть 2;
- «Понимаем RBAC в Kubernetes»;
- «Наш опыт с Kubernetes в небольших проектах» (видео доклада, включающего в себя знакомство с техническим устройством Kubernetes);
- «Что такое service mesh и почему он мне нужен?».
Автор: shurup