Cloud-1 is the follow-up to the Inception project: instead of building the containerized infrastructure by hand on a local VM, the goal here is to automate the deployment of an equivalent WordPress infrastructure on a remote cloud instance. Provisioning the instance, hardening it, installing Docker, and deploying the application are all automated: Terraform provisions the instance via make, and Ansible then configures it and deploys the stack, so the whole thing can be stood up from scratch on any fresh Ubuntu 22.04 server without manual steps on the target itself.
Terraform is a declarative infrastructure-as-code tool: instead of manually clicking through the AWS Console to create a server, you describe the desired end state (an EC2 instance, a security group, a key pair) in .tf files, and Terraform figures out what needs to be created, changed, or destroyed to get there. It tracks everything it manages in a state file, so re-running it is idempotent — if nothing changed, nothing happens. Here, Terraform owns everything up to "a reachable Ubuntu server exists"; Ansible takes over from there.
Ansible is an agentless automation tool that connects to remote hosts over SSH and applies a declarative, idempotent set of tasks described in YAML (playbooks and roles). It is used here to provision the server, install Docker, configure the firewall, issue TLS certificates, and deploy the Docker Compose stack — all in a repeatable way that produces the same result whether run once or a hundred times.
Docker Compose is a tool for defining and managing multi-container Docker applications. Using a docker-compose.yml file, you can configure your services, networks, and volumes in a declarative format. Here, Compose is not run manually — it is templated and triggered by the Ansible deploy role.
A single infrastructure object Terraform is responsible for creating and managing — e.g. aws_instance, aws_security_group, aws_key_pair. Each resource block declares what it should look like; Terraform handles the actual API calls to AWS to make that true.
A read-only lookup against something that already exists, rather than something Terraform creates. data "aws_ami" resolves the current official Ubuntu 22.04 image ID from Canonical at plan time, instead of a hardcoded ID that would go stale or only work in one AWS region.
Terraform's record of what it's already created and what those resources' real attributes are (IDs, IPs, etc.), stored in terraform.tfstate. This is how terraform apply knows the difference between "create a new instance" and "nothing changed, do nothing" on a second run — and why the state file itself is treated as sensitive and kept out of git alongside secrets/ and .env.
Inputs declared in variables.tf (region, instance type, SSH key path) and supplied via terraform.tfvars at apply time, rather than hardcoded — the same reasoning as .env for the application layer: one file holds the machine-specific values, kept out of version control, with a committed .example template showing the expected shape.
A self-contained unit of automation (tasks, handlers, templates, files, defaults) responsible for one concern of the deployment — e.g. installing Docker, configuring the firewall, issuing certificates, or deploying the application stack. Splitting the playbook into roles keeps the automation maintainable and reusable across servers.
The inventory/hosts.ini file lists the target host(s) the playbook can run against, allowing the same automation to deploy to one server or several in parallel.
A Docker image is a lightweight, standalone, executable package that contains everything required to run an application, including the code, runtime, libraries, environment variables, and configuration files.
A Docker volume is a mechanism for persisting data generated by and used by Docker containers. Volumes ensure WordPress files and database data survive a container restart or host reboot.
Sensitive values (database passwords, WordPress admin/user passwords) are never stored in plaintext environment variables or committed to the repository. Each secret is mounted read-only inside its container as a file at /run/secrets/<secret_name>, generated locally before the Ansible run and never baked into images or logged.