Всем привет. Меня зовут Добрый Кот Telegram.
От коллектива FR-Solutions и при поддержке @irbgeo Telegram : Продолжаем серию статей о K8S.
Цели данной статьи:
Актуализировать порядок развертывания kubernetes, описанный всеми любимым Kelsey Hightower.
Доказать что "kubernetes это всего 5-бинарей" и "kubernetes это просто" - это некорректное суждение.
Добавить Key-keeper в конфигурацию kubernetes для управления сертификатами.
Из чего состоит Kubernetes?
Все мы помним шутку "kubernetes это всего 5-бинарей":
etcd
kube-apiserver
kube-controller-manager
kube-scheduler
kubelet
Но, если мы будем оперировать только ими, то кластер вы не соберете. Почему же?
kubelet -у требуются дополнительные компоненты для работы:
Container Runtime Interface - CRI (containerd, cri-o, docker, etc.).
CRI - для работы требуется:
RUNC библиотека, для работы с контейнерами.
Certificates:
(cfssl, kubeadm, key-keeper) требуются для выпуска сертификатов.
Прочее:
kubectl (для работы с kubernetes) - опционально
crictl (для удобной работы с CRI) - опционально
etcdctl (для работы с etcd на мастерах) - опционально
kubeadm (для настройки кластера) - опционально
Таким образом, чтобы развернуть kubernetes, требуется минимум 8 бинарей.
Этапы создания кластера K8S
Создание linux машин, на которых будет развернут control-plane кластера.
-
Настройка операционной системы на созданных linux машинах:
установка базовых пакетов (для обслуживания linux).
работа с modprobe.
работа с sysctls.
установка требуемых для функционирования кластера бинарей.
подготовка конфигурационных файлов для установленных компонентов.
Подготовка Vault хранилища.
Генерация static-pod манифестов.
Проверка доступности кластера.
Как видим, всего 5-ть этапов - ничего сложного)
Ну что, давайте начнем!
1) Создаем 3 Узла под мастера и привязываем к ним DNS имена по маске:master-${INDEX}.${CLUSTER_NAME}.${BASE_DOMAIN}
** ВАЖНО: ${INDEX}
должен начинаться с 0 из-за реализации формирования индексов в модуле терраформ для VAULT, но о нем позже.
environments
## RUN ON EACH MASTER.
## REQUIRED VARS:
export BASE_DOMAIN=dobry-kot.ru
export CLUSTER_NAME=example
export BASE_CLUSTER_DOMAIN=${CLUSTER_NAME}.${BASE_DOMAIN}
# Порты для ETCD
export ETCD_SERVER_PORT="2379"
export ETCD_PEER_PORT="2380"
export ETCD_METRICS_PORT="2381"
# Порты для KUBERNETES
export KUBE_APISERVER_PORT="6443"
export KUBE_CONTROLLER_MANAGER_PORT="10257"
export KUBE_SCHEDULER_PORT="10259"
# Установите значение 1, 3, 5
export MASTER_COUNT=1
# Для Kube-apiserver
export ETCD_SERVERS=$(echo \
$(for INDEX in `seq 0 $(($MASTER_COUNT-1))`; \
do \
echo https://master-${INDEX}.${BASE_CLUSTER_DOMAIN}:${ETCD_SERVER_PORT} ; \
done) |
sed "s/,//" |
sed "s/ /,/g")
# Для формирования ETCD кластера
export ETCD_INITIAL_CLUSTER=$(echo \
$(for INDEX in `seq 0 $(($MASTER_COUNT-1))`; \
do \
echo master-${INDEX}.${BASE_CLUSTER_DOMAIN}=https://master-${INDEX}.${BASE_CLUSTER_DOMAIN}:${ETCD_PEER_PORT} ; \
done) |
sed "s/,//" |
sed "s/ /,/g")
export KUBERNETES_VERSION="v1.23.12"
export ETCD_VERSION="3.5.3-0"
export ETCD_TOOL_VERSION="v3.5.5"
export RUNC_VERSION="v1.1.3"
export CONTAINERD_VERSION="1.6.8"
export CRICTL_VERSION=$(echo $KUBERNETES_VERSION |
sed -r 's/^v([0-9]*).([0-9]*).([0-9]*)/v\1.\2.0/')
export BASE_K8S_PATH="/etc/kubernetes"
export SERVICE_CIDR="29.64.0.0/16"
# Не обижайтесь - regexp сами напишите)
export SERVICE_DNS="29.64.0.10"
export VAULT_MASTER_TOKEN="hvs.vy0dqWuHkJpiwtYhw4yPT6cC"
export VAULT_SERVER="http://193.32.219.99:9200/"
export VAULT_MASTER_TOKEN="root"
export VAULT_SERVER="http://master-0.${CLUSTER_NAME}.${BASE_DOMAIN}:9200/"
Если вы изучали документацию от Kelsey Hightower, то замечали, что в основе конфигурационных файлов лежат ip адреса узлов. Данный подход рабочий, но менее функциональный, для простоты обслуживания и дальнейшей шаблонизации лучше использовать заранее известные нам FQDN маски, как я указывал для мастеров выше.
2) Скачиваем все требуемые кластером K8S бинарные файлы.
В данном сетапе я не буду использовать RPM или DEB пакеты, чтобы постараться детально показать, из чего состоит вся инсталляция.
download components
## RUN ON EACH MASTER.
wget -O /usr/bin/key-keeper "https://storage.yandexcloud.net/m.images/key-keeper-T2?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=YCAJEhOlYpv1GRY7hghCojNX5%2F20221020%2Fru-central1%2Fs3%2Faws4_request&X-Amz-Date=20221020T123413Z&X-Amz-Expires=2592000&X-Amz-Signature=138701723B70343E38D82791A28AD1DB87040677F7C94D83610FF26ED9AF1954&X-Amz-SignedHeaders=host"
wget -O /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/${KUBERNETES_VERSION}/bin/linux/amd64/kubectl
wget -O /usr/bin/kubelet https://storage.googleapis.com/kubernetes-release/release/${KUBERNETES_VERSION}/bin/linux/amd64/kubelet
wget -O /usr/bin/kubeadm https://storage.googleapis.com/kubernetes-release/release/${KUBERNETES_VERSION}/bin/linux/amd64/kubeadm
wget -O /usr/bin/runc https://github.com/opencontainers/runc/releases/download/${RUNC_VERSION}/runc.amd64
wget -O /tmp/etcd.tar.gz https://github.com/etcd-io/etcd/releases/download/${ETCD_TOOL_VERSION}/etcd-${ETCD_TOOL_VERSION}-linux-amd64.tar.gz
wget -O /tmp/containerd.tar.gz https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VERSION}/containerd-${CONTAINERD_VERSION}-linux-amd64.tar.gz
wget -O /tmp/crictl.tar.gz https://github.com/kubernetes-sigs/cri-tools/releases/download/${CRICTL_VERSION}/crictl-${CRICTL_VERSION}-linux-amd64.tar.gz
chmod +x /usr/bin/key-keeper
chmod +x /usr/bin/kubelet
chmod +x /usr/bin/kubectl
chmod +x /usr/bin/kubeadm
chmod +x /usr/bin/runc
mkdir -p /tmp/containerd
mkdir -p /tmp/etcd
tar -C "/tmp/etcd" -xvf /tmp/etcd.tar.gz
tar -C "/tmp/containerd" -xvf /tmp/containerd.tar.gz
tar -C "/usr/bin" -xvf /tmp/crictl.tar.gz
cp /tmp/etcd/etcd*/etcdctl /usr/bin/
cp /tmp/containerd/bin/* /usr/bin/
3) Создание сервисов:
Сервисов в нашей инсталляции всего 3 (key-keeper, kubelet, containerd)
containerd.service
## RUN ON EACH MASTER.
## SETUP SERVICE FOR CONTAINERD
cat <<EOF > /etc/systemd/system/containerd.service
[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target
[Service]
ExecStartPre=/sbin/modprobe overlay
ExecStart=/usr/bin/containerd
Restart=always
RestartSec=5
Delegate=yes
KillMode=process
OOMScoreAdjust=-999
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
[Install]
WantedBy=multi-user.target
EOF
key-keeper.service
## RUN ON EACH MASTER.
## SETUP SERVICE FOR KEY-KEEPER
cat <<EOF > /etc/systemd/system/key-keeper.service
[Unit]
Description=key-keeper-agent
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/usr/bin/key-keeper -config-dir ${BASE_K8S_PATH}/pki -config-regexp .*vault-config
Restart=always
StartLimitInterval=0
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
kubelet.service
## RUN ON EACH MASTER.
## SETUP SERVICE FOR KUBELET
cat <<EOF > /etc/systemd/system/kubelet.service
[Unit]
Description=kubelet: The Kubernetes Node Agent
Documentation=https://kubernetes.io/docs/home/
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/usr/bin/kubelet
Restart=always
StartLimitInterval=0
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
kubelet.d/conf
## RUN ON EACH MASTER.
## SETUP SERVICE-CONFIG FOR KUBELET
mkdir -p /etc/systemd/system/kubelet.service.d
cat <<EOF > /etc/systemd/system/kubelet.service.d/10-fraima.conf
[Service]
EnvironmentFile=-${BASE_K8S_PATH}/kubelet/service/kubelet-args.env
ExecStart=
ExecStart=/usr/bin/kubelet \
\$KUBELET_HOSTNAME \
\$KUBELET_CNI_ARGS \
\$KUBELET_RUNTIME_ARGS \
\$KUBELET_AUTH_ARGS \
\$KUBELET_CONFIGS_ARGS \
\$KUBELET_BASIC_ARGS \
\$KUBELET_KUBECONFIG_ARGS
EOF
kubelet-args.env
## RUN ON EACH MASTER.
## SETUP SERVICE-CONFIG FOR KUBELET
mkdir -p ${BASE_K8S_PATH}/kubelet/service/
cat <<EOF > ${BASE_K8S_PATH}/kubelet/service/kubelet-args.env
KUBELET_HOSTNAME=""
KUBELET_BASIC_ARGS="
--register-node=true
--cloud-provider=external
--image-pull-progress-deadline=2m
--feature-gates=RotateKubeletServerCertificate=true
--cert-dir=/etc/kubernetes/pki/certs/kubelet
--authorization-mode=Webhook
--v=2
"
KUBELET_AUTH_ARGS="
--anonymous-auth="false"
"
KUBELET_CNI_ARGS="
--cni-bin-dir=/opt/cni/bin
--cni-conf-dir=/etc/cni/net.d
--network-plugin=cni
"
KUBELET_CONFIGS_ARGS="
--config=${BASE_K8S_PATH}/kubelet/config.yaml
--root-dir=/var/lib/kubelet
--register-node=true
--image-pull-progress-deadline=2m
--v=2
"
KUBELET_KUBECONFIG_ARGS="
--kubeconfig=${BASE_K8S_PATH}/kubelet/kubeconfig
"
KUBELET_RUNTIME_ARGS="
--container-runtime=remote
--container-runtime-endpoint=/run/containerd/containerd.sock
--pod-infra-container-image=k8s.gcr.io/pause:3.6
"
EOF
** Обратите внимание, что если вы в перспективе будете разворачивать K8S в облаке и интегрировать его с ним, то ставьте --cloud-provider=external
*** Полезной фичей может оказаться автоматический лейблинг ноды при регистрации в кластере--node-labels=node.kubernetes.io/master,foo=bar
Ниже приведен список доступных системных меток, которые можно менять:
kubelet.kubernetes.io
node.kubernetes.io
beta.kubernetes.io/arch,
beta.kubernetes.io/instance-type,
beta.kubernetes.io/os,
failure-domain.beta.kubernetes.io/region,
failure-domain.beta.kubernetes.io/zone,
kubernetes.io/arch,
kubernetes.io/hostname,
kubernetes.io/os,
node.kubernetes.io/instance-type,
topology.kubernetes.io/region,
topology.kubernetes.io/zone
Для примера, нельзя установить системные лейбл не из списка:--node-labels=node-role.kubernetes.io/master
4) Подготовка Vault.
Как мы ранее писали, сертификаты будем создавать через централизованное хранище Vault.
Для примера мы разместим опорный Vault server на master-0
в режиме dev
с уже открытым стореджом и дефолтным токеном для удобства.
Vault
## RUN ON MASTER-0.
export VAULT_VERSION="1.12.1"
export VAULT_ADDR=${VAULT_SERVER}
export VAULT_TOKEN=${VAULT_MASTER_TOKEN}
wget -O /tmp/vault_${VAULT_VERSION}_linux_amd64.zip https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip
unzip /tmp/vault_${VAULT_VERSION}_linux_amd64.zip -d /usr/bin
## RUN ON MASTER-0.
cat <<EOF > /etc/systemd/system/vault.service
[Unit]
Description=Vault secret management tool
After=consul.service
[Service]
PermissionsStartOnly=true
ExecStart=/usr/bin/vault server -log-level=debug -dev -dev-root-token-id="${VAULT_MASTER_TOKEN}" -dev-listen-address=0.0.0.0:9200
Restart=on-failure
LimitMEMLOCK=infinity
[Install]
WantedBy=multi-user.target
EOF
## RUN ON MASTER-0.
#enable Vault PKI secret engine
vault secrets enable -path=pki-root pki
#set default ttl
vault secrets tune -max-lease-ttl=87600h pki-root
#generate root CA
vault write -format=json pki-root/root/generate/internal \
common_name="ROOT PKI" ttl=8760h
*Прошу обратить внимание, что если вы находитесь на территории России, у вас будут проблемы с доступом для скачиванию Vault и Terrraform.
** pki-root/root/generate/internal - Указывает, что сформируется CA, и в response прилетит только публичный ключ, приватный будет закрыт.
*** pki-root - базовое наименование сейфа для Root-CA, смена производится через кастомизацию terraform модуля, о котором будем говорить ниже.
**** Данная инсталляция vault развернута как обзорная и не может использоваться для продуктивной нагрузки.
Отлично, Vault мы развернули, теперь нужно подготовить роли, политики и доступы в нем для key-keeper.
Для этого воспользуемся нашим модулем для Terraform.
Terraform
## RUN ON MASTER-0.
export TERRAFORM_VERSION="1.3.4"
wget -O /tmp/terraform_${TERRAFORM_VERSION}_linux_amd64.zip https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip
unzip /tmp/terraform_${TERRAFORM_VERSION}_linux_amd64.zip -d /usr/bin
## RUN ON MASTER-0.
mkdir terraform
cat <<EOF > terraform/main.tf
terraform {
required_version = ">= 0.13"
}
provider "vault" {
address = "http://127.0.0.1:9200/"
token = "${VAULT_MASTER_TOKEN}"
}
variable "master-instance-count" {
type = number
default = 1
}
variable "base_domain" {
type = string
default = "${BASE_DOMAIN}"
}
variable "cluster_name" {
type = string
default = "${CLUSTER_NAME}"
}
variable "vault_server" {
type = string
default = "http://master-0.${BASE_CLUSTER_DOMAIN}:9200/"
}
# Данный модуль генерит весь набор переменных,
# который потребуется в следующих статьях и модулях.
module "k8s-global-vars" {
source = "git::https://github.com/fraima/kubernetes.git//modules/k8s-config-vars"
cluster_name = var.cluster_name
base_domain = var.base_domain
master_instance_count = var.master-instance-count
vault_server = var.vault_server
}
# Тут происходит вся магия с Vault.
module "k8s-vault" {
source = "git::https://github.com/fraima/kubernetes.git//modules/k8s-vault"
k8s_global_vars = module.k8s-global-vars
}
EOF
cd terraform
terraform init --upgrade
terraform plan
terraform apply
В базовый набор Vault контента боевого кластера входит:
-
Сейфы под etcd, kubernetes, frotend-proxy. (* Сейфы для PKI создаются по маскам):
clusters/${CLUSTER_NAME}/pki/etcd
clusters/${CLUSTER_NAME}/pki/kubernetes-ca
clusters/${CLUSTER_NAME}/pki/front-proxy
-
Сейф Key Value для секретов
clusters/${CLUSTER_NAME}/kv/
-
Роли для заказа сертификатов (линки ведут на описание сертификата)
-
ETCD:
etcd-server(в данной инсталляции не используется)
-
Kubernetes-ca:
bootstrappers-client(в данной инсталляции не используется)kubeadm-client (в данной инсталляции используется как cluster-admin)
kube-apiserver-cluster-admin-client***(в данной инсталляции не используется)kubelet-peer-k8s-certmanager(В данной инсталляции не использется)
-
Front-proxy:
-
Политики доступа к ролям из П.2
-
Аппроли для доступа клиентов.
Путь до Approle формируется по маске -
clusters/${CLUSTER_NAME}/approle
Имя Approle формируется по маске -
${CERT_ROLE}-${MASTER_NAME}
Временные токены.
Ключи шифрования для подписи jwt токенов от сервисных аккаунтов.
** Сертификат kube-apiserver-kubelet-client во всех инсталляциях обычно имеет привилегии cluster-admin, в данной же ситуации, по дефолту он не имеет прав и потребует создания ClusterRolebinding для корректной работы с kubelet-ами нод, но об этом позже (смотрите в конце статьи в блоке Проверка).
*** kubeadm-client по дефолту имеет права cluster-admin. В этой инсталляции он будет использоваться как клиент доступа администратора для первичной настройки кластера.
5) Приступаем к формированию конфигурационных файлов для наших сервисов.
** Напоминаю, что их всего 3 (key-keeper, kubelet, containerd).
*** containerd (рассматривать не будем, т.к. он сам генерит базовый конфиг и в большинстве случаев его достаточно)
Начнем с Key-keeper
Со спецификой формирования конфига можно ознакомиться вот в этом README.
Конфиг очень длинный так, что не удивляйтесь... .
key-keeper.issuers
## RUN ON EACH MASTER.
# Для каждой ноды свое имя!!!!
export MASTER_NAME="master-0"
В первой части конфига указываем имя ноды, все остальные переменные указывали выше.
## RUN ON EACH MASTER.
mkdir -p ${BASE_K8S_PATH}/pki/
cat <<EOF > ${BASE_K8S_PATH}/pki/vault-config
---
issuers:
- name: kube-apiserver-sa
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kube-apiserver-sa-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kube-apiserver-sa/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kube-apiserver-sa/role-id
resource:
kv:
path: clusters/${CLUSTER_NAME}/kv
timeout: 15s
- name: etcd-ca
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: etcd-ca-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/etcd-ca/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/etcd-ca/role-id
resource:
CAPath: "clusters/${CLUSTER_NAME}/pki/etcd"
rootCAPath: "clusters/${CLUSTER_NAME}/pki/root"
timeout: 15s
- name: etcd-client
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: etcd-client-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/etcd-client/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/etcd-client/role-id
resource:
role: etcd-client
CAPath: "clusters/${CLUSTER_NAME}/pki/etcd"
timeout: 15s
- name: etcd-peer
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: etcd-peer-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/etcd-peer/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/etcd-peer/role-id
resource:
role: etcd-peer
CAPath: "clusters/${CLUSTER_NAME}/pki/etcd"
timeout: 15s
- name: front-proxy-ca
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: front-proxy-ca-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/front-proxy-ca/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/front-proxy-ca/role-id
resource:
CAPath: "clusters/${CLUSTER_NAME}/pki/front-proxy"
rootCAPath: "clusters/${CLUSTER_NAME}/pki/root"
timeout: 15s
- name: front-proxy-client
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: front-proxy-client-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/front-proxy-client/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/front-proxy-client/role-id
resource:
role: front-proxy-client
CAPath: "clusters/${CLUSTER_NAME}/pki/front-proxy"
timeout: 15s
- name: kubernetes-ca
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kubernetes-ca-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kubernetes-ca/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kubernetes-ca/role-id
resource:
CAPath: "clusters/${CLUSTER_NAME}/pki/kubernetes"
rootCAPath: "clusters/${CLUSTER_NAME}/pki/root"
timeout: 15s
- name: kube-apiserver
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kube-apiserver-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kube-apiserver/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kube-apiserver/role-id
resource:
role: kube-apiserver
CAPath: "clusters/${CLUSTER_NAME}/pki/kubernetes"
timeout: 15s
- name: kube-apiserver-cluster-admin-client
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kube-apiserver-cluster-admin-client-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kube-apiserver-cluster-admin-client/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kube-apiserver-cluster-admin-client/role-id
resource:
role: kube-apiserver-cluster-admin-client
CAPath: "clusters/${CLUSTER_NAME}/pki/kubernetes"
timeout: 15s
- name: kube-apiserver-kubelet-client
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kube-apiserver-kubelet-client-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kube-apiserver-kubelet-client/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kube-apiserver-kubelet-client/role-id
resource:
role: kube-apiserver-kubelet-client
CAPath: "clusters/${CLUSTER_NAME}/pki/kubernetes"
timeout: 15s
- name: kube-controller-manager-client
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kube-controller-manager-client-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kube-controller-manager-client/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kube-controller-manager-client/role-id
resource:
role: kube-controller-manager-client
CAPath: "clusters/${CLUSTER_NAME}/pki/kubernetes"
timeout: 15s
- name: kube-controller-manager-server
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kube-controller-manager-server-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kube-controller-manager-server/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kube-controller-manager-server/role-id
resource:
role: kube-controller-manager-server
CAPath: "clusters/${CLUSTER_NAME}/pki/kubernetes"
timeout: 15s
- name: kube-scheduler-client
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kube-scheduler-client-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kube-scheduler-client/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kube-scheduler-client/role-id
resource:
role: kube-scheduler-client
CAPath: "clusters/${CLUSTER_NAME}/pki/kubernetes"
timeout: 15s
- name: kube-scheduler-server
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kube-scheduler-server-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kube-scheduler-server/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kube-scheduler-server/role-id
resource:
role: kube-scheduler-server
CAPath: "clusters/${CLUSTER_NAME}/pki/kubernetes"
timeout: 15s
- name: kubeadm-client
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kubeadm-client-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kubeadm-client/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kubeadm-client/role-id
resource:
role: kubeadm-client
CAPath: "clusters/${CLUSTER_NAME}/pki/kubernetes"
timeout: 15s
- name: kubelet-client
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kubelet-client-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kubelet-client/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kubelet-client/role-id
resource:
role: kubelet-client
CAPath: "clusters/${CLUSTER_NAME}/pki/kubernetes"
timeout: 15s
- name: kubelet-server
vault:
server: ${VAULT_SERVER}
auth:
caBundle:
tlsInsecure: true
bootstrap:
file: /var/lib/key-keeper/bootstrap.token
appRole:
name: kubelet-server-${MASTER_NAME}
path: "clusters/${CLUSTER_NAME}/approle"
secretIDLocalPath: /var/lib/key-keeper/vault/kubelet-server/secret-id
roleIDLocalPath: /var/lib/key-keeper/vault/kubelet-server/role-id
resource:
role: kubelet-server
CAPath: "clusters/${CLUSTER_NAME}/pki/kubernetes"
timeout: 15s
EOF
key-keeper.certs
## RUN ON EACH MASTER.
cat <<EOF >> ${BASE_K8S_PATH}/pki/vault-config
certificates:
- name: etcd-ca
issuerRef:
name: etcd-ca
isCa: true
ca:
exportedKey: false
generate: false
hostPath: "${BASE_K8S_PATH}/pki/ca"
- name: kube-apiserver-etcd-client
issuerRef:
name: etcd-client
spec:
subject:
commonName: "system:kube-apiserver-etcd-client"
usage:
- client auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ttl: 10m
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/kube-apiserver"
withUpdate: true
- name: etcd-peer
issuerRef:
name: etcd-peer
spec:
subject:
commonName: "system:etcd-peer"
usage:
- server auth
- client auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ipAddresses:
interfaces:
- lo
- eth*
ttl: 10m
hostnames:
- localhost
- $HOSTNAME
- "${MASTER_NAME}.${BASE_CLUSTER_DOMAIN}"
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/etcd"
withUpdate: true
- name: etcd-server
issuerRef:
name: etcd-peer
spec:
subject:
commonName: "system:etcd-server"
usage:
- server auth
- client auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ipAddresses:
static:
- 127.0.1.1
interfaces:
- lo
- eth*
ttl: 10m
hostnames:
- localhost
- $HOSTNAME
- "${MASTER_NAME}.${BASE_CLUSTER_DOMAIN}"
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/etcd"
withUpdate: true
- name: front-proxy-ca
issuerRef:
name: front-proxy-ca
isCa: true
ca:
exportedKey: false
generate: false
hostPath: "${BASE_K8S_PATH}/pki/ca"
- name: front-proxy-client
issuerRef:
name: front-proxy-client
spec:
subject:
commonName: "custom:kube-apiserver-front-proxy-client"
usage:
- client auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ttl: 10m
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/kube-apiserver"
withUpdate: true
- name: kubernetes-ca
issuerRef:
name: kubernetes-ca
isCa: true
ca:
exportedKey: false
generate: false
hostPath: "${BASE_K8S_PATH}/pki/ca"
- name: kube-apiserver
issuerRef:
name: kube-apiserver
spec:
subject:
commonName: "custom:kube-apiserver"
usage:
- server auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ipAddresses:
static:
- 29.64.0.1
interfaces:
- lo
- eth*
dnsLookup:
- api.${BASE_CLUSTER_DOMAIN}
ttl: 10m
hostnames:
- localhost
- kubernetes
- kubernetes.default
- kubernetes.default.svc
- kubernetes.default.svc.cluster
- kubernetes.default.svc.cluster.local
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/kube-apiserver"
withUpdate: true
- name: kube-apiserver-kubelet-client
issuerRef:
name: kube-apiserver-kubelet-client
spec:
subject:
commonName: "custom:kube-apiserver-kubelet-client"
usage:
- client auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ttl: 10m
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/kube-apiserver"
withUpdate: true
- name: kube-controller-manager-client
issuerRef:
name: kube-controller-manager-client
spec:
subject:
commonName: "system:kube-controller-manager"
usage:
- client auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ttl: 10m
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/kube-controller-manager"
withUpdate: true
- name: kube-controller-manager-server
issuerRef:
name: kube-controller-manager-server
spec:
subject:
commonName: "custom:kube-controller-manager"
usage:
- server auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ipAddresses:
interfaces:
- lo
- eth*
ttl: 10m
hostnames:
- localhost
- kube-controller-manager.default
- kube-controller-manager.default.svc
- kube-controller-manager.default.svc.cluster
- kube-controller-manager.default.svc.cluster.local
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/kube-controller-manager"
withUpdate: true
- name: kube-scheduler-client
issuerRef:
name: kube-scheduler-client
spec:
subject:
commonName: "system:kube-scheduler"
usage:
- client auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ttl: 10m
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/kube-scheduler"
withUpdate: true
- name: kube-scheduler-server
issuerRef:
name: kube-scheduler-server
spec:
subject:
commonName: "custom:kube-scheduler"
usage:
- server auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ipAddresses:
interfaces:
- lo
- eth*
ttl: 10m
hostnames:
- localhost
- kube-scheduler.default
- kube-scheduler.default.svc
- kube-scheduler.default.svc.cluster
- kube-scheduler.default.svc.cluster.local
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/kube-scheduler"
withUpdate: true
- name: kubeadm-client
issuerRef:
name: kubeadm-client
spec:
subject:
commonName: "custom:kubeadm-client"
organizationalUnit:
- system:masters
usage:
- client auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ttl: 10m
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/kube-apiserver"
withUpdate: true
- name: kubelet-client
issuerRef:
name: kubelet-client
spec:
subject:
commonName: "system:node:${MASTER_NAME}-${CLUSTER_NAME}"
organization:
- system:nodes
usage:
- client auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ttl: 10m
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/kubelet"
withUpdate: true
- name: kubelet-server
issuerRef:
name: kubelet-server
spec:
subject:
commonName: "system:node:${MASTER_NAME}-${CLUSTER_NAME}"
usage:
- server auth
privateKey:
algorithm: "RSA"
encoding: "PKCS1"
size: 4096
ipAddresses:
interfaces:
- lo
- eth*
ttl: 10m
hostnames:
- localhost
- $HOSTNAME
- "${MASTER_NAME}.${BASE_CLUSTER_DOMAIN}"
renewBefore: 7m
hostPath: "${BASE_K8S_PATH}/pki/certs/kubelet"
withUpdate: true
secrets:
- name: kube-apiserver-sa
issuerRef:
name: kube-apiserver-sa
key: private
hostPath: ${BASE_K8S_PATH}/pki/certs/kube-apiserver/kube-apiserver-sa.pem
- name: kube-apiserver-sa
issuerRef:
name: kube-apiserver-sa
key: public
hostPath: ${BASE_K8S_PATH}/pki/certs/kube-apiserver/kube-apiserver-sa.pub
EOF
** Обратите внимание, что сертификаты выпускаются с ttl=10минут и renewBefore=7минут, это означает, что сертификат будет перевыпускаться каждые 3 минуты. Такие малые интервалы установлены, чтобы показать корректность работы функции перевыпуска сертификата. (Измените их на актуальные для вас значения.)
*** С версии 1.22 Kubernetes (ниже не проверял) все компоненты умеют автоматически определять, что конфигурационые файлы на файловой системе изменились и перечитывать их без перезапуска.
key-keeper.token
## RUN ON EACH MASTER.
mkdir -p /var/lib/key-keeper/
cat <<EOF > /var/lib/key-keeper/bootstrap.token
${VAULT_MASTER_TOKEN}
EOF
** Не удивляйтесь, что в этом конфигурационном файле мастер ключ от Vault Server, как я говорил ранее - это упрощённая версия настройки.
*** Если чуть глубже изучите наш модуль Vault для Terraform, то поймете, что там создаются временные токены, которые нужно указывать в bootstrap в конфиге key-keeper. Для каждого issuer свой токен. Пример -> https://github.com/fraima/kubernetes/blob/f0e4c7bc8f8d2695c419b17fec4bacc2dd7c5f18/modules/k8s-templates/cloud-init/templates/cloud-init-kubeadm-master.tftpl#L115
Большая части информации, описывающая почему именно так, а не иначе, приведена в статьях:
Сертификаты K8S или как распутать вермишель Часть 1
Сертификаты K8S или как распутать вермишель Часть 2
Важной особенностью является то, что мы больше не задумываемся о протухающих сертификатах, Key-keeper берет на себя эту задачу, от нас только требуется настроить мониторинг и алерты, для отслеживания корректной работы системы.
Kubelet config
config.yaml
## RUN ON EACH MASTER.
mkdir -p ${BASE_K8S_PATH}/kubelet
cat <<EOF >> ${BASE_K8S_PATH}/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
anonymous:
enabled: false
webhook:
cacheTTL: 0s
enabled: true
x509:
clientCAFile: "${BASE_K8S_PATH}/pki/ca/kubernetes-ca.pem"
tlsCertFile: ${BASE_K8S_PATH}/pki/certs/kubelet/kubelet-server.pem
tlsPrivateKeyFile: ${BASE_K8S_PATH}/pki/certs/kubelet/kubelet-server-key.pem
authorization:
mode: Webhook
webhook:
cacheAuthorizedTTL: 0s
cacheUnauthorizedTTL: 0s
cgroupDriver: systemd
clusterDNS:
- "${SERVICE_DNS}"
clusterDomain: cluster.local
cpuManagerReconcilePeriod: 0s
evictionPressureTransitionPeriod: 0s
fileCheckFrequency: 0s
healthzBindAddress: 127.0.0.1
healthzPort: 10248
httpCheckFrequency: 0s
imageMinimumGCAge: 0s
kind: KubeletConfiguration
logging:
flushFrequency: 0
options:
json:
infoBufferSize: "0"
verbosity: 0
memorySwap: {}
nodeStatusReportFrequency: 1s
nodeStatusUpdateFrequency: 1s
resolvConf: /run/systemd/resolve/resolv.conf
rotateCertificates: false
runtimeRequestTimeout: 0s
serverTLSBootstrap: true
shutdownGracePeriod: 15s
shutdownGracePeriodCriticalPods: 5s
staticPodPath: "${BASE_K8S_PATH}/manifests"
streamingConnectionIdleTimeout: 0s
syncFrequency: 0s
volumeStatsAggPeriod: 0s
containerLogMaxSize: 50Mi
maxPods: 250
kubeAPIQPS: 50
kubeAPIBurst: 100
podPidsLimit: 4096
serializeImagePulls: false
systemReserved:
ephemeral-storage: 1Gi
featureGates:
APIPriorityAndFairness: true
DownwardAPIHugePages: true
PodSecurity: true
CSIMigrationAWS: false
CSIMigrationAzureFile: false
CSIMigrationGCE: false
CSIMigrationvSphere: false
rotateCertificates: false
serverTLSBootstrap: true
tlsMinVersion: VersionTLS12
tlsCipherSuites:
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
allowedUnsafeSysctls:
- "net.core.somaxconn"
evictionSoft:
memory.available: 3Gi
nodefs.available: 25%
nodefs.inodesFree: 15%
imagefs.available: 30%
imagefs.inodesFree: 25%
evictionSoftGracePeriod:
memory.available: 2m30s
nodefs.available: 2m30s
nodefs.inodesFree: 2m30s
imagefs.available: 2m30s
imagefs.inodesFree: 2m30s
evictionHard:
memory.available: 2Gi
nodefs.available: 20%
nodefs.inodesFree: 10%
imagefs.available: 25%
imagefs.inodesFree: 15%
evictionPressureTransitionPeriod: 5s
imageMinimumGCAge: 12h
imageGCHighThresholdPercent: 55
imageGCLowThresholdPercent: 50
EOF
** clusterDNS - легко обжечься, если указал некорректное значение.
*** resolvConf - в Centos, Rhel, Almalinux может ругаться на путь, решается командами:
systemctl daemon-reload
systemctl enable systemd-resolved.service
systemctl start systemd-resolved.service
Документация описывающая проблему:
https://kubernetes.io/docs/tasks/administer-cluster/dns-debugging-resolution/#known-issues
System configs
К базовой конфигурации операционной системы относится:
Подготовка дискового пространства для /var/lib/etcd
(в данной инсталляции не рассматривается)Настройка sysctl
Настройка modprobe
Установка базовых пакетов (wget, tar)
modprobe
## RUN ON EACH MASTER.
cat <<EOF >> /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
sysctls
## RUN ON EACH MASTER.
cat <<EOF >> /etc/sysctl.d/99-network.conf
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ip6tables=1
net.ipv4.ip_forward=1
EOF
sysctl --system
Kubeconfigs
Для того, чтобы базовые компоненты кластера и администратор могли общаться с Kube-apiserver, нужно сформировать kubeconfig для каждого из них.
** admin.conf Kubeconfig с правами cluster-admin для базовой настройки кластера администратором.
admin.conf
## RUN ON EACH MASTER.
mkdir -p ${BASE_K8S_PATH}
cat <<EOF >> ${BASE_K8S_PATH}/admin.conf
---
apiVersion: v1
clusters:
- cluster:
certificate-authority: ${BASE_K8S_PATH}/pki/ca/kubernetes-ca.pem
server: https://127.0.0.1:${KUBE_APISERVER_PORT}
name: kubernetes
contexts:
- context:
cluster: kubernetes
namespace: default
user: kubeadm
name: kubeadm@kubernetes
current-context: kubeadm@kubernetes
kind: Config
preferences: {}
users:
- name: kubeadm
user:
client-certificate: ${BASE_K8S_PATH}/pki/certs/kube-apiserver/kubeadm-client.pem
client-key: ${BASE_K8S_PATH}/pki/certs/kube-apiserver/kubeadm-client-key.pem
EOF
kube-scheduler
## RUN ON EACH MASTER.
mkdir -p ${BASE_K8S_PATH}/kube-scheduler/
cat <<EOF >> ${BASE_K8S_PATH}/kube-scheduler/kubeconfig
---
apiVersion: v1
clusters:
- cluster:
certificate-authority: ${BASE_K8S_PATH}/pki/ca/kubernetes-ca.pem
server: https://127.0.0.1:${KUBE_APISERVER_PORT}
name: kubernetes
contexts:
- context:
cluster: kubernetes
namespace: default
user: kube-scheduler
name: kube-scheduler@kubernetes
current-context: kube-scheduler@kubernetes
kind: Config
preferences: {}
users:
- name: kube-scheduler
user:
client-certificate: ${BASE_K8S_PATH}/pki/certs/kube-scheduler/kube-scheduler-client.pem
client-key: ${BASE_K8S_PATH}/pki/certs/kube-scheduler/kube-scheduler-client-key.pem
EOF
kube-controller-manager
## RUN ON EACH MASTER.
mkdir -p ${BASE_K8S_PATH}/kube-controller-manager
cat <<EOF >> ${BASE_K8S_PATH}/kube-controller-manager/kubeconfig
---
apiVersion: v1
clusters:
- cluster:
certificate-authority: ${BASE_K8S_PATH}/pki/ca/kubernetes-ca.pem
server: https://127.0.0.1:${KUBE_APISERVER_PORT}
name: kubernetes
contexts:
- context:
cluster: kubernetes
namespace: default
user: kube-controller-manager
name: kube-controller-manager@kubernetes
current-context: kube-controller-manager@kubernetes
kind: Config
preferences: {}
users:
- name: kube-controller-manager
user:
client-certificate: ${BASE_K8S_PATH}/pki/certs/kube-controller-manager/kube-controller-manager-client.pem
client-key: ${BASE_K8S_PATH}/pki/certs/kube-controller-manager/kube-controller-manager-client-key.pem
EOF
kubelet
## RUN ON EACH MASTER.
mkdir -p ${BASE_K8S_PATH}/kubelet
cat <<EOF >> ${BASE_K8S_PATH}/kubelet/kubeconfig
---
apiVersion: v1
clusters:
- cluster:
certificate-authority: ${BASE_K8S_PATH}/pki/ca/kubernetes-ca.pem
server: https://127.0.0.1:${KUBE_APISERVER_PORT}
name: kubernetes
contexts:
- context:
cluster: kubernetes
namespace: default
user: kubelet
name: kubelet@kubernetes
current-context: kubelet@kubernetes
kind: Config
preferences: {}
users:
- name: kubelet
user:
client-certificate: ${BASE_K8S_PATH}/pki/certs/kubelet/kubelet-client.pem
client-key: ${BASE_K8S_PATH}/pki/certs/kubelet/kubelet-client-key.pem
EOF
Static Pods
kube-apiserver
## RUN ON EACH MASTER.
export ADVERTISE_ADDRESS=$(ip route get 1.1.1.1 | grep -oP 'src \K\S+')
cat <<EOF > /etc/kubernetes/manifests/kube-apiserver.yaml
apiVersion: v1
kind: Pod
metadata:
annotations:
kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: ${ADVERTISE_ADDRESS}:${KUBE_APISERVER_PORT}
creationTimestamp: null
labels:
component: kube-apiserver
tier: control-plane
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --advertise-address=${ADVERTISE_ADDRESS}
- --allow-privileged=true
- --authorization-mode=Node,RBAC
- --bind-address=0.0.0.0
- --client-ca-file=/etc/kubernetes/pki/ca/kubernetes-ca.pem
- --enable-admission-plugins=NodeRestriction
- --enable-bootstrap-token-auth=true
- --etcd-cafile=/etc/kubernetes/pki/ca/etcd-ca.pem
- --etcd-certfile=/etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver-etcd-client.pem
- --etcd-keyfile=/etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver-etcd-client-key.pem
- --etcd-servers=${ETCD_SERVERS}
- --kubelet-client-certificate=/etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver-kubelet-client.pem
- --kubelet-client-key=/etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver-kubelet-client-key.pem
- --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
- --proxy-client-cert-file=/etc/kubernetes/pki/certs/kube-apiserver/front-proxy-client.pem
- --proxy-client-key-file=/etc/kubernetes/pki/certs/kube-apiserver/front-proxy-client-key.pem
- --requestheader-allowed-names=front-proxy-client
- --requestheader-client-ca-file=/etc/kubernetes/pki/ca/front-proxy-ca.pem
- --requestheader-extra-headers-prefix=X-Remote-Extra-
- --requestheader-group-headers=X-Remote-Group
- --requestheader-username-headers=X-Remote-User
- --secure-port=${KUBE_APISERVER_PORT}
- --service-account-issuer=https://kubernetes.default.svc.cluster.local
- --service-account-key-file=/etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver-sa.pub
- --service-account-signing-key-file=/etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver-sa.pem
- --service-cluster-ip-range=${SERVICE_CIDR}
- --tls-cert-file=/etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver.pem
- --tls-private-key-file=/etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver-key.pem
image: k8s.gcr.io/kube-apiserver:${KUBERNETES_VERSION}
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 8
httpGet:
host: ${ADVERTISE_ADDRESS}
path: /livez
port: ${KUBE_APISERVER_PORT}
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
name: kube-apiserver
readinessProbe:
failureThreshold: 3
httpGet:
host: ${ADVERTISE_ADDRESS}
path: /readyz
port: ${KUBE_APISERVER_PORT}
scheme: HTTPS
periodSeconds: 1
timeoutSeconds: 15
resources:
requests:
cpu: 250m
startupProbe:
failureThreshold: 24
httpGet:
host: ${ADVERTISE_ADDRESS}
path: /livez
port: ${KUBE_APISERVER_PORT}
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
volumeMounts:
- mountPath: /etc/ssl/certs
name: ca-certs
readOnly: true
- mountPath: /etc/ca-certificates
name: etc-ca-certificates
readOnly: true
- mountPath: /var/log/kubernetes/audit/
name: k8s-audit
- mountPath: /etc/kubernetes/pki/ca
name: k8s-ca
readOnly: true
- mountPath: /etc/kubernetes/pki/certs
name: k8s-certs
readOnly: true
- mountPath: /etc/kubernetes/kube-apiserver
name: k8s-kube-apiserver-configs
readOnly: true
- mountPath: /usr/local/share/ca-certificates
name: usr-local-share-ca-certificates
readOnly: true
- mountPath: /usr/share/ca-certificates
name: usr-share-ca-certificates
readOnly: true
hostNetwork: true
priorityClassName: system-node-critical
securityContext:
seccompProfile:
type: RuntimeDefault
volumes:
- hostPath:
path: /etc/ssl/certs
type: DirectoryOrCreate
name: ca-certs
- hostPath:
path: /etc/ca-certificates
type: DirectoryOrCreate
name: etc-ca-certificates
- hostPath:
path: /var/log/kubernetes/audit/
type: DirectoryOrCreate
name: k8s-audit
- hostPath:
path: /etc/kubernetes/pki/ca
type: DirectoryOrCreate
name: k8s-ca
- hostPath:
path: /etc/kubernetes/pki/certs
type: DirectoryOrCreate
name: k8s-certs
- hostPath:
path: /etc/kubernetes/kube-apiserver
type: DirectoryOrCreate
name: k8s-kube-apiserver-configs
- hostPath:
path: /usr/local/share/ca-certificates
type: DirectoryOrCreate
name: usr-local-share-ca-certificates
- hostPath:
path: /usr/share/ca-certificates
type: DirectoryOrCreate
name: usr-share-ca-certificates
status: {}
EOF
** Обратите внимание, что переменная ADVERTISE_ADDRESS требует интернета, если его нет просто укажите IP ADDRESS ноды.
kube-controller-manager
## RUN ON EACH MASTER.
cat <<EOF > /etc/kubernetes/manifests/kube-controller-manager.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
component: kube-controller-manager
tier: control-plane
name: kube-controller-manager
namespace: kube-system
spec:
containers:
- command:
- kube-controller-manager
- --authentication-kubeconfig=/etc/kubernetes/kube-controller-manager/kubeconfig
- --authorization-always-allow-paths=/healthz,/metrics
- --authorization-kubeconfig=/etc/kubernetes/kube-controller-manager/kubeconfig
- --bind-address=${ADVERTISE_ADDRESS}
- --client-ca-file=/etc/kubernetes/pki/ca/kubernetes-ca.pem
- --cluster-cidr=${SERVICE_CIDR}
- --cluster-name=kubernetes
- --cluster-signing-cert-file=/etc/kubernetes/pki/ca/kubernetes-ca.pem
- --cluster-signing-key-file=
- --controllers=*,bootstrapsigner,tokencleaner
- --kubeconfig=/etc/kubernetes/kube-controller-manager/kubeconfig
- --leader-elect=true
- --requestheader-client-ca-file=/etc/kubernetes/pki/ca/front-proxy-ca.pem
- --root-ca-file=/etc/kubernetes/pki/ca/kubernetes-ca.pem
- --secure-port=${KUBE_CONTROLLER_MANAGER_PORT}
- --service-account-private-key-file=/etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver-sa.pem
- --tls-cert-file=/etc/kubernetes/pki/certs/kube-controller-manager/kube-controller-manager-server.pem
- --tls-private-key-file=/etc/kubernetes/pki/certs/kube-controller-manager/kube-controller-manager-server-key.pem
- --use-service-account-credentials=true
image: k8s.gcr.io/kube-controller-manager:${KUBERNETES_VERSION}
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 8
httpGet:
host: ${ADVERTISE_ADDRESS}
path: /healthz
port: ${KUBE_CONTROLLER_MANAGER_PORT}
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
name: kube-controller-manager
resources:
requests:
cpu: 200m
startupProbe:
failureThreshold: 24
httpGet:
host: ${ADVERTISE_ADDRESS}
path: /healthz
port: ${KUBE_CONTROLLER_MANAGER_PORT}
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
volumeMounts:
- mountPath: /etc/ssl/certs
name: ca-certs
readOnly: true
- mountPath: /etc/ca-certificates
name: etc-ca-certificates
readOnly: true
- mountPath: /usr/libexec/kubernetes/kubelet-plugins/volume/exec
name: flexvolume-dir
- mountPath: /etc/kubernetes/pki/ca
name: k8s-ca
readOnly: true
- mountPath: /etc/kubernetes/pki/certs
name: k8s-certs
readOnly: true
- mountPath: /etc/kubernetes/kube-controller-manager
name: k8s-kube-controller-manager-configs
readOnly: true
- mountPath: /usr/local/share/ca-certificates
name: usr-local-share-ca-certificates
readOnly: true
- mountPath: /usr/share/ca-certificates
name: usr-share-ca-certificates
readOnly: true
hostNetwork: true
priorityClassName: system-node-critical
securityContext:
seccompProfile:
type: RuntimeDefault
volumes:
- hostPath:
path: /etc/ssl/certs
type: DirectoryOrCreate
name: ca-certs
- hostPath:
path: /etc/ca-certificates
type: DirectoryOrCreate
name: etc-ca-certificates
- hostPath:
path: /usr/libexec/kubernetes/kubelet-plugins/volume/exec
type: DirectoryOrCreate
name: flexvolume-dir
- hostPath:
path: /etc/kubernetes/pki/ca
type: DirectoryOrCreate
name: k8s-ca
- hostPath:
path: /etc/kubernetes/pki/certs
type: DirectoryOrCreate
name: k8s-certs
- hostPath:
path: /etc/kubernetes/kube-controller-manager
type: DirectoryOrCreate
name: k8s-kube-controller-manager-configs
- hostPath:
path: /usr/local/share/ca-certificates
type: DirectoryOrCreate
name: usr-local-share-ca-certificates
- hostPath:
path: /usr/share/ca-certificates
type: DirectoryOrCreate
name: usr-share-ca-certificates
status: {}
EOF
kube-scheduler
## RUN ON EACH MASTER.
cat <<EOF > /etc/kubernetes/manifests/kube-scheduler.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
component: kube-scheduler
tier: control-plane
name: kube-scheduler
namespace: kube-system
spec:
containers:
- command:
- kube-scheduler
- --authentication-kubeconfig=/etc/kubernetes/kube-scheduler/kubeconfig
- --authorization-kubeconfig=/etc/kubernetes/kube-scheduler/kubeconfig
- --bind-address=${ADVERTISE_ADDRESS}
- --kubeconfig=/etc/kubernetes/kube-scheduler/kubeconfig
- --leader-elect=true
- --secure-port=${KUBE_SCHEDULER_PORT}
- --tls-cert-file=/etc/kubernetes/pki/certs/kube-scheduler/kube-scheduler-server.pem
- --tls-private-key-file=/etc/kubernetes/pki/certs/kube-scheduler/kube-scheduler-server-key.pem
image: k8s.gcr.io/kube-scheduler:${KUBERNETES_VERSION}
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 8
httpGet:
host: ${ADVERTISE_ADDRESS}
path: /healthz
port: ${KUBE_SCHEDULER_PORT}
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
name: kube-scheduler
resources:
requests:
cpu: 100m
startupProbe:
failureThreshold: 24
httpGet:
host: ${ADVERTISE_ADDRESS}
path: /healthz
port: ${KUBE_SCHEDULER_PORT}
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
volumeMounts:
- mountPath: /etc/kubernetes/pki/ca
name: k8s-ca
readOnly: true
- mountPath: /etc/kubernetes/pki/certs
name: k8s-certs
readOnly: true
- mountPath: /etc/kubernetes/kube-scheduler
name: k8s-kube-scheduler-configs
readOnly: true
hostNetwork: true
priorityClassName: system-node-critical
securityContext:
seccompProfile:
type: RuntimeDefault
volumes:
- hostPath:
path: /etc/kubernetes/pki/ca
type: DirectoryOrCreate
name: k8s-ca
- hostPath:
path: /etc/kubernetes/pki/certs
type: DirectoryOrCreate
name: k8s-certs
- hostPath:
path: /etc/kubernetes/kube-scheduler
type: DirectoryOrCreate
name: k8s-kube-scheduler-configs
status: {}
EOF
etcd
## RUN ON EACH MASTER.
cat <<EOF > /etc/kubernetes/manifests/etcd.yaml
---
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
component: etcd
tier: control-plane
name: etcd
namespace: kube-system
spec:
containers:
- name: etcd
command:
- etcd
args:
- --name=${MASTER_NAME}.${BASE_CLUSTER_DOMAIN}
- --initial-cluster=${ETCD_INITIAL_CLUSTER}
- --initial-advertise-peer-urls=https://${MASTER_NAME}.${BASE_CLUSTER_DOMAIN}:${ETCD_PEER_PORT}
- --advertise-client-urls=https://${MASTER_NAME}.${BASE_CLUSTER_DOMAIN}:${ETCD_SERVER_PORT}
- --peer-trusted-ca-file=/etc/kubernetes/pki/ca/etcd-ca.pem
- --trusted-ca-file=/etc/kubernetes/pki/ca/etcd-ca.pem
- --peer-cert-file=/etc/kubernetes/pki/certs/etcd/etcd-peer.pem
- --peer-key-file=/etc/kubernetes/pki/certs/etcd/etcd-peer-key.pem
- --cert-file=/etc/kubernetes/pki/certs/etcd/etcd-server.pem
- --key-file=/etc/kubernetes/pki/certs/etcd/etcd-server-key.pem
- --listen-client-urls=https://0.0.0.0:${ETCD_SERVER_PORT}
- --listen-peer-urls=https://0.0.0.0:${ETCD_PEER_PORT}
- --listen-metrics-urls=http://0.0.0.0:${ETCD_METRICS_PORT}
- --initial-cluster-token=etcd
- --initial-cluster-state=new
- --data-dir=/var/lib/etcd
- --strict-reconfig-check=true
- --peer-client-cert-auth=true
- --peer-auto-tls=true
- --client-cert-auth=true
- --snapshot-count=10000
- --heartbeat-interval=250
- --election-timeout=1500
- --quota-backend-bytes=0
- --max-snapshots=10
- --max-wals=10
- --discovery-fallback=proxy
- --auto-compaction-retention=8
- --force-new-cluster=false
- --enable-v2=false
- --proxy=off
- --proxy-failure-wait=5000
- --proxy-refresh-interval=30000
- --proxy-dial-timeout=1000
- --proxy-write-timeout=5000
- --proxy-read-timeout=0
- --metrics=extensive
- --logger=zap
image: k8s.gcr.io/etcd:${ETCD_VERSION}
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 8
httpGet:
host: 127.0.0.1
path: /health
port: ${ETCD_METRICS_PORT}
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
resources:
requests:
cpu: 100m
memory: 100Mi
startupProbe:
failureThreshold: 24
httpGet:
host: 127.0.0.1
path: /health
port: ${ETCD_METRICS_PORT}
scheme: HTTP
volumeMounts:
- mountPath: /var/lib/etcd
name: etcd-data
- mountPath: /etc/kubernetes/pki/certs/etcd
name: etcd-certs
- mountPath: /etc/kubernetes/pki/ca
name: ca
hostNetwork: true
priorityClassName: system-node-critical
securityContext:
null
volumes:
- hostPath:
path: /etc/kubernetes/pki/certs/etcd
type: DirectoryOrCreate
name: etcd-certs
- hostPath:
path: /etc/kubernetes/pki/ca
type: DirectoryOrCreate
name: ca
- hostPath:
path: /var/lib/etcd
type: DirectoryOrCreate
name: etcd-data
status: {}
EOF
Systemd
Теперь дело за малым, включаем все сервисы и добавляем их в автозапуск.
services
## RUN ON EACH MASTER.
systemctl daemon-reload
systemctl enable --now \
key-keeper.service \
kubelet.service \
containerd.service \
systemd-resolved.service
Проверка
Итак, конфигурация готова, мы применили все этапы на каждом мастере, теперь нужно проверить, что все работает корректно.
Первым проверяем, что сертификаты заказаны.
tree /etc/kubernetes/pki/ | grep -v key | grep pem | wc -l
Пулучаем 17 сертификатов
root@master-1-example:/home/dkot# tree
/etc/kubernetes/pki/
├── ca
│ ├── etcd-ca.pem
│ ├── front-proxy-ca.pem
│ └── kubernetes-ca.pem
├── certs
│ ├── etcd
│ │ ├── etcd-peer-key.pem
│ │ ├── etcd-peer.pem
│ │ ├── etcd-server-key.pem
│ │ └── etcd-server.pem
│ ├── kube-apiserver
│ │ ├── front-proxy-client-key.pem
│ │ ├── front-proxy-client.pem
│ │ ├── kubeadm-client-key.pem
│ │ ├── kubeadm-client.pem
│ │ ├── kube-apiserver-etcd-client-key.pem
│ │ ├── kube-apiserver-etcd-client.pem
│ │ ├── kube-apiserver-key.pem
│ │ ├── kube-apiserver-kubelet-client-key.pem
│ │ ├── kube-apiserver-kubelet-client.pem
│ │ ├── kube-apiserver.pem
│ │ ├── kube-apiserver-sa.pem
│ │ └── kube-apiserver-sa.pub
│ ├── kube-controller-manager
│ │ ├── kube-controller-manager-client-key.pem
│ │ ├── kube-controller-manager-client.pem
│ │ ├── kube-controller-manager-server-key.pem
│ │ └── kube-controller-manager-server.pem
│ ├── kubelet
│ │ ├── kubelet-client-key.pem
│ │ ├── kubelet-client.pem
│ │ ├── kubelet-server-key.pem
│ │ └── kubelet-server.pem
│ └── kube-scheduler
│ ├── kube-scheduler-client-key.pem
│ ├── kube-scheduler-client.pem
│ ├── kube-scheduler-server-key.pem
│ └── kube-scheduler-server.pem
Если сертификатов меньше или их вовсе нет, читаем логи сервиса key-keeper.journalctl -xefu key-keeper
Там найдете ответы на все вопросы.
Частые ошибки:
Невалидный конфигурационный файл.
Key-keeper не может авторизоваться.
Нет политик для использования роли у token или approle.
Заказываемый сертификат имеет аргументы неразрешенные в роли vault.
Проверяем, что все контейнеры запущены и корректно работают
crictl --runtime-endpoint unix:///run/containerd/containerd.sock ps -a
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID
08e2c895b4a20 23f16c2de4792 34 minutes ago Running kube-apiserver 4 b89014de1d7d8
5f1f770280cc7 23f16c2de4792 35 minutes ago Exited kube-apiserver 3 b89014de1d7d8
3313b1ec20e0a aebe758cef4cd 35 minutes ago Running etcd 2 cb5b2ca15cc28
e91d3bbb55b97 aebe758cef4cd 37 minutes ago Exited etcd 1 cb5b2ca15cc28
b3b004e6896db 4bf8b96f38e3b 39 minutes ago Running kube-controller-manager 0 9904b2d296bca
77d316d50693a ea40e3ed8cf2f 39 minutes ago Running kube-scheduler 0 24fac1b156ea4
Если какой-то контейнер находится в статусе EXITED - смотрим логи.
crictl --runtime-endpoint unix:///run/containerd/containerd.sock logs $CONTAINER_ID
Проверяем, собранный кластер ETCD
endpoint status
## RUN ON EACH MASTER.
export ETCDCTL_CERT=/etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver-etcd-client.pem \
export ETCDCTL_KEY=/etc/kubernetes/pki/certs/kube-apiserver/kube-apiserver-etcd-client-key.pem \
export ETCDCTL_CACERT=/etc/kubernetes/pki/ca/etcd-ca.pem \
etcd_endpoints () {
export ENDPOINTS=$(echo $(ENDPOINTS=127.0.0.1:${ETCD_SERVER_PORT}
etcdctl \
--endpoints=$ENDPOINTS \
member list |
awk '{print $5}' |
sed "s/,//") | sed "s/ /,/g")
}
etcd_endpoints
estat () {
etcdctl \
--write-out=table \
--endpoints=$ENDPOINTS \
endpoint status
}
estat
Полезно добавить данный кусок в bashrc, для удобной работы, проверки статуса или дебага etcd .
На выходе должны получить аналогичную картину: (Кол-во инстансов должно быть равно значению MASTER_COUNT
)
root@master-1-example:/home/dkot# estat
+--------------------------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+--------------------------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| https://master-1.example.dobry-kot.ru:2379 | 530f4c34efefa4a2 | 3.5.3 | 8.3 MB | true | false | 2 | 6433 | 6433 | |
| https://master-2.example.dobry-kot.ru:2379 | 85281728dcb33e5f | 3.5.3 | 8.3 MB | false | false | 2 | 6433 | 6433 | |
| https://master-0.example.dobry-kot.ru:2379 | ae74003c0ad34ecd | 3.5.3 | 8.3 MB | false | false | 2 | 6433 | 6433 | |
+--------------------------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
И проверяем, что Kubernetes API отвечает, а все ноды добавлены.
kubectl get nodes --kubeconfig=/etc/kubernetes/admin.conf
NAME STATUS ROLES AGE VERSION
master-0-example NotReady <none> 30m v1.23.12
master-1-example NotReady <none> 29m v1.23.12
master-2-example NotReady <none> 25m v1.23.12
Запускаем данную команду на одном из мастеров и видим, что все ноды добавлены и все в статусе NotReady, не пугаемся это связано с тем, что не установлен CNI Plugin.
** Надеюсь не забыли, что мы писали про сертификат kube-apiserver-kubelet-client.
В нашей инсталляции данный сертификат не будет иметь прав изначально, но kube-apiserver-(у) все еще нужны права для доступа к Kubelet на нодах, т.к. именно с этим сертификатом выполняются и проходят через RBAC операции "kubectl exec" и "kubectl logs".
Как ни удивительно, но о нас уже позаботились, и в свежем кластере уже есть подходящая роль, так что просто добавим актуальный ClusterRolebinding и проверим логи.
ClusterRoleBinding
cat <<EOF | kubectl apply -f -
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: custom:kube-apiserver-kubelet-client
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:kubelet-api-admin
subjects:
- kind: User
apiGroup: rbac.authorization.k8s.io
name: custom:kube-apiserver-kubelet-client
EOF
** Обратите внимание, что сертификат выпускается с CN=custom:kube-apiserver-kubelet-client (если требуется кастомизация имени, нужно править в модуле терраформ)
ИТОГО
В данной статье мы реализовали все поставленные цели:
Актуализировали этапы развертывания kubernetes кластера, расширив описание и добавив актуальные конфигурации.
Показали, что даже базовая конфигурация, без настроек под высокую доступность, интеграций с внешними системами - трудоемкий процесс и требует хорошего понимания продукта.
Все сертификаты выпускаются через key-keeper (client) в централизованном Vault хранилище и перевыпускаются, если истекает срок годности.
В следующей статье я хочу затронуть вопрос автоматизации развертывания Kubernetes кластера через Terraform и представить первую версию облачного kubernetes для Яндекс Облака, имеющую почти такой же функционал, что и Yandex Managed Service for Kubernetes.
Подписывайтесь, ставьте палец вверх, если понравилась статья.
Ждем вас на обсуждения нашей работы в https://t.me/fraima_ru
Полезное чтиво:
Сертификаты K8S или как распутать вермишель Часть 1
Сертификаты K8S или как распутать вермишель Часть 2
Комментарии (5)
RumataEstora
20.11.2022 10:24Что-то как-то очень сложно переменную
ETCD_SERVERS
создаете: первый sed безполезный, а еще и двойной echo (первый - в цикле в субшелле, потом внешний). Попробуйте вот это - у него приятный вкус:printf ",https://master-%s.$BASE_CLUSTER_DOMAIN:$ETCD_SERVER_PORT" \ $( seq 0 $(( MASTER_COUNT-1 )) ) \ | sed 's/,//'
Вот еще один деликатес - для
ETCD_INITIAL_CLUSTER
:for n in $( seq 0 $(( MASTER_COUNT-1 )) ) do n="$n.$BASE_CLUSTER_DOMAIN" printf ",master-%s=https://master-%s:$ETCD_PEER_PORT" $n $n done \ | sed 's/,//'
Думается мне - кровушки попили они с вас, пока не пришли к нужному виду.
everis
Длинно. Но интересно! :) Спасибо.
dobry-kot Автор
Большое спасибо, к сожалению цель стать показать внутренности без всякой автоматизации)