Всем привет. Меня зовут Добрый Кот Telegram.

От коллектива FR-Solutions и при поддержке @irbgeo Telegram : Продолжаем серию статей о K8S.

Цели данной статьи:

  1. Актуализировать порядок развертывания kubernetes, описанный всеми любимым Kelsey Hightower.

  2. Доказать что "kubernetes это всего 5-бинарей" и "kubernetes это просто" - это некорректное суждение.

  3. Добавить Key-keeper в конфигурацию kubernetes для управления сертификатами.

Из чего состоит Kubernetes?

Все мы помним шутку "kubernetes это всего 5-бинарей":

  1. etcd

  2. kube-apiserver

  3. kube-controller-manager

  4. kube-scheduler

  5. kubelet

Но, если мы будем оперировать только ими, то кластер вы не соберете. Почему же?

kubelet -у требуются дополнительные компоненты для работы:

  1. Container Runtime Interface - CRI (containerd, cri-o, docker, etc.).

CRI - для работы требуется:

  1. RUNC библиотека, для работы с контейнерами.

Certificates:

  1. (cfssl, kubeadm, key-keeper) требуются для выпуска сертификатов.

Прочее:

  1. kubectl (для работы с kubernetes) - опционально

  2. crictl (для удобной работы с CRI) - опционально

  3. etcdctl (для работы с etcd на мастерах) - опционально

  4. kubeadm (для настройки кластера) - опционально

Таким образом, чтобы развернуть kubernetes, требуется минимум 8 бинарей.

Этапы создания кластера K8S

  1. Создание linux машин, на которых будет развернут control-plane кластера.

  2. Настройка операционной системы на созданных linux машинах:

    1. установка базовых пакетов (для обслуживания linux).

    2. работа с modprobe.

    3. работа с sysctls.

    4. установка требуемых для функционирования кластера бинарей.

    5. подготовка конфигурационных файлов для установленных компонентов.

  3. Подготовка Vault хранилища.

  4. Генерация static-pod манифестов.

  5. Проверка доступности кластера.

Как видим, всего 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 контента боевого кластера входит:

  1. Сейфы под etcd, kubernetes, frotend-proxy. (* Сейфы для PKI создаются по маскам):

    1. clusters/${CLUSTER_NAME}/pki/etcd

    2. clusters/${CLUSTER_NAME}/pki/kubernetes-ca

    3. clusters/${CLUSTER_NAME}/pki/front-proxy

  2. Сейф Key Value для секретов

    1. clusters/${CLUSTER_NAME}/kv/

  3. Роли для заказа сертификатов (линки ведут на описание сертификата)

    1. ETCD:

      1. etcd-client

      2. etcd-server (в данной инсталляции не используется)

      3. etcd-peer

    2. Kubernetes-ca:

      1. bootstrappers-client (в данной инсталляции не используется)

      2. kube-controller-manager-client

      3. kube-controller-manager-server

      4. kube-apiserver-kubelet-client **

      5. kubeadm-client (в данной инсталляции используется как cluster-admin)

      6. kube-apiserver-cluster-admin-client *** (в данной инсталляции не используется)

      7. kube-apiserver

      8. kube-scheduler-server

      9. kube-scheduler-client

      10. kubelet-peer-k8s-certmanager (В данной инсталляции не использется)

      11. kubelet-server

      12. kubelet-client

    3. Front-proxy:

      1. front-proxy-client

  4. Политики доступа к ролям из П.2

  5. Аппроли для доступа клиентов.

    1. Путь до Approle формируется по маске - clusters/${CLUSTER_NAME}/approle

    2. Имя Approle формируется по маске - ${CERT_ROLE}-${MASTER_NAME}

  6. Временные токены.

  7. Ключи шифрования для подписи 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

К базовой конфигурации операционной системы относится:

  1. Подготовка дискового пространства для /var/lib/etcd (в данной инсталляции не рассматривается)

  2. Настройка sysctl

  3. Настройка modprobe

  4. Установка базовых пакетов (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 (если требуется кастомизация имени, нужно править в модуле терраформ)

ИТОГО

В данной статье мы реализовали все поставленные цели:

  1. Актуализировали этапы развертывания kubernetes кластера, расширив описание и добавив актуальные конфигурации.

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

  3. Все сертификаты выпускаются через key-keeper (client) в централизованном Vault хранилище и перевыпускаются, если истекает срок годности.

В следующей статье я хочу затронуть вопрос автоматизации развертывания Kubernetes кластера через Terraform и представить первую версию облачного kubernetes для Яндекс Облака, имеющую почти такой же функционал, что и Yandex Managed Service for Kubernetes.

Подписывайтесь, ставьте палец вверх, если понравилась статья.

Ждем вас на обсуждения нашей работы в https://t.me/fraima_ru

Полезное чтиво:

Сертификаты K8S или как распутать вермишель Часть 1

Сертификаты K8S или как распутать вермишель Часть 2

https://github.com/kelseyhightower/kubernetes-the-hard-way

Key-keeper Github

Комментарии (5)


  1. everis
    19.11.2022 02:24
    +1

    Длинно. Но интересно! :) Спасибо.


    1. dobry-kot Автор
      19.11.2022 02:25
      +1

      Большое спасибо, к сожалению цель стать показать внутренности без всякой автоматизации)


  1. mmaks17
    19.11.2022 19:34

    за статью спасибо)

    заметил 1 момент " Актуализировать " - а версия "v1.23.12" ) . с актуальными не будет нюансов ?


    1. dobry-kot Автор
      19.11.2022 19:58
      +1

      Конфигурация в основном взята от рендера Kubeadm 1.23.12 кастомных полей там нету, так что проблем не будет.


  1. 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/,//'
    

    Думается мне - кровушки попили они с вас, пока не пришли к нужному виду.