Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scenario multi-instance

Sept etapes sur le mode cluster de la demo, avec ce que chacune montre. L’etape 6 coupe le stockage S3, l’etape 7 passe par Keycloak : deployez avec eux.

demo/k3s/demo.sh cluster-up
demo/k3s/demo.sh deploy cluster --with-s3 --with-oidc   # ou --preset example
demo/k3s/demo.sh check cluster                # les sept etapes, verifiees

Avant les etapes

. demo/k3s/.work/cluster/env.sh
k get pods -o wide                     # kubectl, lance dans le conteneur k3s
set -- $(k get pods -l app=craft-file-gate -o jsonpath='{.items[*].metadata.name}')
PA=$1 PB=$2 PC=$3
A=$(pod_ip $PA) B=$(pod_ip $PB) C=$(pod_ip $PC)
TOKEN=$(login $A)

A, B et C sont les adresses des pods. env.sh donne, par Docker seul et sans secret dans une ligne de commande : k (kubectl), t (le conteneur d’outils), jq, login <ip> (un jeton de console d’admin), adm <ip> <chemin> (l’API d’administration avec $TOKEN), bearer <jeton> <ip> <chemin>, files <ip> <chemin> (l’API de fichiers sous demo), demo_sftp ; avec Keycloak, oidc_token <alice|bob|carol>, jwt_part <jeton> 0|1 (en-tete, claims), jwt_files <jeton> <ip> <chemin>, jwt_sftp <jeton>, kc_admin <chemin> (l’API d’administration de Keycloak).

1. SFTP et l’explorateur sur des pods differents

sftp -P 32122 demo@127.0.0.1           # $USER_PASSWORD ; gardez-la ouverte (ou demo_sftp)
sftp> ls /                             # archive  home  (home seul sans S3)
sftp> get /home/welcome.txt

Ouvrez l’explorateur sous demo et la console sous admin. Dans la console, Sessions montre la session SFTP et son Instance ; API clients montre demo et le pod de l’explorateur. Le port du noeud repartit les connexions entre les pods : si les deux tombent sur le meme, reconnectez.

adm $A '/admin/sessions?scope=cluster' | jq -r '.sessions[] | "\(.username) \(.type) \(.pod_name)"'
adm $A '/admin/api-clients?scope=cluster' | jq -r '.clients[] | "\(.username) \(.instance)"'

2. Un ban sur un pod, vu par les autres

Trois mauvais mots de passe sur le pod A ([api.ban] max_failures = 3), puis le bon sur B :

for i in 1 2 3; do files $A '?list' -u demo:wrong -o /dev/null -w '%{http_code}\n'; done
files $B '?list'
adm $C '/admin/bans?scope=cluster' | jq -c '.bans[]'
k get configmap gate-bans -o jsonpath='{.data.bans}'

B et C repondent 403 IP temporarily banned, en quelques centaines de millisecondes : le ban est dans la ConfigMap gate-bans, que chaque pod suit. L’adresse est celle du noeud (10.42.0.1), celle aussi de l’explorateur sur l’hote : il est refuse lui aussi. Bans liste le ban sur chaque instance ; Unban sur un pod le leve partout :

adm $C /admin/bans/api/10.42.0.1 -X DELETE

3. Une session de console acceptee par tous, une deconnexion revoquee partout

S=$(login $A)
for ip in $A $B $C; do bearer $S $ip /admin/me -o /dev/null -w "$ip %{http_code}\n"; done
bearer $S $B /admin/logout -X POST
for ip in $A $B $C; do bearer $S $ip /admin/me -o /dev/null -w "$ip %{http_code}\n"; done
TOKEN=$(login $A)

Le jeton de A ouvre B et C (200) : la cle qui le signe est dans le Secret partage gate-cluster. Apres la deconnexion sur B, les trois repondent 401 en une seconde : la revocation est dans gate-access. Logout dans la console fait de meme.

4. Un acces temporaire, visible sur tous les pods

Dans la console, Temporary access, New access : demo, role drop-zone, 1 h, un motif. Ou :

until=$(( $(date +%s) + 3600 ))
GRANT=$(adm $A /admin/grants -X POST -H 'content-type: application/json' \
  -d "{\"username\":\"demo\",\"role\":\"drop-zone\",\"until\":$until,\"reason\":\"demo delivery\"}" | jq -r .grant.id)
adm $B /admin/grants | jq -c '.grants[] | {username, role, state}'
files $C '?list' | jq -r '.entries[].name'

B et C listent l’acces (active) ; /drop apparait a demo sur chaque pod, dans l’explorateur apres un rechargement. Une revocation sur n’importe quel pod le retire partout :

adm $B /admin/grants/$GRANT -X DELETE

5. La vue du cluster pendant qu’un pod est supprime

peers() { adm $A /admin/instances | jq -r '.instances[] | "\(.status) \(.info.pod_name // .instance_id)"'; }
peers                                  # self, reachable, reachable
k delete pod $PC
peers                                  # le pod supprime : unreachable
k rollout status deploy/gate
peers                                  # son remplacant : reachable

L’onglet Instances montre de meme le pod supprime unreachable, puis plus rien quand le remplacant est pret : trois instances a nouveau. La console revient apres un rechargement en echec : son port-forward repart sur un autre pod. Prenez le nouveau pod pour l’etape 6 : PC=<son nom>; C=$(pod_ip $PC).

6. Stockage coupe : les detecteurs et le retrait du service

Avec S3 seulement. RustFS coupe d’un seul pod : une NetworkPolicy lui refuse l’adresse de RustFS, et la connexion ouverte du client S3 est retiree.

S3IP=$(k get pod -l app=s3 -o jsonpath='{.items[0].status.podIP}')
k label pod $PC demo-cut=s3
k apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: demo-cut-s3 }
spec:
  podSelector: { matchLabels: { demo-cut: s3 } }
  policyTypes: [Egress]
  egress: [{ to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [$S3IP/32] } }] }]
EOF
docker exec $K3S conntrack -D -s $C -p tcp --dport 9000
k logs -f $PC | grep -E 'backend unavailable|cluster detector|withdrawn from service|back in service'   # Ctrl-C
k get pods -w                          # Ctrl-C
ApresCe que vous voyez
~5 ssur $PC, backend unavailable (archive) puis cluster detector active, detector storage_alone : en panne ici, disponible sur un pair
10 s de pluswithdrawn from service : /readyz non pret sur $PC
~30 s de plus3 sondes de readiness : $PC 0/1, hors des Services gate et gate-api ; toujours un pair dans Instances (le Service gate-cluster liste les pods non prets)
k delete networkpolicy demo-cut-s3 && k label pod $PC demo-cut-

backend available again, back in service 5 s plus tard, 1/1 a la sonde suivante. Puis RustFS arrete pour tous :

k scale deploy/s3 --replicas=0
t curl -s http://$A:8081/metrics | grep 'craftfilegate_backend_up\|craftfilegate_unready_detector'
k get pods
k scale deploy/s3 --replicas=1

Chaque pod ecrit backend unavailable et publie craftfilegate_backend_up{backend="archive"} 0 ; aucun pod ne quitte le service : storage_alone demande un pair qui voit le stockage disponible, et isolated_and_storage_down un pod isole. /home reste servi, /archive repond 500 jusqu’au retour de RustFS. Ces detecteurs : Plusieurs instances.

7. Les jetons d’un fournisseur OIDC

Avec Keycloak seulement (Le fournisseur OIDC). Un jeton du realm demo, par le grant mot de passe, a chaque porte :

T=$(oidc_token alice)
jwt_part $T 1 | jq '{iss, aud, preferred_username, realm_access}'
jwt_sftp $T                            # sftp -P 32122 jwt@127.0.0.1, le jeton en mot de passe
jwt_files $T $A '?list' | jq -r '.entries[].name'
bearer $T $A /admin/me | jq '{username, auth_method, roles}'
bearer $(oidc_token bob) $A /admin/me -o /dev/null -w '%{http_code}\n'   # 403

alice ouvre SFTP, l’API de fichiers et la console (console-admins) ; bob, sans role admin, les fichiers seuls. Dans l’explorateur (Use a token instead) et la console (Sign in with a token), collez $T.

Une rotation : dans Keycloak (Realm settings, Keys, Add provider, ecdsa-generated, une priorite au-dessus des autres), ou :

TOP=$(kc_admin '/realms/demo/components?type=org.keycloak.keys.KeyProvider' | jq '[.[].config.priority[0] | tonumber] | max')
RID=$(kc_admin /realms/demo | jq -r .id)
kc_admin /realms/demo/components -X POST -H 'content-type: application/json' -d "{\"name\":\"rotated\",
  \"providerId\":\"ecdsa-generated\",\"providerType\":\"org.keycloak.keys.KeyProvider\",
  \"parentId\":\"$RID\",\"config\":{\"priority\":[\"$((TOP + 100))\"]}}"
T=$(oidc_token alice); jwt_part $T 0 | jq -r .kid    # la nouvelle cle
for ip in $A $B $C; do jwt_files $T $ip '?list' -o /dev/null -w "$ip %{http_code}\n"; done
kc_admin /realms/demo/components/$(kc_admin '/realms/demo/components?name=rotated' | jq -r '.[0].id') -X DELETE

Chaque pod repond 200 sans redemarrer : un kid qu’il ne connait pas lui fait recharger le jeu de cles (JWKS cache refreshed, trigger unknown_kid), une fois par minute au plus ; le DELETE rend la signature a l’ancienne cle, que les pods gardent. Puis carol desactivee (Users, carol, Enabled) : Keycloak ne lui donne plus de jeton (Account disabled), mais celui qu’elle tient reste valable jusqu’a son exp, 10 minutes au plus : un JWT ne se revoque pas.

Avec S3 : chaque operation

check.sh passe aussi par /archive, par l’API de fichiers et par SFTP : depot, liste, telechargement, renommage et suppression, un fichier de 9 Mio (au-dessus du seuil multipart de 8 Mio) dans les deux sens, chaque objet vu dans le bucket. La console de RustFS (http://127.0.0.1:32190/rustfs/console/, les cles affichees par deploy) les montre arriver sous archive/gate/.

Le mode single

demo/k3s/demo.sh deploy single (ports 32222, 32280, 32281) joue les memes etapes sur un pod : le ban est garde dans son fichier (/state/api-bans.json) ; le jeton de la console reste valable apres le remplacement du pod, sa cle etant dans le Secret partage ; S3 arrete, le pod reste en service, sans pairs il n’y a pas de detecteur. demo/k3s/demo.sh check single les verifie.