feat(ansible): add private registry configuration for k3s and Flux with secure key handling

This commit is contained in:
emelinda 2026-08-03 15:40:07 +03:00
parent 8a8cd66f59
commit 457f0dad41
20 changed files with 6862 additions and 41 deletions

21
.gitignore vendored
View File

@ -10,10 +10,23 @@ tmp
snapshots/*
# --- Рантайм-артефакты контура aero (создаются docker-compose на хосте) ---
gitea-data/ # данные gitea: репозитории, sqlite
k3s-storage/ # PVC-данные local-path-provisioner
k3s/kubeconfig.yaml # kubeconfig, который пишет k3s-server
nginx/certs/ # самоподписанные TLS-сертификаты
# ВНИМАНИЕ: комментарии в .gitignore работают только с начала строки,
# в конце строки они становятся частью паттерна и ломают его.
# данные gitea: репозитории, sqlite
gitea-data/
# PVC-данные local-path-provisioner
k3s-storage/
# kubeconfig, который пишет k3s-server
k3s/kubeconfig.yaml
# СОДЕРЖИТ КЛЮЧ реестра — рендерится ролью sarex_stack
k3s/registries.yaml
# СОДЕРЖИТ КЛЮЧ реестра — временный, удаляется после kubectl apply
k3s/registry-secret.yaml
# временная копия ключа реестра на хосте
registry-key.json
# самоподписанные TLS-сертификаты
nginx/certs/
# --- Секреты и ключи: не коммитить никогда ---
*.pem

View File

@ -36,11 +36,28 @@ secrets_store: "{{ playbook_dir }}/.secrets"
jwt_private_key_path: "{{ secrets_store }}/{{ inventory_hostname }}/jwt_private.pem"
jwt_public_key_path: "{{ secrets_store }}/{{ inventory_hostname }}/jwt_public.pem"
# Приватный реестр образов и ключ для входа (json_key)
# --- Приватный реестр образов и чартов (cr.yandex, аутентификация json_key) ---
registry_host: cr.yandex
registry_username: json_key
# Windows: C:\Users\user\.ssh\local\authorized_key.json → в WSL это /mnt/c/...
registry_key_src: /mnt/c/Users/user/.ssh/local/authorized_key.json
# Путь к ключу сервисного аккаунта НА CONTROL-NODE (ansible запускается из WSL,
# поэтому диск C: виден как /mnt/c). Это машинно-зависимое значение —
# переопределяйте, не редактируя роль:
# uv run poe platform -- -e registry_key_src=/path/to/authorized_key.json
registry_key_src: "/mnt/c/Users/user/Downloads/Telegram Desktop/authorized_key.json"
# Куда ключ кладётся на целевом хосте на время провижининга (удаляется после).
registry_key_dest: "{{ deploy_dir }}/registry-key.json"
# Секреты в кластере. Namespace создаётся вместе с секретом — он нужен раньше,
# чем чарт создаст его сам. Список пополняется по мере переезда компонентов.
# * yc-cr-auth (flux-system) — Flux тянет им OCI-ЧАРТЫ. Обязателен.
# * regcred — imagePullSecrets подов в namespace потребителя.
registry_secrets:
- {name: yc-cr-auth, namespace: flux-system}
- {name: regcred, namespace: cert-manager}
- {name: regcred, namespace: istio-system}
- {name: regcred, namespace: kubernetes-dashboard}
# Поднимать стек (docker compose up -d). Требует запущенной службы docker и
# docker login в cr.yandex — по умолчанию выключено.

View File

@ -144,40 +144,12 @@
-addext "subjectAltName=DNS:{{ platform_domain }},DNS:{{ admin_domain }},DNS:{{ minio_domain }}"
creates: "{{ cert_dir }}/selfsigned.crt"
# --- Логин в приватный реестр образов (json_key) --------------------------
- name: Проверить, выполнен ли вход в реестр
ansible.builtin.command: grep -q "{{ registry_host }}" /root/.docker/config.json
register: registry_login_state
changed_when: false
failed_when: false
tags: [login]
- name: Скопировать ключ реестра на хост (временно)
ansible.builtin.copy:
src: "{{ registry_key_src }}"
dest: /root/.registry-key.json
mode: "0600"
when: registry_login_state.rc != 0
no_log: true
tags: [login]
- name: docker login в приватный реестр
ansible.builtin.shell: >-
set -o pipefail && cat /root/.registry-key.json
| docker login --username {{ registry_username }} --password-stdin {{ registry_host }}
args:
executable: /bin/bash
when: registry_login_state.rc != 0
register: registry_login_result
changed_when: "'Login Succeeded' in registry_login_result.stdout"
no_log: true
tags: [login]
- name: Удалить временный ключ реестра с хоста
ansible.builtin.file:
path: /root/.registry-key.json
state: absent
tags: [login]
# --- Доступ к приватному реестру (docker на хосте + registries.yaml нод) ---
# Обязательно ДО подъёма k3s: registries.yaml монтируется в ноды, и k3s читает
# его только при старте.
- name: Настроить доступ к приватному реестру
ansible.builtin.include_tasks: registry.yml
tags: [login, registry]
# --- GitOps-подложка: k3s + gitea + vault + Flux --------------------------
# Отдельный тег и отдельный флаг: подложка поднимается публичными образами и

View File

@ -28,6 +28,11 @@
search_regex: "clusters:"
timeout: 60
# Секреты реестра — до bootstrap: без yc-cr-auth в flux-system source-controller
# не сможет скачать ни один OCI-чарт, и вся первая волна инфраструктуры встанет.
- name: Создать секреты доступа к реестру в кластере
ansible.builtin.include_tasks: registry-secrets.yml
- name: Волна 2 — gitea-init, vault(+init) и bootstrap Flux
ansible.builtin.command:
cmd: "{{ compose_cmd }} up -d {{ aero_platform_services_wave2 | join(' ') }}"

View File

@ -0,0 +1,41 @@
---
# Секреты доступа к приватному реестру внутри кластера.
# Требуют поднятого k3s — включается из platform.yml после первой волны.
#
# Манифест собирается из шаблона и ключа сервисного аккаунта, применяется
# kubectl'ом и сразу удаляется: в нём секрет в открытом виде.
# Ключ уже прочитан в registry_key_content задачей из registry.yml.
- name: Отрендерить манифест секретов реестра
ansible.builtin.template:
src: registry-secret.yaml.j2
dest: "{{ deploy_dir }}/k3s/registry-secret.yaml"
owner: root
group: root
mode: "0600"
no_log: true
# ./k3s смонтирован в k3s-server как /output — отсюда и путь.
- name: Применить секреты реестра в кластере
ansible.builtin.command:
argv:
- "{{ compose_cmd.split()[0] }}"
- compose
- exec
- "-T"
- k3s-server
- kubectl
- apply
- "-f"
- /output/registry-secret.yaml
chdir: "{{ deploy_dir }}"
register: registry_secret_apply
changed_when: >-
'created' in registry_secret_apply.stdout or
'configured' in registry_secret_apply.stdout
# Удаляем всегда, в том числе если apply упал: файл содержит ключ.
- name: Удалить манифест секретов с хоста
ansible.builtin.file:
path: "{{ deploy_dir }}/k3s/registry-secret.yaml"
state: absent

View File

@ -0,0 +1,112 @@
---
# Доступ к приватному реестру cr.yandex (аутентификация json_key).
#
# Три независимых потребителя, каждому нужен свой способ:
# 1. docker на хосте — образы сервисов compose (backend, frontend, ...)
# 2. containerd на нодах — образы подов; /etc/rancher/k3s/registries.yaml
# 3. source-controller Flux — OCI-чарты; Secret yc-cr-auth в flux-system
#
# Ключ читается с control-node и на целевой хост в открытом виде не кладётся
# (кроме короткого окна для docker login, см. ниже).
- name: Проверить наличие ключа реестра на control-node
ansible.builtin.stat:
path: "{{ registry_key_src }}"
delegate_to: localhost
become: false
register: registry_key_stat
tags: [login, registry]
- name: Прервать деплой, если ключа нет
ansible.builtin.fail:
msg: |
Не найден ключ сервисного аккаунта: {{ registry_key_src }}
Без него не поедет ни один компонент — все чарты и образы лежат
в приватном реестре {{ registry_host }}.
Укажите актуальный путь:
uv run poe platform -- -e registry_key_src=/path/to/authorized_key.json
when: not registry_key_stat.stat.exists
tags: [login, registry]
- name: Прочитать ключ реестра с control-node
ansible.builtin.set_fact:
registry_key_content: "{{ lookup('file', registry_key_src) }}"
no_log: true
tags: [login, registry]
# --- 1. docker на хосте ----------------------------------------------------
- name: Проверить, выполнен ли вход в реестр
ansible.builtin.command: grep -q "{{ registry_host }}" /root/.docker/config.json
register: registry_login_state
changed_when: false
failed_when: false
tags: [login]
- name: Скопировать ключ реестра на хост (временно)
ansible.builtin.copy:
content: "{{ registry_key_content }}"
dest: "{{ registry_key_dest }}"
mode: "0600"
when: registry_login_state.rc != 0
no_log: true
tags: [login]
- name: docker login в приватный реестр
ansible.builtin.shell: >-
set -o pipefail && cat {{ registry_key_dest | quote }}
| docker login --username {{ registry_username }} --password-stdin {{ registry_host }}
args:
executable: /bin/bash
when: registry_login_state.rc != 0
register: registry_login_result
changed_when: "'Login Succeeded' in registry_login_result.stdout"
no_log: true
tags: [login]
# helm на хосте может отсутствовать — он нужен только для ручной отладки.
# Flux чарты тянет сам, по секрету yc-cr-auth, а не через helm CLI.
- name: Проверить наличие helm на хосте
ansible.builtin.command: which helm
register: registry_helm_present
changed_when: false
failed_when: false
tags: [login]
- name: helm registry login (если helm установлен)
ansible.builtin.shell: >-
set -o pipefail && cat {{ registry_key_dest | quote }}
| helm registry login {{ registry_host }} --username {{ registry_username }} --password-stdin
args:
executable: /bin/bash
when:
- registry_helm_present.rc == 0
- registry_login_state.rc != 0
register: registry_helm_login
changed_when: "'Login Succeeded' in registry_helm_login.stdout"
no_log: true
tags: [login]
- name: Удалить временный ключ реестра с хоста
ansible.builtin.file:
path: "{{ registry_key_dest }}"
state: absent
tags: [login]
# --- 2. containerd на нодах k3s -------------------------------------------
# Монтируется во все ноды (см. docker-compose.yaml). Изменение файла требует
# перезапуска нод — k3s читает registries.yaml только на старте.
- name: Отрендерить registries.yaml для нод k3s
ansible.builtin.template:
src: registries.yaml.j2
dest: "{{ deploy_dir }}/k3s/registries.yaml"
owner: root
group: root
mode: "0600"
no_log: true
tags: [registry]
# Секреты доступа в самом кластере создаются отдельно (registry-secrets.yml):
# они требуют уже поднятого k3s, поэтому включаются из platform.yml после
# первой волны, а не здесь.

View File

@ -0,0 +1,21 @@
#jinja2: trim_blocks:True, lstrip_blocks:True
# Аутентификация containerd в приватном реестре — на уровне НОДЫ k3s.
# Файл монтируется в /etc/rancher/k3s/registries.yaml на каждую ноду.
#
# Почему так, а не imagePullSecrets/regcred в каждом namespace: контур
# однотенантный и реестр всего один, поэтому узловая настройка избавляет от
# необходимости заводить секрет в каждом новом namespace. Манифесты из репозитория
# при этом продолжают ссылаться на regcred — отсутствующий imagePullSecret
# kubelet допускает: он логирует событие и тянет образ кредами ноды.
#
# ВНИМАНИЕ: файл содержит секрет. Рендерится с mode 0600, в git не попадает.
mirrors:
"{{ registry_host }}":
endpoint:
- "https://{{ registry_host }}"
configs:
"{{ registry_host }}":
auth:
username: "{{ registry_username }}"
password: {{ registry_key_content | to_json }}

View File

@ -0,0 +1,43 @@
#jinja2: trim_blocks:True, lstrip_blocks:True
{#
Манифест секретов доступа к приватному реестру, собираемый из ключа
сервисного аккаунта (registry_key_src) на control-node.
Секретов два и они РАЗНОГО назначения:
* yc-cr-auth (flux-system) — им source-controller тянет OCI-ЧАРТЫ.
Обязателен: без него не поедет ни один HelmRelease.
* regcred (namespace потребителя) — imagePullSecrets подов. Продублирован
из манифестов репозитория; на уровне ноды образы и так доступны через
/etc/rancher/k3s/registries.yaml, но манифесты на него ссылаются.
Namespace создаётся здесь же: секрет нужен РАНЬШЕ, чем чарт создаст namespace.
ВНИМАНИЕ: файл содержит секрет в открытом виде. Рендерится с mode 0600
во временный каталог и удаляется сразу после kubectl apply.
#}
{% set docker_config = {
"auths": {
registry_host: {
"username": registry_username,
"password": registry_key_content,
"auth": (registry_username ~ ":" ~ registry_key_content) | b64encode
}
}
}
%}
{% for s in registry_secrets %}
---
apiVersion: v1
kind: Namespace
metadata:
name: {{ s.namespace }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ s.name }}
namespace: {{ s.namespace }}
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: {{ docker_config | to_json | b64encode }}
{% endfor %}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
# This manifest was generated by flux. DO NOT EDIT.
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 1m0s
ref:
branch: master
secretRef:
name: flux-system
url: http://gitea.flux-system.svc.cluster.local:3000/infra/iac.git
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: flux-system
namespace: flux-system
spec:
interval: 10m0s
path: ./clusters/aero
prune: true
sourceRef:
kind: GitRepository
name: flux-system

View File

@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gotk-components.yaml
- gotk-sync.yaml

View File

@ -0,0 +1,14 @@
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: yc-oci-charts
namespace: flux-system
spec:
type: oci
interval: 10m0s
url: oci://cr.yandex/crp3ccidau046kdj8g9q/charts
# Секрет создаёт роль sarex_stack из ключа сервисного аккаунта
# (см. aero/roles/sarex_stack/templates/registry-secret.yaml.j2).
secretRef:
name: yc-cr-auth

View File

@ -14,6 +14,9 @@ x-k3s-worker: &k3s-worker
environment:
K3S_URL: https://k3s-server:6443
K3S_TOKEN: ${K3S_TOKEN:-supersecrettoken}
# ВНИМАНИЕ: volumes здесь не задаём — merge-ключ `<<:` не сливает списки,
# и volumes сервиса перекрыл бы якорь целиком. Монтирования, включая
# registries.yaml, перечислены в каждом воркере явно.
tmpfs:
- /run
- /var/run
@ -48,6 +51,7 @@ services:
volumes:
- k3s-server-data:/var/lib/rancher/k3s # etcd/state, образы, манифесты — только named volume
- ./k3s:/output:z # отсюда забираем контекст k8s (:z — SELinux label для RedOS)
- ./k3s/registries.yaml:/etc/rancher/k3s/registries.yaml:ro,z # доступ containerd к cr.yandex
# PVC-данные local-path-provisioner (StorageClass local-path, он же default
# в k3s). Перекрываем ровно дефолтный путь провижинера, поэтому настраивать
# ConfigMap local-path-config не нужно. Bind-mount на хост означает, что
@ -82,6 +86,7 @@ services:
volumes:
- k3s-worker-1-data:/var/lib/rancher/k3s
- ./k3s-storage/worker-1:/var/lib/rancher/k3s/storage:z
- ./k3s/registries.yaml:/etc/rancher/k3s/registries.yaml:ro,z
k3s-worker-2:
<<: *k3s-worker
@ -90,6 +95,7 @@ services:
volumes:
- k3s-worker-2-data:/var/lib/rancher/k3s
- ./k3s-storage/worker-2:/var/lib/rancher/k3s/storage:z
- ./k3s/registries.yaml:/etc/rancher/k3s/registries.yaml:ro,z
k3s-worker-3:
<<: *k3s-worker
@ -98,6 +104,7 @@ services:
volumes:
- k3s-worker-3-data:/var/lib/rancher/k3s
- ./k3s-storage/worker-3:/var/lib/rancher/k3s/storage:z
- ./k3s/registries.yaml:/etc/rancher/k3s/registries.yaml:ro,z
# Одноразовый провижининг k3s: namespace processing + DNS-мост postgres/minio.
# Образ k3s содержит kubectl; server/tls перекрываем на k3s-server:6443

View File

@ -0,0 +1,53 @@
# Внутренний удостоверяющий центр контура aero.
#
# Зачем отдельный издатель: в репозитории все ClusterIssuer — ACME (Let's Encrypt)
# с решателем http01 и ingress class nginx. Контур закрытый, публичного DNS и
# входящего HTTP от ACME-сервера нет, поэтому ACME здесь принципиально нерабочий.
#
# Схема стандартная для внутренней PKI, в три шага:
# 1. selfsigned-issuer — самоподписывающий издатель, нужен только чтобы
# выпустить корневой сертификат;
# 2. aero-ca — сам корневой сертификат (isCA: true). Его приватный
# ключ попадает в secret aero-ca-key-pair в namespace cert-manager;
# 3. aero-ca-issuer — издатель, который подписывает этим корнем сертификаты
# для сервисов кластера (istio-gateway, dashboard и далее).
#
# Корневой сертификат нужно раздать клиентам (браузеры, curl), иначе они будут
# ругаться на недоверенный CA. Выгрузить его можно так:
# kubectl -n cert-manager get secret aero-ca-key-pair -o jsonpath='{.data.tls\.crt}' | base64 -d
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: aero-ca
# Namespace обязателен и должен совпадать с cert-manager --cluster-resource-namespace
# (по умолчанию cert-manager): именно там ClusterIssuer ищет свой секрет.
namespace: cert-manager
spec:
isCA: true
commonName: aero-ca
secretName: aero-ca-key-pair
duration: 87600h # 10 лет — корень контура, ротация вручную
renewBefore: 720h # 30 суток
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned-issuer
kind: ClusterIssuer
group: cert-manager.io
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: aero-ca-issuer
spec:
ca:
secretName: aero-ca-key-pair

View File

@ -0,0 +1,29 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
# Внутренний CA вместо ACME: контур закрытый, до Let's Encrypt не достучаться.
- clusterissuer-ca.yaml
# ACME-издатели из base выключены удалением, а не форком base: так overlay
# остаётся патчем и не расходится с остальными кластерами при обновлении base.
patches:
- target:
kind: ClusterIssuer
name: letsencrypt-issuer
patch: |
$patch: delete
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-issuer
- target:
kind: ClusterIssuer
name: letsencrypt-issuer-istio
patch: |
$patch: delete
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-issuer-istio

View File

@ -0,0 +1,5 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base

View File

@ -0,0 +1,5 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base

View File

@ -0,0 +1,10 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: istio-config.yaml
target:
kind: HelmRelease
name: istio-config

View File

@ -0,0 +1,5 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base

View File

@ -0,0 +1,5 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base