This page installs the AWX Operator on the Kubernetes cluster and brings up the first AWX instance.
It’s the “Lab” level of the series: an in-cluster PostgreSQL, the default on-cluster Execution Environment, and access via port-forward. The dedicated execution nodes and custom EE images come in the following pages. The one production-grade simplification we make here is the database — in a real deployment PostgreSQL lives on a dedicated external host (see the note at the end).
See the stack overview for the architecture and the reasoning behind every choice.
Prerequisites
Requirement
Notes
A running Kubernetes cluster
3 worker nodes (4 vCPU / 8 GB). I used Node CIDR 10.20.0.0/24, Pod CIDR 10.244.0.0/16
kubectl ≥ 1.27 on your workstation (I run it in WSL) + the cluster kubeconfig
The CLI that talks to the cluster
Your public IP in the cluster API allowlist
If the KaaS restricts API access by IP, your address must be authorized
A default StorageClass
Our in-cluster PostgreSQL needs a PVC
1. Connect to your KaaS / K8s using kubectl
Since I use a KaaS, I downloaded its kubeconfig and pointed kubectl at it:
mkdir -p ~/.kubemv ~/Downloads/kubeconfig-awx.yaml ~/.kube/kubeconfig-awx.yamlexport KUBECONFIG=~/.kube/kubeconfig-awx.yaml# Verify: the 3 worker nodes should be Readykubectl get nodes
Expected:
NAME STATUS ROLES AGE VERSION
awx-kaas-nodes-1 Ready <none> 5m v1.30.x
awx-kaas-nodes-2 Ready <none> 5m v1.30.x
awx-kaas-nodes-3 Ready <none> 5m v1.30.x
Confirm there’s a default StorageClass (our PostgreSQL PVC depends on it):
kubectl get storageclass# One of them must be marked (default)
IMPORTANT
If no StorageClass is marked (default), the PostgreSQL PVC stays Pending forever and AWX never starts. Mark one as default:
The operator is deployed with kustomize (bundled into kubectl as apply -k). Pin a specific release rather than tracking latest: pick the newest tag from the awx-operator releases.
Create a working directory:
mkdir -p ~/awx-deploy && cd ~/awx-deploy
~/awx-deploy/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationnamespace: awxresources: - github.com/ansible/awx-operator/config/default?ref=2.19.1images: # Pin the operator image to the release tag - name: quay.io/ansible/awx-operator newTag: 2.19.1 # Redirect the kube-rbac-proxy from the decommissioned gcr.io registry # to the upstream maintainer's repo on Quay (see callout below) - name: gcr.io/kubebuilder/kube-rbac-proxy newName: quay.io/brancz/kube-rbac-proxy newTag: v0.15.0
The kube-rbac-proxy image override is mandatory on awx-operator 2.19.x
The operator pod has two containers; one of them references gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0, but Google decommissioned the gcr.io/kubebuilder registry: without the override the operator pod gets stuck at 1/2 Running with ImagePullBackOff and not found.
The redirect to quay.io/brancz/kube-rbac-proxy (the maintainer’s repo) fixes it.
Validate the kustomization before applying (catches YAML typos before they touch the cluster):
The operator is now watching the awx namespace for AWX custom resources.
It does nothing yet: it’s the controller that will build the deployment when you declare an instance.
3. Deploy PostgreSQL in the cluster
For production: move the database out
The in-cluster PostgreSQL is my only one lab shortcut.
In production you’d run a dedicated external PostgreSQL (private-network VM or managed DB) so it survives cluster rebuilds with its own backups and HA.
Same wiring: keep postgres_configuration_secret, just point host: elsewhere.
Generate a password and apply the database manifest in one shot:
host: awx-db: the Service name, resolved by the cluster’s internal DNS. AWX reaches the DB there.
One secret for both: awx-postgres-configuration is read by PostgreSQL (to create the awx user/db on first boot) and by AWX (to connect). No chance of mismatched passwords.
PGDATA=/var/lib/postgresql/data/pgdata: a subdirectory of the mount. Required: block-storage volumes have a lost+found at the root, and initdb refuses a non-empty directory.
Verify the database is up:
kubectl get pods -n awx -l app=awx-db -w# wait for Running, then:kubectl logs -n awx deploy/awx-db | tail -5# expected: "database system is ready to accept connections"
4. Deploy the AWX instance
You declare what you want with an AWX custom resource: the operator reconciles the cluster to match.
We point it at the database secret from Step 3 (postgres_configuration_secret), and keep the service internal.
~/awx-deploy/awx-instance.yaml:
apiVersion: awx.ansible.com/v1beta1kind: AWXmetadata: name: awx namespace: awxspec: # ClusterIP = internal only. We reach the UI via port-forward for the Lab. # Production exposure (LoadBalancer / Ingress + TLS) comes later. service_type: ClusterIP # Use the in-cluster PostgreSQL from Step 3 instead of a managed pod postgres_configuration_secret: awx-postgres-configuration
Add it to the kustomization:
# append to resources: in ~/awx-deploy/kustomization.yamlresources: - github.com/ansible/awx-operator/config/default?ref=2.19.1 - awx-instance.yaml
Re-apply and watch the control plane converge (a few minutes, it pulls images, runs DB migrations, starts the services):
kubectl apply -k .kubectl get pods -n awx -w
Expected end state:
awx-db-xxxxxxxxxx-xxxxx 1/1 Running # our PostgreSQL
awx-operator-controller-manager-xxxxx 2/2 Running
awx-web-xxxxxxxxxx-xxxxx 3/3 Running # web
awx-task-xxxxxxxxxx-xxxxx 4/4 Running # task + ee + redis
awx-migration-24.6.1-xxxxx 0/1 Completed # DB schema migrations
TIP
The awx-migration-* job running is the signal that AWX connected to the database successfully: migrations only run once the connection works. If it appears, your external-DB wiring is correct.
5. Get the admin password and open the UI
The operator generates a random admin password into a secret:
(if the service isn’t named awx-service, find it with kubectl get svc -n awx)
Open http://localhost:8080 and log in as admin + the password above.
6. Verify end-to-end
A fresh AWX ships with a Demo Project, Demo Inventory (localhost), and Demo Job Template.
Running it confirms the whole execution path works, including the default on-cluster EE.
In the UI: Resources → Templates → Demo Job Template → Launch.
The job should run and finish Successful, with green output streaming live.
If that job goes green, your control plane is fully functional: the web layer launched it, the task layer scheduled it, the default EE executed it, and PostgreSQL recorded it.
That’s the Lab done.
Where to go next
Execution nodes: join dedicated workers over the Receptor mesh and cross into pattern A. (next in this series)
Production: external PostgreSQL: when your environment allows a dedicated database host, move the DB out of the cluster, it is better for production.