← All work
IaC · AnsibleSource available

Automated Cloud Deployment

The gap between a stack that works on your machine and a stack that works on a server is usually a human with an SSH session and a fading memory of what they typed. This closes that gap: the server is described in code, and the description is the only thing that touches it.

Built with Bilal Eddinaoui.

1
playbook, from bare VM to serving application
3
inbound ports open; everything else denied
0
manual steps on the target host

What it is

A single Ansible playbook, run against hosts declared in an inventory, that provisions a fresh cloud server into a working multi-service web stack. It installs and hardens the base system, sets up Docker, transfers the application stack, renders its configuration, brings the containers up, and verifies they are actually running.

The property that matters is idempotence: running it twice is the same as running it once. That is what makes it safe to re-run against a live host to correct drift, rather than something you only dare execute on a machine you are willing to lose.

Provisioning in the order that matters

The firewall goes on before the services do. The role installs UFW, sets the default incoming policy to deny, and then opens exactly the three ports the stack needs — SSH and the two web ports. Doing this first means there is never a window where a freshly installed service is exposed while the rules are still being written.

Docker is installed from the vendor's own repository rather than the distribution's, which means first purging the older conflicting packages that ship under similar names, importing the official signing key, and then installing the engine together with the compose plugin. Distribution-packaged Docker is frequently far enough behind that compose syntax the stack relies on simply isn't supported.

Only then is the application transferred and started — each container brought up and then explicitly asserted to be running, rather than assuming that a start command exiting zero means the service came up. A container that starts and immediately dies exits zero on the way in.

  • Default-deny inbound, with three ports opened deliberately rather than inherited from the image.
  • Container state is asserted after each step, so the playbook fails where the fault is rather than three roles later.
  • Pinned versions for the datastore and application images, so a rebuild months later produces the same stack.

Secrets that stay out of the repository

The database root password is the detail that decides whether this work can be published at all. It lives in an encrypted vault, and is decrypted only in memory during the run and injected when the environment file is rendered from its template — written with restrictive permissions so it is not readable by other accounts on the host.

That is what lets the repository be public while the credential is not in it, and it is why the environment file exists as a template rather than as a committed file with a placeholder somebody eventually fills in and commits by accident.

Resilience and rebuild

Persistence lives in named volumes rather than inside containers, so a container can be replaced — a version bump, a crash, a deliberate teardown — without taking the data with it. Backups run on a schedule against those volumes.

The test of the whole thing is the rebuild: point the playbook at a brand new server, run it once, restore the volumes, and the stack is back. That is the difference between infrastructure you have documented and infrastructure you can actually reproduce.

Built with

  • Ansible
  • Ansible Vault
  • Docker
  • Docker Compose
  • Nginx
  • Cloud VPS

Not hosted: this provisions a server rather than running on one. The playbook is the artifact.