Provision cloud infrastructure with Terraform. Configure it with Ansible. Two tools, one clean automated workflow — from zero to a live web server without touching a server by hand.
Terraform and Ansible address two distinct layers of the infrastructure problem. Terraform talks to AWS APIs and creates resources. Ansible connects to those resources over SSH and configures what runs on them. The split is intentional — provisioning and configuration management are different problems that benefit from different tools.
// PROVISIONING LAYER — Terraform terraform apply │ ├── aws_key_pair → uploads SSH public key to AWS ├── aws_security_group → port 22 (SSH) + port 80 (HTTP) inbound ├── aws_iam_role → EC2 service role ├── aws_iam_role_policy_attachment → AmazonS3ReadOnlyAccess ├── aws_iam_instance_profile → wraps role for EC2 attachment ├── aws_s3_bucket → object storage (force_destroy enabled) └── aws_instance → EC2 t2.micro, Amazon Linux 2023 │ └── outputs: ec2_public_ip, web_url, s3_bucket_name // CONFIGURATION LAYER — Ansible ansible-playbook -i inventory.ini playbook.yml │ ├── Install Nginx → dnf package manager (AL2023) ├── Deploy index.html → copy module → /usr/share/nginx/html/ └── Start + enable Nginx → service module, survives reboots │ └── http://<EC2_PUBLIC_IP> ← live web page
Split across variables.tf, main.tf, and outputs.tf following standard Terraform practice. Variables centralize configurable inputs — region, AMI ID, instance type, key name, bucket name — so the full infrastructure can be redeployed to a different region by editing a single file. Outputs expose the EC2 public IP and web URL after apply, which feeds directly into Ansible's inventory.
Provisioned a t2.micro EC2 instance on Amazon Linux 2023 with a dedicated IAM role granting S3 read access via instance profile — no static credentials embedded anywhere. The security group allows inbound SSH on port 22 for Ansible connectivity and HTTP on port 80 for the web server, with unrestricted outbound. SSH key pair is provisioned by Terraform from the local public key, so no manual key upload required.
S3 bucket provisioned alongside EC2 in a single Terraform config, demonstrating multi-resource IaC management. The EC2 instance's IAM role has the AWS-managed AmazonS3ReadOnlyAccess policy attached via aws_iam_role_policy_attachment, giving the instance read access to the bucket without any embedded credentials. force_destroy = true is set so terraform destroy cleans up the bucket even if it contains objects.
Three-task playbook running against the EC2 instance with become: yes. Uses the dnf module for package management — Amazon Linux 2023 replaced yum with dnf, a distinction that matters in practice. The copy module deploys the HTML page directly to the Nginx web root. The service module starts Nginx and enables it on boot. Idempotency confirmed — a second playbook run after an HTML correction showed changed=1 on only the modified file.
The initial terraform apply completed successfully but all 7 resources landed in us-east-1. The AMI ID ami-0c02fb55956c7d316 is region-specific and only valid in us-east-1, making it unusable in the target region. The EC2 instance also wasn't visible in the AWS Console because EC2 is a regional service — the console only shows instances in the currently selected region. The S3 bucket did appear because S3 is a global service that shows all buckets regardless of region.
Ran terraform destroy, updated variables.tf with the correct region (us-east-2) and the correct Amazon Linux 2023 AMI for that region (ami-0acc7a0cdd879d0f2). AMI IDs are not portable across regions — the same OS image has a different ID in every region and must be verified before deployment.
During the second terraform apply, all 6 other resources provisioned cleanly but the S3 bucket creation hung indefinitely. The previous bucket from the us-east-1 run was still in a releasing state after destroy. S3 bucket names are globally unique across all AWS accounts worldwide — not just within your own account or region. A name that's claimed anywhere, even temporarily, cannot be used again until it fully releases.
Cancelled the apply with Ctrl+C and renamed the bucket from tc3-webapp-bucket-leyder-ben to tc3-webapp-bucket-ben-leyder. The name change cleared the global namespace conflict and the bucket created immediately on reapply. Unlike EC2 or most AWS resources where naming conflicts are scoped to a single account, S3 names are a shared global namespace — if Terraform hangs specifically on S3 creation with no error, name conflict is the first thing to check.
After updating the Terraform variables, a sed command was run to swap yum for dnf in the playbook. The command wiped the file instead of doing a clean in-place substitution, leaving it empty. Ansible returned "Empty playbook, nothing to do" with no other error.
Rewrote the playbook directly using cat with a heredoc. For files containing special characters or complex formatting, writing directly with cat or nano is more reliable than sed substitution. Also confirmed that Amazon Linux 2023 uses dnf — not yum — as the package manager. The Ansible yum module will still work on AL2023 as a compatibility alias, but using dnf explicitly is the correct practice.
These tools are often grouped together as "IaC" but they solve fundamentally different problems. Understanding the distinction matters for building maintainable infrastructure.
| TERRAFORM | ANSIBLE | |
|---|---|---|
| Primary role | Infrastructure provisioning | Configuration management |
| What it manages | Cloud resources (EC2, S3, VPC, IAM) | Software and config on running servers |
| State tracking | Yes — maintains tfstate, plans diffs | No — checks current state on each run |
| Idempotency | Via state file and plan | Built-in — only changes what needs changing |
| Connection method | AWS API calls | SSH into running instances |
| Language | HCL (HashiCorp Configuration Language) | YAML playbooks |
| Best used for | Spinning up the environment | Configuring what runs inside it |