AWX in Kubernetes: Containerize OSS Ansible

Published: Jul 12, 2022 by Isaac Johnson

On multiple occasions I have had the need to setup up Ansible Tower, or it’s Open Source cousin AWX, as part of cloud migrations or to replace dated setups on-prem.

It is most often installed directly or as a docker compose based install on a linux host. However, we can install it using a Helm chart instead for a robust Kubernetes driven implementation.

Installing

There are quite a few charts we can use. A simple Google search found the following:

  • Datatree: https://www.datree.io/helm-chart/awx-lifen
  • Adwerx: https://artifacthub.io/packages/helm/adwerx/awx
  • ArtifactHub / Arthur-C: https://artifacthub.io/packages/helm/lifen/awx
  • Rfyio: https://github.com/arthur-c/ansible-awx-helm-chart/blob/master/README.md
  • Novum: https://novumrgi.github.io/helm/charts/awx/

I opted to use the Adwerx chart which will install AWX 17.1.0 with Ansible 2.9.18. We’ll look at two more later in this article.

We just need to add the repo and install with helm

$ helm repo add adwerx https://adwerx.github.io/charts
"adwerx" has been added to your repositories


$ helm install adwerxawx -n adwerx --create-namespace adwerx/awx --set defaultAdminUser=admin --set defaultAdminPassword=MyPassword --set
 postgresql.postgresqlPassword=password --set secretKey=myawxsecret
NAME: adwerxawx
LAST DEPLOYED: Sat Jul  9 21:48:48 2022
NAMESPACE: adwerx
STATUS: deployed
REVISION: 1

We can then port-forward to the service

$ kubectl port-forward svc/adwerxawx -n adwerx 8080:8080
Forwarding from 127.0.0.1:8080 -> 8052
Forwarding from [::1]:8080 -> 8052

/content/images/2022/07/awxink8s-01.png

Our first step is to create an Organization

/content/images/2022/07/awxink8s-03.png

Now that we created “HomeLab”

/content/images/2022/07/awxink8s-04.png

We can add an Inventory. Inventories are what hold Hosts

/content/images/2022/07/awxink8s-02.png

/content/images/2022/07/awxink8s-05.png

In order to connect, we’ll need to add a credential.

We have a lot of options. Perhaps the simplest is a simple SSH username/password.

Choose “Machine” for the type

/content/images/2022/07/awxink8s-07.png

Then add your username and password.

/content/images/2022/07/awxink8s-08.png

To test, we need a playbook. There are a few Test Ansible repos out there. I opted for https://github.com/ansible/test-playbooks

/content/images/2022/07/awxink8s-09.png

We can then add a Template from the Project in Templates

/content/images/2022/07/awxink8s-10.png

If you are able to connect to the GIT Repo you will see the list of Playbooks on the right

/content/images/2022/07/awxink8s-11.png

Once added, we can click Launch to test the Template

/content/images/2022/07/awxink8s-12.png

which turns into a Run

/content/images/2022/07/awxink8s-13.png

Lastly, if we can connect to the host and execute the playbook, we should see output in the details of the run.

/content/images/2022/07/awxink8s-14.png

Testing Resiliency

Let’s rotate the pods like might happen in a scaling or update event

$ kubectl get pods -n adwerx
NAME                         READY   STATUS    RESTARTS   AGE
adwerxawx-postgresql-0       1/1     Running   0          22h
adwerxawx-68889fdd67-c7dsc   3/3     Running   0          22h
builder@DESKTOP-72D2D9T:~$ kubectl delete pod adwerxawx-postgresql-0 -n adwerx & kubectl delete pod adwerxawx-68889fdd67-c7dsc -n adwerx &
[1] 12899
[2] 12900

We can see the pods terminating

$ kubectl get pods -n adwerx
NAME                         READY   STATUS              RESTARTS   AGE
adwerxawx-postgresql-0       1/1     Terminating         0          22h
adwerxawx-68889fdd67-c7dsc   3/3     Terminating         0          22h
adwerxawx-68889fdd67-lvg5d   0/3     ContainerCreating   0          3s

$ kubectl get pods -n adwerx
NAME                         READY   STATUS    RESTARTS   AGE
adwerxawx-postgresql-0       1/1     Running   0          41s
adwerxawx-68889fdd67-lvg5d   3/3     Running   0          72s

Then to check it all still works, I’ll port-forward back

$ kubectl port-forward svc/adwerxawx -n adwerx 8080:8080
Forwarding from 127.0.0.1:8080 -> 8052
Forwarding from [::1]:8080 -> 8052
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080

I can login

/content/images/2022/07/awxink8s-15.png

And we can see all our history and details are there

/content/images/2022/07/awxink8s-16.png

Ingress

First, I need to create Route53

$ cat r53-awx.json
{
  "Comment": "CREATE awx fb.s A record ",
  "Changes": [
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "awx.freshbrewed.science",
        "Type": "A",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": "73.242.50.46"
          }
        ]
      }
    }
  ]
}

$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-awx.json
{
    "ChangeInfo": {
        "Id": "/change/C0046530W9QFMQ2ULX7J",
        "Status": "PENDING",
        "SubmittedAt": "2022-07-11T02:15:06.921Z",
        "Comment": "CREATE awx fb.s A record "
    }
}

We can then add an Ingress

$ cat awx-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-buffer-size: 32k
    nginx.ingress.kubernetes.io/proxy-buffers-number: 8 32k
    nginx.ingress.kubernetes.io/proxy-read-timeout: "43200"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "43200"
  generation: 1
  labels:
    app: awx
  name: awx-ingress
  namespace: adwerx
spec:
  ingressClassName: nginx
  rules:
  - host: awx.freshbrewed.science
    http:
      paths:
      - backend:
          service:
            name: adwerxawx
            port:
              number: 8080
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - awx.freshbrewed.science
    secretName: tls-awx

$ kubectl apply -f awx-ingress.yaml -n adwerx
ingress.networking.k8s.io/awx-ingress created

As soon as the cert is satisfied, we can reach the AWX externally without port-forward

builder@DESKTOP-72D2D9T:~/Workspaces/jekyll-blog$ kubectl get cert -n adwerx
NAME      READY   SECRET    AGE
tls-awx   False   tls-awx   103s
builder@DESKTOP-72D2D9T:~/Workspaces/jekyll-blog$ kubectl get cert -n adwerx
NAME      READY   SECRET    AGE
tls-awx   True    tls-awx   117s

/content/images/2022/07/awxink8s-17.png

Other Charts: Lifen / Datree AWX

I thought it wise to try some of the other charts I found.

The first was from Datree.io which is a Kubernetes configuration checking tool provider.

$ helm repo add lifen-charts http://honestica.github.io/lifen-charts/
"lifen-charts" has been added to your repositories
builder@DESKTOP-72D2D9T:~/Workspaces/jekyll-blog$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "lifen-charts" chart repository
...Successfully got an update from the "cribl" chart repository
...Successfully got an update from the "metallb" chart repository
...Successfully got an update from the "actions-runner-controller" chart repository
...Successfully got an update from the "adwerx" chart repository
...Successfully got an update from the "hashicorp" chart repository
...Successfully got an update from the "harbor" chart repository
...Successfully got an update from the "datadog" chart repository
...Successfully got an update from the "jenkins" chart repository
...Successfully got an update from the "argo-cd" chart repository
...Successfully got an update from the "gitlab" chart repository
...Successfully got an update from the "bitnami" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈Happy Helming!⎈

$ helm install -n datreeawx --create-namespace myawx lifen-charts/awx --version 10.0.3 --set default_admin_password=MyPassword
W0710 21:53:06.324770    1867 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
W0710 21:53:07.116774    1867 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
NAME: myawx
LAST DEPLOYED: Sun Jul 10 21:53:04 2022
NAMESPACE: datreeawx
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:

I then checked the pods

$ kubectl get pods -n datreeawx
NAME                          READY   STATUS              RESTARTS   AGE
myawx-55dc6b97c-7xjqr         0/3     ContainerCreating   0          29s
myawx-memcached-0             0/1     ContainerCreating   0          29s
myawx-postgresql-0            0/1     ContainerCreating   0          28s
myawx-rabbitmq-0              0/1     Running             0          28s
myawx-web-88c9d89f4-kt7td     0/1     ImagePullBackOff    0          29s
myawx-task-78c64c6965-lrs2l   0/1     ImagePullBackOff    0          29s


$ kubectl get pods -n datreeawx
NAME                          READY   STATUS             RESTARTS   AGE
myawx-rabbitmq-0              1/1     Running            0          2m36s
myawx-web-88c9d89f4-kt7td     0/1     ImagePullBackOff   0          2m37s
myawx-memcached-0             1/1     Running            0          2m37s
myawx-task-78c64c6965-lrs2l   0/1     ImagePullBackOff   0          2m37s
myawx-memcached-1             1/1     Running            0          36s
myawx-memcached-2             1/1     Running            0          26s
myawx-postgresql-0            1/1     Running            0          2m36s
myawx-55dc6b97c-7xjqr         1/3     ErrImagePull       0          2m37s


$ kubectl get pods -n datreeawx
NAME                          READY   STATUS             RESTARTS   AGE
myawx-rabbitmq-0              1/1     Running            0          14m
myawx-memcached-0             1/1     Running            0          14m
myawx-memcached-1             1/1     Running            0          12m
myawx-memcached-2             1/1     Running            0          12m
myawx-postgresql-0            1/1     Running            0          14m
myawx-web-88c9d89f4-kt7td     0/1     ImagePullBackOff   0          14m
myawx-task-78c64c6965-lrs2l   0/1     ImagePullBackOff   0          14m
myawx-55dc6b97c-7xjqr         1/3     ImagePullBackOff   0          14m

Several of them used a lable long since removed out of github

I moved to the latest label I saw on their AWX images

$ helm upgrade -n datreeawx myawx lifen-charts/awx --set awx_task.image.tag=11.2.0 --set awx_web.image.tag=11.2.0 --set
 default_admin_password=MyPassword
W0710 22:14:19.599212    2786 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
W0710 22:14:19.817989    2786 warnings.go:70] policy/v1beta1 PodDisruptionBudget is deprecated in v1.21+, unavailable in v1.25+; use policy/v1 PodDisruptionBudget
Release "myawx" has been upgraded. Happy Helming!
NAME: myawx
LAST DEPLOYED: Sun Jul 10 22:14:17 2022
NAMESPACE: datreeawx
STATUS: deployed
REVISION: 2
TEST SUITE: None
NOTES:
1. Get the application URL by running these commands:

However, this did not solve the issue

$ kubectl get pods -n datreeawx
NAME                          READY   STATUS             RESTARTS          AGE
myawx-memcached-0             1/1     Running            0                 7h55m
myawx-memcached-1             1/1     Running            0                 7h53m
myawx-memcached-2             1/1     Running            0                 7h53m
myawx-postgresql-0            1/1     Running            0                 7h55m
myawx-task-589676bdf9-wpd6j   1/1     Running            0                 7h34m
myawx-web-88c9d89f4-kt7td     0/1     ImagePullBackOff   0                 7h55m
myawx-55dc6b97c-7xjqr         1/3     ImagePullBackOff   0                 7h55m
myawx-web-9f8dbc576-b5tcl     0/1     CrashLoopBackOff   153 (2m42s ago)   7h34m
myawx-rabbitmq-0              0/1     Running            80 (3m58s ago)    7h33m
myawx-58cb777df-m4r29         2/3     Running            128 (6m11s ago)   7h34m

Even rotating the pod doesn’t help.

$ kubectl get pods -n datreeawx
NAME                          READY   STATUS             RESTARTS          AGE
myawx-memcached-0             1/1     Running            0                 7h59m
myawx-memcached-1             1/1     Running            0                 7h57m
myawx-memcached-2             1/1     Running            0                 7h57m
myawx-postgresql-0            1/1     Running            0                 7h59m
myawx-task-589676bdf9-wpd6j   1/1     Running            0                 7h38m
myawx-58cb777df-m4r29         2/3     CrashLoopBackOff   129 (3m17s ago)   7h38m
myawx-web-88c9d89f4-kt7td     0/1     ImagePullBackOff   0                 7h59m
myawx-55dc6b97c-7xjqr         1/3     ImagePullBackOff   0                 7h59m
myawx-rabbitmq-0              0/1     Running            81 (3m13s ago)    7h38m
myawx-web-9f8dbc576-q8c75     0/1     CrashLoopBackOff   4 (29s ago)       2m59s

$ kubectl delete pod myawx-web-9f8dbc576-q8c75 -n datreeawx &
[1] 23059

$ kubectl get pods -n datreeawx | grep web
myawx-web-88c9d89f4-kt7td     0/1     ImagePullBackOff   0                8h
myawx-web-9f8dbc576-79pfk     0/1     CrashLoopBackOff   4 (17s ago)      2m47s

$ kubectl logs myawx-web-9f8dbc576-79pfk -n datreeawx
Using /etc/ansible/ansible.cfg as config file

$ kubectl get pods -n datreeawx | grep web
myawx-web-88c9d89f4-kt7td     0/1     ImagePullBackOff   0               8h
myawx-web-9f8dbc576-79pfk     0/1     Running            17 (17s ago)    38m

Lastly, just in case there was a newer chart, I checked that as well

$ helm search repo lifen-charts
NAME                                    CHART VERSION   APP VERSION     DESCRIPTION
lifen-charts/awx                        10.0.3          v10.0.0         A Helm chart for Kubernetes
lifen-charts/kube-iptables-tailer       0.2.3           v0.1.0          A Helm chart for Kubernetes
lifen-charts/looker                     0.1.2           2.16.0          A Looker Helm chart for Kubernetes.
lifen-charts/neuvector                  1.5.2           3.2.1           NeuVector Full Lifecycle Container Security Pla...
lifen-charts/op-scim-bridge             1.0.3           v2.3.1          The 1Password SCIM bridge
lifen-charts/squid                      0.4.3           v0.1.0          Squid proxy helm chart
lifen-charts/teleport                   0.1.1                           Teleport Enterprise

but 10.0.3 (which I used) is the latest.

I’ll have to call this path a bust.

I then cleaned up so I didn’t leave running broken charts

$ helm list -n datreeawx
NAME    NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
myawx   datreeawx       2               2022-07-10 22:14:17.366489 -0500 CDT    deployed        awx-10.0.3      v10.0.0
$ helm delete myawx -n datreeawx
release "myawx" uninstalled

Other Charts: Gitea

The next chart comes from a Consulting firm, Novum-RGI.

First, we add the Helm repo

$ helm repo add novum-rgi-helm https://novumrgi.github.io/helm/
"novum-rgi-helm" has been added to your repositories

Then install

$ helm install -n giteaawx --create-namespace gitea novum-rgi-helm/awx
NAME: gitea
LAST DEPLOYED: Sun Jul 10 22:06:49 2022
NAMESPACE: giteaawx
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace giteaawx -l "app.kubernetes.io/name=awx,app.kubernetes.io/instance=gitea" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8052 to use your application"
  kubectl --namespace giteaawx port-forward $POD_NAME 8052:8052

Note: you can use the defaults (adminPassword:awxPassword123). Or override (as you should) with --set awx.adminPassword=mypasword --set awx.adminUser=myadminuser --set awx.adminMail=myemailaddress

We can just deal with the defaults for now:

$ helm get values gitea -n giteaawx --all
COMPUTED VALUES:
awx:
  adminMail: admin@awx.com
  adminPassword: awxPassword123
  adminUser: admin
  ...

Let’s get the running pods

$ kubectl get pods -n giteaawx
NAME                         READY   STATUS    RESTARTS   AGE
gitea-postgresql-0           1/1     Running   0          9m16s
gitea-awx-66ff757c75-f62ws   3/3     Running   0          9m16s

And port-forward

$ kubectl port-forward svc/gitea-awx -n giteaawx 8052:8052
Forwarding from 127.0.0.1:8052 -> 8052
Forwarding from [::1]:8052 -> 8052
Handling connection for 8052
Handling connection for 8052
Handling connection for 8052
Handling connection for 8052
Handling connection for 8052
Handling connection for 8052
Handling connection for 8052

Here we see the older “winged” AWX login

/content/images/2022/07/awxink8s-19.png

And once logged in, we can see it’s the newer version (AWX 15.0.0 , Ansible 2.9.13).

/content/images/2022/07/awxink8s-18.png

One thing that caught my attention was that this instance had some demo content already there (I did not add the Demo Job Template or any projects)

/content/images/2022/07/awxink8s-20.png

Ingress

Now let’s assume I wanted to add Ingress to this instance.

As before, we could create an R53 Route

$ cat r53-awx2.json
{
  "Comment": "CREATE awx2 fb.s A record ",
  "Changes": [
    {
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "awx2.freshbrewed.science",
        "Type": "A",
        "TTL": 300,
        "ResourceRecords": [
          {
            "Value": "73.242.50.46"
          }
        ]
      }
    }
  ]
}

$ aws route53 change-resource-record-sets --hosted-zone-id Z39E8QFU0F9PZP --change-batch file://r53-awx2.j
son
{
    "ChangeInfo": {
        "Id": "/change/C08657872IO7KCP8AI9S7",
        "Status": "PENDING",
        "SubmittedAt": "2022-07-11T11:47:47.187Z",
        "Comment": "CREATE awx2 fb.s A record "
    }
}

Then add the Ingress definition

$ cat awx2-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-body-size: "0"
    nginx.ingress.kubernetes.io/proxy-buffer-size: 32k
    nginx.ingress.kubernetes.io/proxy-buffers-number: 8 32k
    nginx.ingress.kubernetes.io/proxy-read-timeout: "43200"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "43200"
  generation: 1
  labels:
    app: awx2
  name: awx2-ingress
  namespace: giteaawx
spec:
  ingressClassName: nginx
  rules:
  - host: awx2.freshbrewed.science
    http:
      paths:
      - backend:
          service:
            name: gitea-awx
            port:
              number: 8052
        path: /
        pathType: ImplementationSpecific
  tls:
  - hosts:
    - awx2.freshbrewed.science
    secretName: tls-awx2

$ kubectl apply -f awx2-ingress.yaml
ingress.networking.k8s.io/awx2-ingress created

Clearly, I’ll want to set the password now that it’s externally exposed.

$ helm upgrade -n giteaawx --create-namespace gitea novum-rgi-helm/awx --set awx.adminPassword=myrealpassword --set awx.adminUser=myrealuser --set awx.adminMail=isaac.johnson@gmail.com
Release "gitea" has been upgraded. Happy Helming!
NAME: gitea
LAST DEPLOYED: Mon Jul 11 06:53:33 2022
NAMESPACE: giteaawx
STATUS: deployed
REVISION: 2
NOTES:
1. Get the application URL by running these commands:
  export POD_NAME=$(kubectl get pods --namespace giteaawx -l "app.kubernetes.io/name=awx,app.kubernetes.io/instance=gitea" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8052 to use your application"
  kubectl --namespace giteaawx port-forward $POD_NAME 8052:8052

As soon as we see the Cert is valid, we can test

$ kubectl get cert -n giteaawx
NAME       READY   SECRET     AGE
tls-awx2   False   tls-awx2   43s

$ kubectl get cert -n giteaawx
NAME       READY   SECRET     AGE
tls-awx2   True    tls-awx2   3m13s

Interestingly enough, I did not find the upgrade actually changed the password.

However, I changed in the UI and it worked. To be certain it would persist, I also rotated the pods and tried again

$ kubectl delete pod gitea-postgresql-0 -n giteaawx & kubectl delete pod gitea-awx-66ff757c75-f62ws -n giteaawx &
[1] 25222
[2] 25223

$ kubectl get pods -n giteaawx
NAME                         READY   STATUS    RESTARTS   AGE
gitea-postgresql-0           1/1     Running   0          70s
gitea-awx-66ff757c75-4mjzt   2/3     Running   0          83s

Summary

Ansible as a provisioner solves the problem of how do we update non-containerized infrastructure in a reliable repeatable way.

Ansible is one of the most popular infrastructure updating platforms thus there are plenty of example playbooks and sample code. And as it is Open Source, plenty have expanded Ansibles functionality with plugins and tasks.

We can also use Ansible in conjunction with Packer to build and update VM Images and AMIs.

awx kubernetes ansible

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes