Aller au contenu
Infrastructure as Code medium

Installer AWX sur Kubernetes avec l'operator

18 min de lecture

Logo AWX

Ce guide déploie AWX sur un cluster Kubernetes que vous possédez déjà, avec un operator et kustomize, en trois manifestes. Vous obtiendrez une instance servie par un ingress, avec un mot de passe administrateur que vous maîtrisez et toutes les images épinglées, y compris les deux que l'operator laisse flotter en latest sans le dire.

Le déploiement est validé sur k3s 1.31, avec kubectl 1.36. Il ne demande ni Helm, ni git clone, ni script d'installation. Public visé : administrateurs Kubernetes à l'aise avec kubectl et les ressources personnalisées.

  • Distinguer l'operator, ses CRD et l'objet AWX que vous déclarez
  • Installer l'operator par kustomize, sans cloner de dépôt
  • Déclarer une instance avec un mot de passe administrateur maîtrisé
  • Exposer l'interface par un ingress, et comprendre le 404 qui déroute
  • Épingler les images, y compris celles que l'operator laisse en latest
  • Diagnostiquer les pièges rencontrés en lab
  • Un cluster Kubernetes existant, avec un kubectl administrateur. Le lab de référence est un k3s mono-nœud, en version 1.31.
  • Un ingress controller installé. k3s embarque Traefik par défaut.
  • Une classe de stockage par défaut, pour le volume de PostgreSQL. kubectl get sc doit afficher une ligne marquée (default).
  • kubectl 1.14 ou plus récent : kustomize y est intégré, l'option -k suffit.
  • Environ 4 Go de mémoire disponibles sur le nœud, et de quoi tirer 2 Go d'images.

Installeur ou operator : ce n'est pas le même choix qu'on croit

Section intitulée « Installeur ou operator : ce n'est pas le même choix qu'on croit »

Le hub documente deux chemins d'installation, et beaucoup les opposent à tort. L'installeur officiel n'est pas une alternative à l'operator : c'est un enrobage autour de lui. Son fichier de configuration porte une ligne ASCENDER_OPERATOR_VERSION: 2.19.6, et la plateforme qu'il produit fait tourner exactement le contrôleur que vous allez déployer ici. Dans les deux cas, c'est l'operator qui fabrique PostgreSQL, le pod web et le pod task.

La vraie différence tient à ce que chacun fait à votre place, et à ce qu'il vous prend en échange.

L'installeur part d'une machine virtuelle nue. Il installe ansible-core, monte k3s, déploie l'operator, écrit l'objet AWX, crée le secret TLS depuis votre certificat, inscrit les noms d'hôte dans /etc/hosts et installe Ledger. Un fichier de configuration, une commande, vingt minutes. En contrepartie, il décide pour vous : la distribution Kubernetes, la version des images, la topologie. Et il ne s'intègre à aucune chaîne GitOps.

L'operator par kustomize suppose un cluster déjà là et ne touche à rien d'autre. Vous écrivez trois manifestes, donc vous choisissez tout : la classe de stockage, l'ingress, le digest de chaque image, le namespace. Ces manifestes se versionnent et se rejouent. Personne, en revanche, ne vous monte le cluster, ne génère le certificat, ni ne déploie Ledger.

Installeur setup.shOperator et kustomize
Clusteril le monte (k3s)il faut le fournir
Operator déployéascender-operatorascender-operator
Versions des imagesimposées, EE en latestles vôtres, jusqu'au digest
TLSsecret généré depuis votre certificatà câbler soi-même
Ledgerinstallé par défautabsent
Reproductible en GitOpsnonoui
Ce qu'il vous demandeun fichier de configurationtrois manifestes

Le critère de décision tient en une question : avez-vous déjà un cluster Kubernetes ? Si oui, l'installeur ne peut rien pour vous, il en monterait un second à côté. Sinon, il vous économise une demi-journée, et Installer Ascender sur k3s est la page à lire.

Un operator est un contrôleur qui tourne dans le cluster et réconcilie en boucle un objet que vous déclarez avec la réalité observée. Vous n'écrivez pas les Deployment, les Service, le StatefulSet de PostgreSQL ni l'Ingress : vous décrivez une instance AWX, et l'operator fabrique puis maintient la quinzaine de ressources qui la composent.

Ce modèle est expliqué en profondeur dans Les opérateurs Kubernetes et, côté manipulation, dans Operators et CRDs pour développeurs.

Concrètement, l'installation se fait en deux temps. On installe d'abord l'operator et ses CRD, qui enseignent au cluster ce qu'est un objet AWX. On déclare ensuite une instance, et l'operator la construit.

kustomize sait assembler des manifestes distants et surcharger ce qu'ils contiennent. Il est intégré à kubectl depuis la version 1.14. Sa logique de bases et de surcharges est détaillée dans Kustomize : factorisez vos manifests Kubernetes.

Créez un répertoire de travail, et un unique fichier kustomization.yaml :

kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: ascender
resources:
- github.com/ctrliq/ascender-operator/config/default?ref=2.19.6
images:
- name: ghcr.io/ctrliq/ascender-operator
newTag: 2.19.6

Trois lignes portent tout le sens. La référence ?ref=2.19.6 épingle la version des manifestes : sans elle, kustomize suivrait la branche de développement et votre installation ne serait pas reproductible. La section images force le tag du contrôleur, qui vaut latest dans le dépôt. Le namespace place l'ensemble dans un espace dédié, créé au passage.

Vérifiez le rendu avant d'appliquer quoi que ce soit. C'est le réflexe qui distingue une installation d'une incantation :

Fenêtre de terminal
kubectl kustomize . | grep "image: ghcr.io"
image: ghcr.io/ctrliq/ascender-operator:2.19.6
Fenêtre de terminal
kubectl apply -k .

La commande crée le namespace, les CRD, les rôles RBAC du contrôleur et son déploiement. Attendez qu'il soit disponible :

Fenêtre de terminal
kubectl -n ascender rollout status deploy/awx-operator-controller-manager --timeout=240s
deployment "awx-operator-controller-manager" successfully rolled out

Le cluster connaît désormais quatre nouvelles ressources. Vous pouvez les interroger comme n'importe quelle ressource native, avec kubectl api-resources :

Fenêtre de terminal
kubectl api-resources --api-group=awx.ansible.com
NAME APIVERSION NAMESPACED KIND
awxbackups awx.ansible.com/v1beta1 true AWXBackup
awxmeshingresses awx.ansible.com/v1alpha1 true AWXMeshIngress
awxrestores awx.ansible.com/v1beta1 true AWXRestore
awxs awx.ansible.com/v1beta1 true AWX

Trois enseignements tiennent dans ce tableau. Le kind reste AWX et le groupe reste awx.ansible.com, y compris pour Ascender : le fork n'a renommé ni l'API ni les objets, ce qui garantit la compatibilité des manifestes entre les deux. Les CRD AWXBackup et AWXRestore apportent la sauvegarde et la restauration déclaratives. Enfin AWXMeshIngress est bien installée, alors que la documentation officielle n'en annonce que trois.

Étape 3 - Maîtriser le mot de passe administrateur

Section intitulée « Étape 3 - Maîtriser le mot de passe administrateur »

Si vous ne faites rien, l'operator génère un mot de passe et le range dans un secret nommé <nom-de-l-instance>-admin-password. C'est pratique pour une maquette, et intenable dès qu'une chaîne d'intégration doit rejouer le déploiement.

Créez le secret avant l'instance, et il sera utilisé tel quel :

admin-password.yaml
apiVersion: v1
kind: Secret
metadata:
name: ascender-admin-password
namespace: ascender
stringData:
password: RemplacezCeMotDePasse
Fenêtre de terminal
kubectl apply -f admin-password.yaml

Voici le manifeste complet, toutes images épinglées. Chaque champ est justifié juste après.

ascender.yaml
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
name: ascender
namespace: ascender
spec:
# Application : sans image_version, l'operator deploie latest
image: ghcr.io/ctrliq/ascender
image_version: "25.4.0"
# Environnement d'execution : aucun tag de version n'est publie,
# l'epinglage passe obligatoirement par le digest
control_plane_ee_image: ghcr.io/ctrliq/ascender-ee@sha256:bbe950a24e647ab9b3cead24b92bba8680ebec56a26a80a649420674a9e9ae22
# Base de donnees : les DEUX champs sont necessaires
postgres_image: quay.io/sclorg/postgresql-15-c9s
postgres_image_version: "20260708"
admin_user: admin
admin_password_secret: ascender-admin-password
service_type: ClusterIP
ingress_type: ingress
ingress_hosts:
- hostname: ascender.k3d.local
postgres_storage_requirements:
requests:
storage: 4Gi
projects_persistence: false
Fenêtre de terminal
kubectl apply -f ascender.yaml

L'operator lance alors PostgreSQL, exécute un job de migration du schéma, puis démarre les deux déploiements web et task. Comptez cinq à dix minutes, presque entièrement consacrées au téléchargement des images.

Fenêtre de terminal
kubectl -n ascender get pods
NAME READY STATUS RESTARTS AGE
ascender-migration-25.4.0-m69qq 0/1 Completed 0 6m
ascender-postgres-15-0 1/1 Running 0 7m26s
ascender-task-6c487975fd-n2rpm 4/4 Running 0 6m48s
ascender-web-b6f796d7f-f9sh5 3/3 Running 0 6m48s
awx-operator-controller-manager-8789847df-wkzw4 2/2 Running 0 9m24s

Le pod migration, à l'état Completed, est normal : c'est un Job, pas un service. Il ne se relance qu'aux montées de version.

ingress_type: ingress vaut none par défaut : sans lui, aucun ingress n'est créé et l'interface reste inaccessible depuis l'extérieur du cluster. ingress_hosts remplace le champ hostname, marqué obsolète, et accepte une liste d'hôtes avec leur secret TLS.

service_type: ClusterIP est correct dès lors qu'un ingress expose le service. Un LoadBalancer ferait double emploi et consommerait une adresse.

projects_persistence: false convient tant que vos playbooks viennent de dépôts Git, ce qui est le cas normal. Passez-le à true seulement si vous devez écrire dans /var/lib/projects : le volume réclame alors un accès ReadWriteMany, que peu de classes de stockage fournissent.

postgres_storage_requirements demande 8 Go par défaut. La valeur ne se réduit pas après coup sans recréer le volume.

C'est le point que la documentation passe sous silence, et il mérite une section entière. Épingler image_version ne fige que l'application. Deux autres images continuent de flotter.

Vérifiez ce que votre cluster fait réellement tourner, plutôt que ce que vous croyez avoir demandé :

Fenêtre de terminal
kubectl -n ascender get pods \
-o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}' \
| sort -u

Sans les trois champs d'image du manifeste précédent, la sortie contient :

ghcr.io/ctrliq/ascender:25.4.0
ghcr.io/ctrliq/ascender-ee:latest
quay.io/sclorg/postgresql-15-c9s:latest

Deux latest dans un déploiement censé être reproductible. Deux images qui peuvent changer sous vos pieds au prochain redémarrage d'un pod.

L'environnement d'exécution n'a aucun tag de version

Section intitulée « L'environnement d'exécution n'a aucun tag de version »

L'image ghcr.io/ctrliq/ascender-ee, celle qui exécute réellement vos playbooks, ne publie qu'un seul tag, latest. Le registre est formel :

Fenêtre de terminal
TOKEN=$(curl -s "https://ghcr.io/token?scope=repository:ctrliq/ascender-ee:pull&service=ghcr.io" | jq -r .token)
curl -s -H "Authorization: Bearer $TOKEN" https://ghcr.io/v2/ctrliq/ascender-ee/tags/list | jq .tags
["latest"]

Il n'existe donc aucune version à épingler. La seule prise possible est le digest, l'empreinte du contenu de l'image, que le registre renvoie volontiers :

Fenêtre de terminal
curl -sI -H "Authorization: Bearer $TOKEN" \
https://ghcr.io/v2/ctrliq/ascender-ee/manifests/latest | grep -i docker-content-digest
docker-content-digest: sha256:bbe950a24e647ab9b3cead24b92bba8680ebec56a26a80a649420674a9e9ae22

Ce digest se place dans control_plane_ee_image, avec un @ au lieu du :. L'operator l'accepte et le propage aux pods. Un digest désigne un contenu, jamais un alias : il ne peut pas changer de sens.

Le champ postgres_image_version est ignoré s'il est fourni sans postgres_image. Le comportement est silencieux : le StatefulSet n'est pas modifié, aucun avertissement n'apparaît, et vous continuez de tourner sur latest en croyant l'avoir figé.

Renseignez les deux. Le StatefulSet est alors mis à jour, et le pod redémarre sur l'image demandée :

Fenêtre de terminal
kubectl -n ascender get statefulset ascender-postgres-15 \
-o jsonpath='{.spec.template.spec.containers[0].image}'
quay.io/sclorg/postgresql-15-c9s:20260708

Avec les trois champs renseignés, plus aucun latest ne subsiste :

ghcr.io/ctrliq/ascender:25.4.0
ghcr.io/ctrliq/ascender-ee@sha256:bbe950a24e647ab9b3cead24b92bba8680ebec56a26a80a649420674a9e9ae22
ghcr.io/ctrliq/ascender-operator:2.19.6
quay.io/sclorg/postgresql-15-c9s:20260708

L'operator a créé un ingress. Regardez-le, il explique le comportement qui suit :

Fenêtre de terminal
kubectl -n ascender get ingress
NAME CLASS HOSTS ADDRESS PORTS
ascender-ingress traefik ascender.k3d.local 10.43.0.1 80

La preuve tient en deux commandes. La première, sans nom d'hôte, échoue ; la seconde réussit :

Fenêtre de terminal
curl -s -o /dev/null -w "%{http_code}\n" http://localhost/api/v2/ping/
curl -s -H "Host: ascender.k3d.local" http://localhost/api/v2/ping/
404
{"ha":true,"version":"25.4.0","active_node":"ascender-web-5cd46dfc59-xq2pz",...}

Pour un navigateur, il faut donc que le nom se résolve. En attendant une entrée DNS, déclarez-le sur votre poste.

Fenêtre de terminal
echo "127.0.0.1 ascender.k3d.local" | sudo tee -a /etc/hosts

Récupérez enfin le mot de passe. Même quand vous l'avez fourni, le secret reste la source de vérité :

Fenêtre de terminal
kubectl -n ascender get secret ascender-admin-password \
-o jsonpath="{.data.password}" | base64 -d ; echo

Ouvrez http://ascender.k3d.local/ et connectez-vous avec admin. Votre première action doit être de changer ce mot de passe et de créer un compte nominatif : la marche à suivre est dans Les premières actions d'administration.

Servez l'interface en HTTPS. Le manifeste ci-dessus expose l'ingress en clair, ce qui convient à un lab et jamais à autre chose : le mot de passe et les jetons d'API transitent dans la requête. Ajoutez un secret TLS à l'hôte, et laissez cert-manager le remplir :

ingress_hosts:
- hostname: ascender.example.com
tls_secret: ascender-tls

Ne versionnez jamais le secret du mot de passe en clair. Le manifeste de l'étape 3 est un exemple de lab.

Épinglez les images, ce qui relève autant de la reproductibilité que de la sécurité : une image latest reconstruite peut introduire une régression ou une dépendance vulnérable sans qu'aucun manifeste ne change.

Restreignez qui peut créer un objet AWX. Le pod task exécute des playbooks : ce droit équivaut à une exécution de code arbitraire depuis le cluster. L'operator lui-même réclame des droits étendus pour fabriquer ses ressources.

SymptômeCauseCorrectif
kubectl apply -k . installe l'operator d'Ansiblele kustomization.yaml de la doc du fork n'est pas rebadgéviser github.com/ctrliq/ascender-operator/config/default?ref=2.19.6
Le contrôleur tourne en latestpas de section images dans la kustomizationajouter newTag: 2.19.6
ascender-ee:latest malgré image_versionimage_version ne fige que l'applicationcontrol_plane_ee_image avec un digest
postgres_image_version sans effetle champ est ignoré s'il est seulrenseigner aussi postgres_image
L'adresse IP du cluster répond 404l'ingress route sur l'en-tête Hostrésoudre le nom déclaré dans ingress_hosts
Aucun ingress n'est crééingress_type vaut none par défautingress_type: ingress
Le pod migration reste Completedc'est un Job, pas un servicecomportement normal
Le pod postgres reste Pendingpas de classe de stockage par défautkubectl get sc, puis postgres_storage_class
Un kubectl patch sur l'ingress disparaîtl'operator réconcilie ses ressourcespasser par ingress_annotations sur l'objet AWX
  • L'operator d'Ansible est figé à la version 2.19.1 du 2 juillet 2024 ; le fork ascender-operator 2.19.6 est maintenu et publié régulièrement.
  • Les deux partagent le même groupe d'API awx.ansible.com et le même kind: AWX : les manifestes sont interchangeables.
  • L'installation se fait en deux temps : l'operator et ses CRD, puis l'objet AWX qui décrit l'instance.
  • La documentation du fork n'est pas rebadgée : son kustomization.yaml d'exemple installerait l'operator d'Ansible.
  • ?ref=<tag> et la section images sont ce qui rend l'installation reproductible.
  • Épingler image_version ne suffit pas. L'environnement d'exécution et PostgreSQL restent en latest.
  • L'image ascender-ee ne publie aucun tag de version : seul le digest permet de l'épingler.
  • postgres_image_version est ignoré sans postgres_image, silencieusement.
  • ingress_type vaut none par défaut : sans lui, rien n'est exposé.
  • Un ingress route sur l'en-tête Host : l'adresse IP nue renvoie un 404 trompeur.
  • Toute modification directe d'une ressource gérée est écrasée à la réconciliation : passer par l'objet AWX.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn