This project demonstrates automated provisioning and deployment of a secure web application stack on Azure, featuring Keycloak for authentication and OpenResty (Nginx) as a reverse proxy / web-server. Infrastructure is managed with OpenTofu (Terraform-compatible), configuration is handled by Ansible, and containers are orchestrated via Podman. The example app is a static website built with Hugo.
Infrastructure is automatically provisioned and configured on push via GitHub Actions workflow(s). A manually triggered infrastructure teardown (destroy) workflow is also defined.
Built and tested on Azure Cloud, Ubuntu Server 24.04 image.
Tools used:
-
OpenTofu
I'm experimenting with OpenTofu as there seems to be a community shift towards it at the moment, although the HCL here would probably also work just fine on Terraform.
-
Ansible
-
Podman
First time using Podman, I chose it because I liked it's rootless/daemonless capabilities and standardized approach (meaning possibly easier migration to other container orchestrators in the future, e.g k8s). It also integrates well with Ansible.
-
Hugo
Generating a decent looking site and simulating a bit more complex app build process. I just thought it would be more interesting than
echo "<a>Hello World!</a>" > index.html. -
Keycloak
-
OpenResty
Based on Nginx, which I have the most experience with. It's Lua-based extension system provides support for the OAuth authorization required by the project. Nginx/OpenResty can sometimes have issues in containerized environments (internal DNS resolution, load balancing between X instances), but for the purposes of this project it's absolutely fine.
Derived from # Quickstart: Use Terraform to create a Linux VM , extended with:
- ansible provider for generating inventory from state
- remote state storage on an Azure storage account/container
- dns zone and A record(s) for the provisioned host on Azure DNS
Ansible configuration is split into two playbooks
- for Keycloak (
keycloak.yml) - Sets up Keycloak, its Postgres database, reverse proxy configuration and new realm initialization (with client and user). - and App (
app.yml) - Builds Hugo-based static site (locally on the Ansible runner), synchronizes the result 'artifact' to the host and configures it's OpenResty web server for access (with Keycloak authentication).
with a main.yml tying them both together, along with some prep pre_steps. A separate task list is defined for OpenResty configuration, called from both keycloak.yml and app.yml allowing each one of them to define their own proxy configuration.
main.yml pre_steps setup the necessary iptables routes to route traffic from port 80/443 to their respective container-exposed counterparts. This is necessary because podman rootless can't bind to privileged ports.
Inventory used is generated by Terraform's ansible provider. The cloud.terraform collection inventory plugin reads host data from Terraform state and generates a workable inventory.
www.redhat.com/en/blog/providing-terraform-with-that-ansible-magic
- A server block is defined for
app.avalonpark.storewhere the app is served from. Access is restricted behind Keycloak authorization using theresty.openidcmodule. - Keycloak is reverse proxied behind Nginx/OpenResty at
keycloak.avalonpark.storefor easy single point SSL termination.
The server names (app/keycloak.avalonpark.store) are configurable. They are passed through from the DNS A records provisioned in dns.tf.
SSL certificates are provided by Let's Encrypt, using the lua-resty-auto-ssl module for auto provisioning and renewal.
Issues encountered:
https://stackoverflow.com/questions/48507224/cant-access-keycloak-rest-api-methods-404
https://stackoverflow.com/questions/70577004/keycloak-could-not-find-resource-for-full-path - see top answer
Two GH Actions workflows are defined:
deploy.yml- runs the provisioning steps with OpenTofu and configuration/deployment with Ansible (in two separate, dependent jobs)teardown.yml- runstofu destroywhich destroys all the provisioned infrastructure.
Secrets/variables for Azure authentication (for provisioning and state storage) are provided via GitHub Actions variables, passed down to OpenTofu as environment variables.
-
There's no DNS setup here, instead it's "faked" through /etc/hosts. In a real production setup there should be an actual DNS A record set-up to the host, ideally dynamically provisioned together with the rest of the infrastructure (e.g on Azure's DNS service) -
Enable SSL. No SSL certificates are deployed here, in part because of 1. OpenResty has been configured with this in mind though, acting both as a reverse proxy for Keycloak and serving the app, as such it's able to do SSL termination for both. Both existing certificates and Let's Encrypt can be used. -
Secret handling. For Keycloak secrets (client secret, admin password, user password), temporary values are used. In prod it would be preferable to store the real values in an encrypted vault (Ansible Vault, Azure KeyVault) and decrypt them at playbook runtime.
-
Consider using a more container-friendly reverse proxy like Traefik instead.
-
Investigate why containers are sometimes "randomly" left in a stopped state on subsequent updates/redeployments. Consider running containers differently.
Podman:
https://blog.jdboyd.net/2024/05/exposing-privileged-ports-with-podman/
https://www.geeksforgeeks.org/devops/set-up-a-postgresql-database-with-podman/
OpenResty/Keycloak:
https://kevalnagda.github.io/configure-nginx-and-keycloak-to-enable-sso-for-proxied-applications
https://www.keycloak.org/server/containers
https://medium.com/@cabreltchoffo12/secure-keycloak-in-5-minutes-with-nginx-lets-encrypt-e81a9ac15807
https://www.keycloak.org/server/reverseproxy
https://www.lshnk.me/2025/07/10/keycloak-and-reverse-proxy-a-guide-to-production-setup/
Hugo: