A little website.
A real cloud lesson.
Open an AWS account, keep costs small, and put a website online. Then rebuild the same design with Terraform and change its content with Ansible.
These are sample names, not our infrastructure. Before copying commands, read Customize the examples: paths, keys, IPs and names. Substitute your own authorized account and resources. Existing lab? Retain its original resource names, usernames and state; renamed samples can cause replacement. See the existing-lab warning before applying updated recipes.
What “a server in S3” really means
Think of S3 as a toy box for your website files. CloudFront is the helper who brings those files to visitors. Terraform is the instruction sheet for building the toy box and helper. Ansible swaps the toys inside.
S3 is storage, not a computer running Windows or Linux. It can hold HTML, CSS, JavaScript and pictures. This lab creates a static website: the browser displays saved files. It cannot run Python, PHP, a database or a login backend in S3. For those jobs, you would add a service such as Lambda or EC2 in a later lab.
The website is public; the bucket stays private. CloudFront receives permission to read it. We use the normal S3 bucket origin, not the S3 website endpoint, because website endpoints only support HTTP and do not support origin access control.
1. Open your account and find your credits
- Visit AWS Free Tier and choose Create an AWS account. Use an email you control and a unique password. Complete the contact, payment and identity verification steps shown by AWS. Select Basic support for learning.
- Choose the Free account plan if offered and you want a limited learning account. A Paid plan can charge for usage beyond applicable credits. Read the signup summary before accepting it.
- Open Billing and Cost Management. Check your account plan, credit balance, expiration dates and available credit-earning activities.
- New eligible customers receive $100 at signup and can earn up to $100 more through the activities AWS presents. Follow those activities individually and remove their resources afterward. An S3 upload alone does not guarantee a credit reward.
AWS: current eligibility, credits and plan rules
Put a lock on the account
Your first login is the root user, the master key to the account. Enable multi-factor authentication (MFA) in its security credentials page. Keep recovery details safe. Use root for initial account administration only, never for routine deployments or access keys.
Create a separate lab identity
If you already have IAM Identity Center, use its existing temporary-credential login. Otherwise, for this standalone beginner lab, create an IAM user named contoso-lab and a customer-managed policy using the JSON below. Attach the policy to that user. If you enable console sign-in, give the user a unique password and MFA, then use its account sign-in URL for the lab.
This policy allows S3 administration only for buckets beginning contoso-lab-. Its listed CloudFront permissions apply across the account, so use it in a dedicated learning account. It is a lab starting point, not a tightly scoped production policy. Do not attach it to a user in a shared production account.
2. Make the bill boring
A budget alert is a smoke alarm. It tells you something is happening; it does not turn off the stove.
- In Billing, open Budgets → Create budget. If available, start with the zero-spend template. Also create a monthly cost budget of $5 with email alerts at 50%, 80% and 100%, plus a forecasted 100% alert.
- For your learning budget, inspect the advanced cost options and exclude credits and refunds so credits do not hide underlying service usage. Check both this budget and the actual bill.
- Enable Free Tier usage alerts in Billing preferences if offered. Read the email you receive, and check Billing after every lab. Cost data and alerts can arrive late.
- Keep the website tiny: one HTML file, a few manual visits, no large downloads or traffic generators. Use
us-east-1for this example and label resourcesProject=ContosoLearning. - Use the supplied CloudFront domain. Skip buying a domain, Route 53, WAF, access logs and extra services for this first lab. No EC2 computer, NAT gateway or load balancer is needed.
| Item | What to watch |
|---|---|
| S3 | Stored data and read/write requests. Delete unused files; do not upload large videos. |
| CloudFront | Requests and data delivered, depending on pricing plan and allowances. Caching reduces origin reads. |
| Optional extras | Domains, DNS, logs and paid security options can create separate charges. |
For an estimate, open the AWS Pricing Calculator, add S3 and CloudFront, and try 0.01 GB stored, 1,000 S3 reads, 100 writes, 1,000 viewer requests and 0.1 GB delivered per month. These are example inputs, not enforced limits. Review the region and delivery geography; calculate before credits and compare with the offers in your account.
AWS: budget delays and behaviorS3 pricingCloudFront pricingPricing Calculator
3. Build it once in the browser
This part works the same on Windows and Linux. Console labels can change; follow the settings below rather than choosing paid add-ons when offered.
A. Save your first page
Copy this into a text editor and save it as index.html using UTF-8. On Windows, choose “All files” in Notepad so it does not become index.html.txt. Open it locally to see the page.
B. Create the S3 bucket
- Sign in as your lab user. Open S3 → Create bucket, choose a general purpose bucket in
us-east-1, and give it a unique name such ascontoso-lab-blue-kite-7f3a-manual. Change the random suffix if taken. Bucket names are globally unique and can appear in URLs; do not use personal information. - Keep Block all public access enabled, ACLs disabled (Bucket owner enforced), and default S3-managed encryption (SSE-S3). Leave versioning off for this disposable lab. Do not enable static website hosting.
- Create the bucket, open its Objects tab, and upload
index.htmlat the bucket root. In upload properties set content type totext/htmland Cache-Control topublic, max-age=60if available. Keep the original local file.
C. Add the HTTPS delivery service
- Open CloudFront → Create distribution. To match the Terraform lab, use a standard pay-as-you-go distribution; inspect the price summary before creating it.
- Choose your S3 bucket as the origin using its regular bucket domain. Do not switch to the website endpoint if suggested.
- For origin access, create an Origin Access Control (OAC), select S3 and “Sign requests / always.” Think of OAC as CloudFront's signed permission slip.
- Set default root object to
index.html, viewer protocol policy to Redirect HTTP to HTTPS, and allowed methods to GET and HEAD. Use the default CloudFront certificate and no custom domain. - For the learning cache, use a custom/legacy cache configuration with minimum TTL 0, default 60 seconds and maximum 300 seconds; forward no cookies or query strings. This keeps content updates easy to observe. Decline optional paid add-ons.
- Create the distribution. Copy the bucket policy generated for this OAC into S3 → your bucket → Permissions → Bucket policy. It should allow
cloudfront.amazonaws.comto read objects only whenAWS:SourceArnequals this distribution's ARN. Keep public access blocked. - Wait until deployment finishes, then open
https://YOUR-DISTRIBUTION.cloudfront.net. CloudFront supplies HTTPS for this address; no certificate purchase is needed.
OAC and generated bucket policyHTTPS between CloudFront and S3
4. Prepare Windows or Linux
Use PowerShell on Windows for AWS CLI and Terraform. Ansible runs from Ubuntu in WSL on Windows, or directly on Linux. WSL is a small Linux environment inside Windows; it is not an AWS resource.
- Install AWS CLI v2 for Windows using its official installer.
- Install Terraform for Windows, put its executable on PATH, and open a new PowerShell window.
- Run the checks below. Install WSL from an administrator PowerShell window, restart if requested, and open Ubuntu to finish its username/password setup. Your local Linux password is separate from AWS.
aws --version
terraform version
wsl --install -d UbuntuFor Terraform, keep the lab in a local folder such as C:\Labs\contoso-aws, outside a shared or cloud-synced folder. For Ansible, copy index.html and update-site.yml into ~/contoso-ansible inside Ubuntu. Install AWS CLI there too and configure the contoso profile again; Windows credentials do not automatically become WSL credentials.
- Install AWS CLI v2 for your Linux architecture. The official instructions distinguish x86-64 and ARM; choose the matching package.
- Install Terraform using HashiCorp's Ubuntu instructions.
- Run the checks below and create a folder such as
~/contoso-awsfor the lab files.
aws --version
terraform version
mkdir -p ~/contoso-aws
cd ~/contoso-awsLet the tools use your lab identity
Prefer temporary credentials if your existing account offers them. With an already configured IAM Identity Center portal, run aws configure sso --profile contoso, then aws sso login --profile contoso. Do not create an AWS Organization solely for this lab: joining one can change Free Tier credit eligibility.
For the standalone IAM user from step 1, an administrator can create a lab access key in IAM → Users → contoso-lab → Security credentials. Choose the CLI use case and configure it below. Enter the key only at the prompts. The local credentials file stores it in plain text, so keep it outside your project and remove the key after cleanup. This fallback uses long-lived keys; MFA on console sign-in does not automatically protect API calls made with them.
aws configure --profile contoso
# Enter your lab user's access key and secret at the prompts.
# Default region: us-east-1
# Default output format: json
aws sts get-caller-identity --profile contosoThe last command should show the lab identity, not root. Do not post its output publicly. Never place keys in HTML, Terraform, Ansible, Git or screenshots.
AWS: access-key handlingAWS: existing SSO setupMicrosoft: install WSL
5. Rebuild the design with Terraform
Instead of clicking each setting, write down what the finished setup should look like. Terraform reads that recipe, shows the changes, then builds it.
Save the following as main.tf in your empty Terraform lab folder. Save a copy of index.html in that folder too. The code keeps S3 private, signs origin requests, redirects visitors to HTTPS and uses a short cache. It intentionally leaves page content outside Terraform so Ansible can change it later.
Your three important commands
init downloads the AWS provider. plan previews the proposed resources. apply executes the reviewed plan. The provider version is limited to major version 6; keep the generated lockfile so later runs use the same selected version.
Create terraform.tfvars with the line below. Replace the name with a globally unique one beginning contoso-lab-. Keep this name distinct from your manual bucket.
bucket_name = "contoso-lab-blue-kite-7f3a-code"terraform init
terraform fmt
terraform validate
terraform plan -out=lab.tfplan
# Read the plan. Expect only this lab's S3 and CloudFront resources.
terraform apply lab.tfplan
terraform outputApplying the saved plan creates resources without another yes/no prompt, so inspect it first. CloudFront can take several minutes to deploy. The output prints the bucket, distribution ID and HTTPS address. Save .gitignore below before committing anything; state and saved plans can expose account details. Keep local state safe because Terraform needs it to remove resources.
Upload the page, then open the URL
$labBucket = terraform output -raw bucket_name
aws s3 cp ./index.html "s3://$labBucket/index.html" --profile contoso --region us-east-1 --content-type "text/html; charset=utf-8" --cache-control "public, max-age=60"
terraform output -raw site_urllab_bucket="$(terraform output -raw bucket_name)"
aws s3 cp ./index.html "s3://$lab_bucket/index.html" --profile contoso --region us-east-1 --content-type 'text/html; charset=utf-8' --cache-control 'public, max-age=60'
terraform output -raw site_urlterraform plan again: without configuration changes, expect no infrastructure changes. The uploaded file is intentionally not a Terraform resource.Terraform: CloudFront configurationHashiCorp: Terraform basics
6. Change the page with Ansible
Terraform builds the bookshelf. Ansible replaces the book on it. Neither tool needs to log into a server in this lab.
Use a Linux terminal (Ubuntu in WSL for Windows). Install Ansible and its AWS dependencies in a Python virtual environment, a separate toolbox that keeps packages out of the system Python.
sudo apt update
sudo apt install -y python3 python3-venv
python3 -m venv ~/.venvs/contoso
source ~/.venvs/contoso/bin/activate
python -m pip install ansible boto3 botocore
ansible-galaxy collection install amazon.aws
ansible --version
aws sts get-caller-identity --profile contosoSave update-site.yml beside your local index.html. Change the heading or a sentence in that HTML file, then run the playbook with the bucket name from Terraform's output. The playbook writes just index.html, not the whole bucket.
# Replace the example name with your Terraform bucket output.
ansible-playbook update-site.yml --syntax-check
ansible-playbook update-site.yml -e 'bucket_name=contoso-lab-blue-kite-7f3a-code'
# Run again without editing the file: expect changed=0.
ansible-playbook update-site.yml -e 'bucket_name=contoso-lab-blue-kite-7f3a-code'Revisit the HTTPS site after about a minute and refresh. CloudFront and your browser may still have an old copy cached. If you previously used a longer cache policy, wait for that earlier lifetime to expire. No invalidation is needed for this short-cache exercise.
terraform plan still reports no infrastructure changes.7. Finish the lab and stop paying for leftovers
The manual copy
- In CloudFront, select the manual lab distribution, disable it, wait until that change is deployed, then delete it.
- In S3, open the manual lab bucket, verify its exact name, empty it and then delete it. Emptying permanently removes its files. Keep your local copy first.
- Delete the unused manual Origin Access Control after its distribution is gone.
The Terraform copy
The bucket contains a file uploaded outside Terraform. Empty only this lab bucket first; then destroy the infrastructure. These commands permanently remove that bucket's contents and the resources in this folder's state. Verify the printed name before running the deletion.
$labBucket = terraform output -raw bucket_name
$labBucket
# Stop if this is not your disposable contoso-lab- bucket.
aws s3 rm "s3://$labBucket" --recursive --profile contoso --region us-east-1
terraform plan -destroy -out=cleanup.tfplan
# Read the deletion plan before proceeding.
terraform apply cleanup.tfplanlab_bucket="$(terraform output -raw bucket_name)"
printf '%s\n' "$lab_bucket"
# Stop if this is not your disposable contoso-lab- bucket.
aws s3 rm "s3://$lab_bucket" --recursive --profile contoso --region us-east-1
terraform plan -destroy -out=cleanup.tfplan
# Read the deletion plan before proceeding.
terraform apply cleanup.tfplanCloudFront removal may take several minutes. If it fails, keep your state and rerun the destroy plan after resolving the error. If you enabled versioning outside this guide, delete all versions and delete markers too: aws s3 rm alone will not empty a versioned bucket.
- Confirm both lab buckets and distributions are gone in the AWS console. Check other regions for resources created while earning credits.
- Deactivate and delete the IAM access key used for the lab. Remove the lab identity/policy when no longer needed. For SSO, use
aws sso logout. - Keep budgets enabled and inspect Billing again after usage data catches up. Destroying resources stops future use; it does not erase charges already incurred.
When something goes wrong
| Symptom | Try this |
|---|---|
| Bucket name already taken | Change the random suffix. If Terraform already created it, inspect the plan before renaming: renaming replaces the bucket. |
| CloudFront shows 403 | Confirm index.html exists at the root, default root object is set, origin is the normal bucket endpoint, and the OAC policy points to the correct distribution ARN. Keep public access blocked. |
| Direct S3 URL shows AccessDenied | This is expected for the private bucket. Test through CloudFront. |
| Old page still appears | Wait for the previous cache lifetime, then refresh. Verify Ansible targeted the code lab's bucket, not the manual bucket. |
| Profile not found / expired login | Configure contoso in the same Windows, Linux or WSL environment that runs the tool. Renew SSO login if needed. |
| AccessDenied during apply | Check the lab policy is attached to the correct identity, the bucket begins contoso-lab-, and the account plan permits the service. Inspect the denied action before adding permissions. |
| Ansible module missing | Activate the Python environment and install amazon.aws with ansible-galaxy; make sure boto3 and botocore are installed there. |
| Destroy says bucket not empty | Verify the exact lab bucket and remove its files. Versioned buckets also require deleting older versions and delete markers. |
You have now learned storage, identity, HTTPS, caching, infrastructure as code and repeatable content updates—without running a virtual server.