Ansible vs Terraform — private Ubuntu EC2 walkthrough Article: https://www.alekseialeinikov.com/en/blog/topics/devops/ansible-vs-terraform Scope: documentation-based teaching example, not a provisioned or benchmarked cloud deployment. Creates one billable EC2 instance and its root volume. The existing VPC, subnet, security group and SSH key are NOT created or destroyed. Use a separate disposable AWS account/workspace. No credentials are included. Prerequisites: - Terraform >=1.10,<2; AWS provider 6.x (terraform init selects an available compatible version; preserve .terraform.lock.hcl after reviewing it). - A supported ansible-core and compatible controller Python. Only builtin Ansible modules are used. Ubuntu target needs Python 3, apt and systemd. - AWS credentials obtained separately, ideally short-lived; permissions for EC2 creation/deletion/tagging, volume encryption and relevant API reads. Organization SCPs, KMS policy and instance-type availability may add constraints. - An approved Ubuntu amd64 AMI ID in your chosen region, with ubuntu SSH user and suitable sudo privileges. Verify the image publisher and SSH host identity. - Existing subnet and security group in the same VPC; controller must have a private routed path (VPN/bastion or runner inside the VPC). No public IP is used. - Permit TCP 22 only from the controller and TCP 80 only from the test client. Allow DNS and package repository egress through an existing controlled route, NAT or package mirror. No NAT gateway or package mirror is created here. - Existing EC2 key pair; its matching PRIVATE key stays in your SSH agent or protected controller credential store. Do not put it in Terraform or Git. - Populate known_hosts with a host key verified through a trusted channel. ssh-keyscan alone does not authenticate a server; do not disable host checking. Files: main.tf, site.yml, terraform.tfvars.example. Copy the example variables to terraform.tfvars and replace all placeholders. Local state is for this single-user lab only. Teams need an access-controlled, encrypted remote backend with supported locking and a tested restore process. Never commit state, plans, private keys or generated inventory. Commit the reviewed provider lock file. Suggested .gitignore lines: .terraform/ *.tfstate *.tfstate.* *.tfplan terraform.tfvars inventory.json Run from a local directory containing all three downloaded files: terraform init terraform fmt -check terraform validate terraform plan -out=demo.tfplan terraform apply demo.tfplan terraform output -json ansible_inventory > inventory.json ansible-inventory -i inventory.json --graph ansible web -i inventory.json -m ansible.builtin.wait_for_connection -a 'timeout=180' ansible-playbook -i inventory.json site.yml --syntax-check ansible-playbook -i inventory.json site.yml --ask-become-pass ansible-playbook -i inventory.json site.yml --check --diff --ask-become-pass ansible-playbook -i inventory.json site.yml --ask-become-pass If the image uses passwordless sudo, omit --ask-become-pass. Authentication and verified host keys must already be configured. Package manager readiness may lag SSH; wait for initial image setup/cloud-init and package locks to clear. Inspect inventory.json for the private address, then verify from the authorized client: curl --fail http://PRIVATE_IP/ Check mode is most informative after initial package installation, because the web root may not exist on a completely fresh machine. Expected behavior, NOT measured output: with unchanged inputs and a stable host, Terraform plans no infrastructure changes and the final Ansible run reports no managed changes (apart from cache refresh/other changed conditions). Cleanup, only after verifying this is the disposable lab state: terraform plan -destroy -out=destroy.tfplan terraform apply destroy.tfplan Remove the lab's generated plans/inventory after cleanup. Keep necessary state and backups protected. Ansible does not remove a VM when a host leaves inventory.