Летнее утро началось с ласковых лучей солнца, оторвавших меня от некрепкого сна. Понежевшись в его лучах (а также лучах экрана смартфона) я отправился на кухню и начал готовить незамысловатый завтрак. В очередной раз на этой неделе меня ждали разогретые в микроволновке макароны с сосисками. Задумчиво пережевывая простое блюдо, лишенное сильных гастрономических качеств, я понял, что надо как-то растрясти обыденность и сделать что-то странное. Например, запустить Kubernetes на своем макбуке.
Зачем? Конечно же, потому что это круто. Но помимо этого я прочитал комментарий
на оранжевом сайте, что люди запускают kube-apiserver
отдельно, чтобы
использовать такие концепты куберов, как reconcilliation loop1 и
декларативную модель, но не для запуска приложений, а для каких-то других целей.
Доказательств этому я в интернете не нашел, поэтому решил, что надо послужить
примером.
Шаг 1: раздобываем бинари
Задача-минимум на сегодня - сделать так, чтобы kubectl get pods
, натравленный
на поднятый kube-apiserver
не падал с ошибкой, а возвращал ответ, что подов в
кластере нет. Для этого нам потребуется kube-apiserver
и etcd
в качестве
бекенда, хранящего его данные. Если с последним нет никаких проблем, и его можно
скачать с его страницы релизов на
GitHub2, то с
kube-apiserver
есть нюанс: по какой-то неведомой для меня причине официальных
бинарников для macOS не публикуется, так что надо собрать их самим. Скачиваем
репозиторий kubernetes
в соседнюю папку, делаем git checkout v1.23.4
, и
незамысловато пишем make kube-apiserver
3. Через пару минут из папки
_output/bin/
можно забирать наш исполняемый файл.
Эти и все последующие бинари я буду складывать в папку bin
в каталоге, в
котором мы играемся.
На данном этапе в этой папке должны быть etcd
, etcdctl
, kube-apiserver
.
Шаг 2: Запускаем etcd
Когда надо запускать одну и ту же команду с охапкой аргументов, я предпочитаю создать примитивный shell скрипт, который состоит из одного вызова. Это позволяет избежать излишней нагрузки на палец и на стрелку вверх в терминале.
Чтобы понять, какие аргументы нам нужны, попробуем просто запустить etcd
, как
он есть:
kubernetes-apiserver-mac $ bin/etcd
<snip>
{"level":"info","ts":"2022-07-27T14:32:38.003+0300","caller":"embed/etcd.go:308","msg":"starting an etcd server","etcd-version":"3.5.2","git-sha":"99018a77b","go-version":"go1.16.3","go-os":"darwin","go-arch":"amd64","max-cpu-set":8,"max-cpu-available":8,"member-initialized":true,"name":"default","data-dir":"default.etcd","wal-dir":"","wal-dir-dedicated":"","member-dir":"default.etcd/member","force-new-cluster":false,"heartbeat-interval":"100ms","election-timeout":"1s","initial-election-tick-advance":true,"snapshot-count":100000,"snapshot-catchup-entries":5000,"initial-advertise-peer-urls":["http://localhost:2380"],"listen-peer-urls":["http://localhost:2380"],"advertise-client-urls":["http://localhost:2379"],"listen-client-urls":["http://localhost:2379"],"listen-metrics-urls":[],"cors":["*"],"host-whitelist":["*"],"initial-cluster":"","initial-cluster-state":"new","initial-cluster-token":"","quota-size-bytes":2147483648,"pre-vote":true,"initial-corrupt-check":false,"corrupt-check-time-interval":"0s","auto-compaction-mode":"periodic","auto-compaction-retention":"0s","auto-compaction-interval":"0s","discovery-url":"","discovery-proxy":"","downgrade-check-interval":"5s"}
<snip>
{"level":"info","ts":"2022-07-27T14:32:38.155+0300","caller":"embed/etcd.go:553","msg":"cmux::serve","address":"127.0.0.1:2380"}
<snip>
{"level":"info","ts":"2022-07-27T14:33:55.859+0300","caller":"embed/serve.go:140","msg":"serving client traffic insecurely; this is strongly discouraged!","address":"127.0.0.1:2379"}
Похоже, что без каких-либо аргументов etcd запускается, создает директорию
etcd.default
, запускается как реплика с именем default
, слушает на порту
2379
, и делает это по простому HTTP. Вопрос с CA мы решим позже, а пока что
поменяем папку, в которую кладутся данные и имя реплики:
#!/bin/sh
# etcd.sh
# exec allows to avoid extra shell process existing for no reason.
exec etcd \
--name s1 \
--data-dir=etcd-data
Теперь из спортивного интереса попробуем подключиться к нашему новорожденному
кластеру с помощью etcdctl
(так как мы запускаемся с небезопасными
станадртными параметрами, никаких дополнительных флагов чтобы найти базу
команда не просит):
kubernetes-apiserver-mac $ bin/etcdctl member list
8e9e05c52164694d, started, s1, http://localhost:2380, http://localhost:2379, false
kubernetes-apiserver-mac $ bin/etcdctl put a b
OK
bin/etcdctl get 'a'
a
b
Отлично, кластер работает. Можно его смело выключать и удалять папку с данными.
По результатам данного шага наш код должен выглядеть примерно так
Шаг 3: Дружим etcd и kube-apiserver
Не будем останавливаться на успешно выданной базе, попробуем теперь запустить
kube-apiserver
.
kubernetes-apiserver-mac $ bin/kube-apiserver
W0727 14:44:22.375955 95614 services.go:37] No CIDR for service cluster IPs specified. Default value which was 10.0.0.0/24 is deprecated and will be removed in future releases. Please specify it using --service-cluster-ip-range on kube-apiserver.
E0727 14:44:22.581038 95614 run.go:74] "command failed" err="error creating self-signed certificates: mkdir /var/run/kubernetes: permission denied"
Увы, но с первого пинка ничего не завелось. Нам сразу пожаловались на диапазон
внутренних IP-адресов для объектов Service
и, что критичнее, кубернетес не
смог положить сертификаты в папку, так как он привык к Linux, и не создан для
той ереси, которую мы сейчас творим. Время изучать --help
:
--service-account-key-file stringArray
File containing PEM-encoded x509 RSA or ECDSA private or public keys, used to verify ServiceAccount tokens. The specified file can contain multiple keys, and the flag can be specified multiple times with different
files. If unspecified, --tls-private-key-file is used. Must be specified when --service-account-signing-key is provided
--service-account-signing-key-file string
Path to the file that contains the current private key of the service account token issuer. The issuer will sign issued ID tokens with this private key.
--service-account-issuer stringArray
Identifier of the service account token issuer. The issuer will assert this identifier in "iss" claim of issued tokens. This value is a string or URI. If this option is not a valid URI per the OpenID Discovery 1.0
spec, the ServiceAccountIssuerDiscovery feature will remain disabled, even if the feature gate is set to true. It is highly recommended that this value comply with the OpenID spec:
https://openid.net/specs/openid-connect-discovery-1_0.html. In practice, this means that service-account-issuer must be an https URL. It is also highly recommended that this URL be capable of serving OpenID discovery
documents at {service-account-issuer}/.well-known/openid-configuration. When this flag is specified multiple times, the first is used to generate tokens and all are used to determine which issuers are accepted.
--api-audiences strings
Identifiers of the API. The service account token authenticator will validate that tokens used against the API are bound to at least one of these audiences. If the --service-account-issuer flag is configured and this
flag is not, this field defaults to a single element list containing the issuer URL.
--cert-dir string
The directory where the TLS certs are located. If --tls-cert-file and --tls-private-key-file are provided, this flag will be ignored. (default "/var/run/kubernetes")
--tls-cert-file string
File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated after server cert). If HTTPS serving is enabled, and --tls-cert-file and --tls-private-key-file are not provided, a self-signed
certificate and key are generated for the public address and saved to the directory specified by --cert-dir.
--tls-private-key-file string
File containing the default x509 private key matching --tls-cert-file.
--etcd-servers strings
List of etcd servers to connect with (scheme://ip:port), comma separated.
Да, это вам не etcd
, kube-apiserver
куда требовательнее к тому, что ему
нужно для работы даже в минимальной комплектации. Методом проб и ошибок получаем
минимальный набор аргументов, чтобы угодить аписерверу:
#!/bin/sh
# kube-apiserver.sh
CERT_DIR=certs
exec bin/kube-apiserver \
--api-audiences=https://127.0.0.1:6443 \
--service-account-key-file="$CERT_DIR/sa.pub" \
--service-account-signing-key-file="$CERT_DIR/sa.key" \
--service-account-issuer=https://kubernetes.default.svc.cluster.local \
--cert-dir="$CERT_DIR" \
--etcd-servers="http://127.0.0.1:2379"
К сожалению, если основные TLS сертификаты для работы сервера еще генерируются
автоматически, то для подписи токенов ServiceAccount
ключ надо создать нам
самостоятельно. Для этого сразу заведем вспомогательный скрипт gen-certs.sh
(название намекает, что у него появится еще несколько функций):
#!/bin/env bash
# gen-certs.sh
CERT_DIR=certs
function gen_keypair() {
local name=$1
local key_file="$CERT_DIR/$name.key"
local pub_file="$CERT_DIR/$name.pub"
test -f "$key_file" || openssl genrsa -out "$key_file" 2048
test -f "$pub_file" || openssl rsa -in "$key_file" -pubout -out "$pub_file"
}
mkdir -p "$CERT_DIR"
gen_keypair sa
Теперь можно и запустить apiserver:
kubernetes-apiserver-mac $ sh gen-certs.sh && sh kube-apiserver.sh
Вывод команды совершенно неинтересный, но сервер-таки успешно запустился!
kubernetes-apiserver-mac $ curl https://localhost:6443 --cacert certs/apiserver.crt
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "Unauthorized",
"reason": "Unauthorized",
"code": 401
}
Сервер, конечно, запустился, но нас он авторизовывать не хочет. К сожалению, никакие мои эксперименты не дали способа запустить аписервер в максимально небезопасном режиме, разрешающем кому угодно ходить в кластер без какой-либо аутентификации и авторизации: можно либо отключить первое, либо второе, но при попытке отключить все сервер отказывается, и насильно отменяет анонимный доступ. Похоже, что время взять генерацию сертификатов на себя и выписать себе права для админского доступа по-честному.
По результатам данного шага наш код должен выглядеть примерно так
Шаг 4: Создаем сертификаты для доступа в кластер
Настало время дополнить наш скрипт по генерации публичных ключей созданием иерархии сертификатов.
Маленький рефакторинг
Также воспользуемся случаем, и вынесем те переменные, что
мы используем в разных скриптах (путь к папке bin
, папке с сертификатами,
имена сертификатов) в отдельный скрипт под названием config
, который только
экспортирует переменные:
# config
# certificate settings
export CERT_DIR=certs
export SA_NAME="sa"
export SA_PUB="$CERT_DIR/$SA_NAME.pub"
export SA_KEY="$CERT_DIR/$SA_NAME.key"
# binary file locations
export BIN=bin
export ETCD="$BIN/etcd"
export ETCDCTL="$BIN/etcdctl"
export KUBE_APISERVER="$BIN/kube-apiserver"
По результатам данного шага наш код должен выглядеть примерно так
Иерархия сертификатов
API kube-apiserver авторизует пользователя по его клиентскому сертификату, смотря на Canonical Name (CN) сертификата чтобы определить имя пользователя, и на Organization (O), чтобы определить группы (в случае RBAC это ClusterRole4), в которых он состоит. Это означает, что нам достаточно выписать от имени корневого CA сертификат с правильным Subject, и все доступы заработают.
В нашей текущей настройке используется стандартный
--authorization-mode=AlwaysAllow
, то есть всем аутентифицированным
пользователям дается полный доступ к кластеру. Для спортивности переключим этот
режим в RBAC
.
Для админского сертификата будем использовать группу system:masters
. Это
особая группа (захардкоженная в kube-apiserver
), которой можно делать все.
Генерируем сертификаты
На данном этапе нам надо создать два сертификата:
-
Сертификат для клиентского доступа, чтобы мы могли, наконец, залезть в кластер
-
Сертификат для kube-apiserver. Эта часть не критична, потому что kube-apiserver сам отлично справляется с генерацией серверного сертификата, но так у нас получается единый корневой CA, которому можно довериться.
#!/bin/env bash
# gen-certs.sh
set -eauxo pipefail
. config
function gen_ca() {
local name=$1
local subj=$2
local key_file="$CERT_DIR/$name.key"
local crt_file="$CERT_DIR/$name.crt"
test -f "$key_file" || openssl genrsa -out "$key_file" 2048
test -f "$crt_file" || openssl req -x509 -new -nodes -sha256 \
-days 1024 \
-subj "$subj" \
-key "$key_file" \
-out "$crt_file"
}
function gen_crt() {
local name=$1
local subj=$2
local ca=$3
local san=
if [ $# -ge 4 ]; then
san=$4
fi
local key_file="$CERT_DIR/$name.key"
local crt_file="$CERT_DIR/$name.crt"
local csr_file="$CERT_DIR/$name.csr"
local ext_file="$CERT_DIR/$name.ext"
local ca_key_file="$CERT_DIR/$ca.key"
local ca_crt_file="$CERT_DIR/$ca.crt"
local extfile=
if [ -n "$san" ]; then
printf "subjectAltName=DNS:$san" > "$ext_file"
extfile="-extfile $ext_file"
fi
test -f "$key_file" || openssl genrsa -out "$key_file" 2048
if ! [ -f "$crt_file" ]; then
openssl req -new -key "$key_file" -out "$csr_file" -subj "$subj"
openssl x509 -req -sha256 \
$extfile \
-days 1024 \
-CA "$ca_crt_file" \
-CAkey "$ca_key_file" \
-CAcreateserial \
-in "$csr_file" \
-out "$crt_file"
fi
if [ -f "$csr_file" ]; then
rm "$csr_file"
fi
if [ -f "$ext_file" ]; then
rm "$ext_file"
fi
}
function gen_keypair() {
local name=$1
local key_file="$CERT_DIR/$name.key"
local pub_file="$CERT_DIR/$name.pub"
test -f "$key_file" || openssl genrsa -out "$key_file" 2048
test -f "$pub_file" || openssl rsa -in "$key_file" -pubout -out "$pub_file"
}
mkdir -p "$CERT_DIR"
gen_ca "$CA_NAME" "/CN=kubernetes"
gen_crt kube-apiserver "/O=system:masters/CN=kube-apiserver" "$CA_NAME" localhost
gen_crt admin "/O=system:masters/CN=kubernetes-admin" "$CA_NAME"
gen_crt whodis "/CN=some-user" "$CA_NAME"
gen_keypair "$SA_NAME"
Теперь у нас есть новая иерархия сертификатов, надо освежить команду для запуска kube-apiserver:
#!/bin/sh
# kube-apiserver.sh
. config
CRT_FILE="$CERT_DIR/kube-apiserver.crt"
KEY_FILE="$CERT_DIR/kube-apiserver.key"
exec "$KUBE_APISERVER" \
--api-audiences=https://127.0.0.1:6443 \
--service-account-key-file="$SA_PUB" \
--service-account-signing-key-file="$SA_KEY" \
--service-account-issuer=https://kubernetes.default.svc.cluster.local \
--etcd-servers="http://127.0.0.1:2379" \
--client-ca-file="$CA_FILE" \
--authorization-mode=RBAC \
--tls-cert-file="$CRT_FILE" \
--tls-private-key-file="$KEY_FILE"
Из нового здесь последние четыре аргумента - авторизовываем пользователей с помощью корневого сертификата, используем RBAC, и теперь apiserver отвечает клиентам не случайным самоподписным сертификатом, а уже каноничным, выписанным в соответствии с иерархией.
После данных манипуляций можно попробовать зайти в кластер.
По результатам данного шага наш код должен выглядеть примерно так
Шаг 5: Залезаем в кластер
Здесь все просто, берем старый добрый kubectl
, и передаем ему аргументы для
того, чтобы он знал, где кластер, как в него ходить, и как ему доверять:
#!/bin/sh
# kubectl.sh
. config
CERT_NAME=admin
CRT_FILE="$CERT_DIR/$CERT_NAME.crt"
KEY_FILE="$CERT_DIR/$CERT_NAME.key"
exec kubectl \
--client-certificate="$CRT_FILE" \
--client-key="$KEY_FILE" \
--server=https://localhost:6443 \
--certificate-authority="$CA_FILE" \
"$@"
kubernetes-apiserver-mac $ sh kubectl.sh get pods
No resources found in default namespace.
Алилуя, доступ к кластеру получен! Чтобы проверить, что RBAC работает как надо,
попробуем поменять в скрипте CERT_NAME
на whodis - тестовый пользователь, не
состоящий ни в одной группе, а потому не имеющий доступов:
kubernetes-apiserver-mac $ sh kubectl.sh get pods
Error from server (Forbidden): pods is forbidden: User "some-user" cannot list resource "pods" in API group "" in the namespace "default"
Отлично, авторизация работает как надо, и у нас есть волшебный кластер, в котором не работает ни одной ноды, и полезную нагрузку он выполнять не может. Тяга к странному успешно достигнута.
По результатам данного шага наш код должен выглядеть примерно так
Воспользовавшись случаем, посмотрим, какие стандартные роли присутствуют в кластере:
kubernetes-apiserver-mac $ sh kubectl.sh get clusterrole
NAME CREATED AT
admin 2022-07-27T12:50:15Z
cluster-admin 2022-07-27T12:50:15Z
edit 2022-07-27T12:50:15Z
system:aggregate-to-admin 2022-07-27T12:50:15Z
system:aggregate-to-edit 2022-07-27T12:50:15Z
system:aggregate-to-view 2022-07-27T12:50:15Z
system:auth-delegator 2022-07-27T12:50:15Z
system:basic-user 2022-07-27T12:50:15Z
system:certificates.k8s.io:certificatesigningrequests:nodeclient 2022-07-27T12:50:15Z
system:certificates.k8s.io:certificatesigningrequests:selfnodeclient 2022-07-27T12:50:16Z
system:certificates.k8s.io:kube-apiserver-client-approver 2022-07-27T12:50:16Z
system:certificates.k8s.io:kube-apiserver-client-kubelet-approver 2022-07-27T12:50:16Z
system:certificates.k8s.io:kubelet-serving-approver 2022-07-27T12:50:16Z
system:certificates.k8s.io:legacy-unknown-approver 2022-07-27T12:50:16Z
system:controller:attachdetach-controller 2022-07-27T12:50:16Z
system:controller:certificate-controller 2022-07-27T12:50:16Z
system:controller:clusterrole-aggregation-controller 2022-07-27T12:50:16Z
system:controller:cronjob-controller 2022-07-27T12:50:16Z
system:controller:daemon-set-controller 2022-07-27T12:50:16Z
system:controller:deployment-controller 2022-07-27T12:50:16Z
system:controller:disruption-controller 2022-07-27T12:50:16Z
system:controller:endpoint-controller 2022-07-27T12:50:16Z
system:controller:endpointslice-controller 2022-07-27T12:50:16Z
system:controller:endpointslicemirroring-controller 2022-07-27T12:50:16Z
system:controller:ephemeral-volume-controller 2022-07-27T12:50:16Z
system:controller:expand-controller 2022-07-27T12:50:16Z
system:controller:generic-garbage-collector 2022-07-27T12:50:16Z
system:controller:horizontal-pod-autoscaler 2022-07-27T12:50:16Z
system:controller:job-controller 2022-07-27T12:50:16Z
system:controller:namespace-controller 2022-07-27T12:50:16Z
system:controller:node-controller 2022-07-27T12:50:16Z
system:controller:persistent-volume-binder 2022-07-27T12:50:16Z
system:controller:pod-garbage-collector 2022-07-27T12:50:16Z
system:controller:pv-protection-controller 2022-07-27T12:50:16Z
system:controller:pvc-protection-controller 2022-07-27T12:50:16Z
system:controller:replicaset-controller 2022-07-27T12:50:16Z
system:controller:replication-controller 2022-07-27T12:50:16Z
system:controller:resourcequota-controller 2022-07-27T12:50:16Z
system:controller:root-ca-cert-publisher 2022-07-27T12:50:16Z
system:controller:route-controller 2022-07-27T12:50:16Z
system:controller:service-account-controller 2022-07-27T12:50:16Z
system:controller:service-controller 2022-07-27T12:50:16Z
system:controller:statefulset-controller 2022-07-27T12:50:16Z
system:controller:ttl-after-finished-controller 2022-07-27T12:50:16Z
system:controller:ttl-controller 2022-07-27T12:50:16Z
system:discovery 2022-07-27T12:50:15Z
system:heapster 2022-07-27T12:50:15Z
system:kube-aggregator 2022-07-27T12:50:15Z
system:kube-controller-manager 2022-07-27T12:50:15Z
system:kube-dns 2022-07-27T12:50:15Z
system:kube-scheduler 2022-07-27T12:50:16Z
system:kubelet-api-admin 2022-07-27T12:50:15Z
system:monitoring 2022-07-27T12:50:15Z
system:node 2022-07-27T12:50:15Z
system:node-bootstrapper 2022-07-27T12:50:15Z
system:node-problem-detector 2022-07-27T12:50:15Z
system:node-proxier 2022-07-27T12:50:16Z
system:persistent-volume-provisioner 2022-07-27T12:50:15Z
system:public-info-viewer 2022-07-27T12:50:15Z
system:service-account-issuer-discovery 2022-07-27T12:50:16Z
system:volume-scheduler 2022-07-27T12:50:16Z
view 2022-07-27T12:50:15Z
Ох, как их много. Отметим, что в этом списке отсутствуют system:masters
и
system:anonimous
, так как они захардкожены прямо в kube-apiserver
.
Но если мы хотим делать свои контроллеры, работающие с этим сервером, выписывать
сертификат каждому приложению радикально неинтересно. Куда интереснее
пользоваться механизмом ServiceAccount
, и ходить через токены.
Попробуем завести себе sa
и украсть его токен:
kubernetes-apiserver-mac $ sh kubectl.sh create sa test
serviceaccount/test created
kubernetes-apiserver-mac $ sh kubectl.sh get secret
No resources found in default namespace.
Пришла беда, откуда не ждали. А где же токен?
Контроллеры - не часть kube-apiserver
Кубернеты внутри состоят из вагона и маленькой тележки независимых друг от друга
маленьких контроллеров, каждый из которых представляет свой reconcilliation
loop, следит только за своим объектом или даже отдельным полем объекта.
Например, за Deployment
отвечает свой контроллер, за ReplicaSet
отвечает
свой, за Service
отвечает третий, и так далее. Эти контроллеры не знают ничего
друг про друга, и коммуницируют строго через API server. Но эти контроллеры не
являются частью kube-apiserver, они живут отдельно. Во избежание целого
зоопарка отдельных исполняемых файлов они все помещены в
kube-controller-manager
, еще одну часть здорового Kubernetes кластера.
В данном случае нам из этих контроллеров интересны serviceaccount
и
serviceaccounttoken
. Закатываем рукава обратно и готовимся поднимать еще один
компонент.
Шаг 6: Запускаем kube-controller-manager
Бинарь собирается по аналогии с kube-apiserver
простой командой make kube-controller-manager
.
В отличие от kubelet
, которому можно передать адрес kube-apiserver
и все
сертификаты в аргументах командной строки, kube-controller-manager
настаивает
на kubeconfig
файле, поэтому дополним наш gen-certs.sh
генерацией
оного. Благо формат файла позволяет ссылаться на файлы с сертификатами, поэтому
генерация достаточно простая:
function gen_kubeconfig() {
local name=$1
cat << EOF > "$name.kubeconfig"
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority: $CA_FILE
server: https://localhost:6443
name: default
users:
- name: admin
user:
client-certificate: $CERT_DIR/$name.crt
client-key: $CERT_DIR/$name.key
contexts:
- context:
cluster: default
namespace: default
user: admin
name: default
current-context: default
EOF
}
gen_crt kube-controller-manager "/O=system:masters/CN=kube-controller-manager" "$CA_NAME" localhost
gen_kubeconfig kube-controller-manager
В этот раз опустим итеративный процесс, вот список параметров, которые нужны для успешного запуска:
#!/bin/sh
# kube-controller-manager.sh
. config
exec "$KUBE_CONTROLLER_MANAGER" \
--controllers=serviceaccount,serviceaccount-token \
--kubeconfig kube-controller-manager.kubeconfig \
--authentication-kubeconfig kube-controller-manager.kubeconfig \
--authorization-kubeconfig kube-controller-manager.kubeconfig \
--requestheader-client-ca-file="$CA_FILE" \
--use-service-account-credentials \
--service-account-private-key-file="$SA_KEY"
Все достаточно просто - указываем, какие контроллеры нам нужны, указываем наш
kubeconfig
три раза5, указываем, каким сертификатом авторизовать клиентские
сертификаты6, просим использовать ServiceAccount
для контроллеров и
говорим, каким ключом подписываются токены ServiceAccount
7.
Запускаем, и ждем минуту, чтобы нужные нам контроллеры успели запуститься и отработать, после можно проверять наличие секрета:
kubernetes-apiserver-mac $ kubectl.sh get secret
NAME TYPE DATA AGE
default-token-cp22w kubernetes.io/service-account-token 2 15m
test-token-62jxf kubernetes.io/service-account-token 2 4m51s
Отлично, токены появились, можно их воровать и пробовать использовать для авторизации:
#!/bin/sh
# kubectl-bearer.sh
. config
SECRET=$1
shift
TOKEN=`sh kubectl.sh get secret "$SECRET" -o jsonpath='{.data.token}' | base64 -d`
exec kubectl \
--server=https://localhost:6443 \
--certificate-authority="$CA_FILE" \
--token="$TOKEN" \
"$@"
kubernetes-apiserver-mac $ sh kubectl-bearer.sh test-token-62jxf get pods
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:test" cannot list resource "pods" in API group "" in the namespace "default"
Пользователя успешно пустило, но у него не хватило прав получить список
ресурсов. Схема работает. Можно смело создавать ServiceAccount
, настраивать
RBAC, и использовать это хтоническое нечто в своих грязных целях.
По результатам данного шага наш код должен выглядеть примерно так
На сладкое: защищаем etcd
Чтобы наш кластер был действительно production-grade, надо перевести наш etcd с http на https.
Здесь все достаточно просто, добавляем новый сертификат в наш список для
генерации, добавляем этот сертификат в аргументы etcd
, предоставляем CA для
аутентификации клиентов, и просим слушать на https. На стороне
kube-apiserver
8 говорим, что надо стучаться не по http://127.0.0.1:2379
,
а по https://localhost:2379
, и предъявляем тот же сертификат, который у нас
уже есть для kube-apiserver
. localhost
по той причине, что в SAN
сертификата для etcd
прописан именно он, а не IP:127.0.0.1
:
#!/bin/sh
# etcd.sh
. config
CRT_FILE="$CERT_DIR/etcd.crt"
KEY_FILE="$CERT_DIR/etcd.key"
# exec allows to avoid extra shell process existing for no reason.
exec "$ETCD" \
--name s1 \
--data-dir=etcd-data \
--listen-client-urls=https://127.0.0.1:2379 \
--advertise-client-urls=https://127.0.0.1:2379 \
--cert-file="$CRT_FILE" \
--key-file="$KEY_FILE" \
--client-cert-auth \
--trusted-ca-file="$CA_FILE"
#!/bin/sh
# kube-apiserver.sh
. config
CRT_FILE="$CERT_DIR/kube-apiserver.crt"
KEY_FILE="$CERT_DIR/kube-apiserver.key"
exec "$KUBE_APISERVER" \
--api-audiences=https://127.0.0.1:6443 \
--service-account-key-file="$SA_PUB" \
--service-account-signing-key-file="$SA_KEY" \
--service-account-issuer=https://kubernetes.default.svc.cluster.local \
--etcd-servers="https://localhost:2379" \
--etcd-cafile="$CA_FILE" \
--etcd-certfile="$CRT_FILE" \
--etcd-keyfile="$KEY_FILE" \
--client-ca-file="$CA_FILE" \
--authorization-mode=RBAC \
--tls-cert-file="$CRT_FILE" \
--tls-private-key-file="$KEY_FILE"
Пересоздаем сертификаты, перезапускаем компоненты, и все заверте…
Итоговое состояние нашего кода: так
-
Про декларативную модель Kubernetes можно написать отдельную статью, но общая идея reconcilliation loop в том, чтобы периодиечски сверять состояние объекта в базе данных, являющего собой запрос “хочу чтобы было так”, и состояние реального мира, и исправлять расхождение. Как пример -
ReplicaSet
считает, что у вас должно быть запущено три пода с такой конфигурацией. Если окажется, что запущено не три, а два, то третий будет создан. Если объект поменяют, сказав, что их должно быть не три, а один - лишние будут удалены. Этот же подход можно применять ко внешнему миру, например, с помощью оператора, который в соответствии с гипотетическим объектом типаDatabase
создает инстансы managed базы данных (RDS) в AWS. ↩︎ -
Тут и далее я буду хардкодить версии на те, с которыми экспериментировал, из соображений воспроизводимости^tm. ↩︎
-
Я не проверял, какие зависимости надо установить, чтобы сборка прошла успешна, но подозреваю, что надо установить компилятор
go
какой-нибудь не слишком древней версии (в моем случае 1.17.6). ↩︎ -
К сожалению, не смог раскопать информацию, всегда ли группа пользователя это именно ClusterRole, или можно как-то привязать группу к обычной Role. ↩︎
-
Почему-то в целях аутентикации и авторизации можно предоставлять отдельный
kubeconfig
, не такой же, какой используется остальным контроллером, но в наших целях это не очень полезно. Я даже не знаю, в каких полезно. ↩︎ -
Не уверен, кто ходит с авторизацией к
kube-controller-manager
, хорошая тема для дальнейшего исследования ↩︎ -
Этот ключ передается и в
kube-apiserver
, и вkube-controller-manager
. Не уверен, могут ли они быть различными, возможно,kube-apiserver
использует их для OIDC токенов, аkube-controller-manager
дляServiceAccount
токенов. ↩︎ -
То, что из всех компонентов Kubernetes кластера доступ к
etcd
имеет толькоkube-apiserver
, а все остальные взаимодействуют друг с другом только через него - это гениальный инжиниринг, позволивший альтернативные дистрибутивы вродеk3s
, которые могут использовать под капотом другие базы навродеsqlite
. К сожалению, в официальном дистрибутиве поддержки других баз так и не появилось. ↩︎