Files
Ben Duncan adb3d9876d fix: read a tagged bucket's tags through S3 instead of S3 Control (#1109)
The AWS provider reads a bucket's tags with `ListTagsForResource` on S3 Control when it knows the account ID. S3 Control puts the account in the hostname — `https://{account}.{endpoint}` — so against an IP-addressed endpoint the request fails a DNS lookup before a packet leaves, and no endpoint configuration can help. A tagged bucket therefore fails its first read after being created.

`skip_requesting_account_id` alone does not fix it: `aws-sdk-go-base` takes the account ID from the STS `GetCallerIdentity` credentials check before it consults that flag, so `skip_credentials_validation` is needed alongside it. With no account ID the provider falls back to `GetBucketTagging`, which predastore serves.

`s3-webapp` is the only workbook that creates a bucket, and it uses no `aws_caller_identity` and references no other security group, so a blank account ID costs it nothing.

The bucket also gets `force_destroy`, since the webapp uploads into it and a destroy of a used bucket fails `BucketNotEmpty` without it.

The e2e harness now asserts the bucket carries the tags Terraform set, which is what catches this regressing, and no longer aborts the remaining workbooks on a failure — `s3-webapp` failing says nothing about whether RDS works, and four nights of `rds-quickstart` coverage were lost that way. `STOP_ON_FAIL=1` restores the early abort.

Every workbook's provider versions are pinned, so a run reproduces rather than picking up whatever the registry published that morning.
2026-09-26 16:11:00 -05:00
..

title, seoTitle, description, category, tags, resources
title seoTitle description category tags resources
S3-Backed Web App Terraform S3-Backed Flask Web App on EC2 — Spinifex Docs Deploy a Flask file-sharing app on EC2 backed by S3 (Predastore) with Terraform, using an IAM instance profile and short-lived STS credentials from IMDS. Terraform Workbooks
terraform
s3
predastore
flask
webapp
workbook
imds
sts
instance-profile
title url
Terraform AWS Provider https://registry.terraform.io/providers/hashicorp/aws/latest
title url
Spinifex Repository https://github.com/mulgadc/spinifex
title url
Predastore (S3) https://github.com/mulgadc/predastore
title url
OpenTofu https://opentofu.org/

Terraform: S3-Backed Web App

Deploy a Flask file-sharing web application backed by S3 (Predastore) using Terraform on Spinifex.

Table of Contents


Overview

Deploy an EC2 instance running a Flask file-sharing web application backed by S3 (Predastore). Users can upload files through a web form and browse uploaded content — demonstrating Terraform managing both compute and object storage together.

This workbook uses the idiomatic AWS credential model: the instance is launched with an IAM instance profile and the app pulls short-lived STS credentials from IMDS (169.254.169.254) through boto3's default credential chain. No long-lived S3 keys are baked into the instance. The Terraform run still uses the operator's admin credentials (to create the bucket, role, profile, and instance), but the running instance authenticates to S3 with credentials it fetches at runtime and which expire in ~1 hour.

Architecture:

S3 webapp — browser to Flask EC2 instance; the instance fetches short-lived STS credentials from IMDS (169.254.169.254) via its instance profile, then calls Predastore over the S3 API

What you'll learn:

  • Configuring the AWS provider with both Spinifex and Predastore endpoints
  • Creating S3 buckets on Predastore via Terraform
  • Defining an IAM role, least-privilege managed policy, and instance profile, and passing the role to an instance (iam:PassRole)
  • How the instance obtains short-lived credentials from IMDS via boto3's default credential chain — no static keys on the instance
  • Deploying a Python webapp with cloud-init that signs S3 requests with the STS session token

Prerequisites:

  • Spinifex installed and running (see Installing Spinifex)
  • Predastore running (S3 API on port 8443)
  • An Ubuntu 26.04 AMI imported (see Setting Up Your Cluster)
  • OpenTofu or Terraform installed
  • The operator identity running the Terraform apply (AWS_PROFILE=spinifex) must be allowed to manage IAM and pass the role: iam:CreateRole, iam:CreatePolicy, iam:AttachRolePolicy, iam:CreateInstanceProfile, iam:AddRoleToInstanceProfile, and iam:PassRole on the new role. The bootstrap admin profile satisfies this.
  • The EC2 instance must be able to reach Predastore — use the host's br-wan IP, not localhost

Instructions

Step 1. Get the Template

Clone the Terraform examples from the Spinifex repository:

git clone --depth 1 --filter=blob:none --sparse https://github.com/mulgadc/spinifex.git spinifex-tf
cd spinifex-tf
git sparse-checkout set docs/terraform
cd docs/terraform/s3-webapp

Or create the files manually and paste the full configuration below.

Step 2. Create terraform.tfvars

Before deploying, create a terraform.tfvars with your Predastore credentials. The predastore_host must be reachable from inside the VPC — use the host's br-wan or LAN IP, not localhost.

Step 3. Create main.tf

Step 4. Deploy

The workbook defaults to t3.small (2 vCPU, 2 GiB). On clusters without that type registered, override with TF_VAR_instance_type — query what's available with aws ec2 describe-instance-types.

export AWS_PROFILE=spinifex
tofu init
tofu apply

Step 5. Test the Application

Note: EC2 instances can take 30+ seconds to boot after apply. If SSH or HTTP is unreachable, wait and retry.

Open the web_url output in your browser. You should see the file browser UI. Upload a file and verify it appears in the list.

# Verify via CLI
curl http://<public_ip>

# Check the S3 bucket directly
aws s3 ls s3://webapp-uploads/ --profile spinifex --endpoint-url https://localhost:8443

Clean Up

tofu destroy

Troubleshooting

Predastore Connection Refused from Instance

The EC2 instance cannot reach localhost on the host. Set predastore_host to the host's br-wan or LAN IP address:

predastore_host = "192.168.1.10:8443"

S3 Bucket Creation Fails

Verify Predastore is running and accessible:

curl -k https://localhost:8443/
aws s3 ls --profile spinifex --endpoint-url https://localhost:8443

ListTagsForResource ... no such host After the Bucket Is Created

The bucket was created and the failure is on the read-back of its tags. AWS provider 6.x reads bucket tags through S3 Control, whose endpoint carries the account ID in the hostname, so the provider dials https://000000000001.<your-endpoint> — a name that cannot resolve against an IP address.

This workbook's provider block already sets skip_requesting_account_id and skip_credentials_validation, which is what keeps the provider on S3 GetBucketTagging. If you copied the block into your own configuration, carry both across: skip_requesting_account_id on its own has no effect, because the credentials check supplies the account ID before the account lookup is skipped.

Flask App Not Starting

SSH into the instance and check the service:

ssh -i s3-webapp-demo.pem ubuntu@<public_ip>
sudo systemctl status s3-webapp
sudo journalctl -u s3-webapp --no-pager -n 50

Upload Fails with 403 AccessDenied

The instance reached S3 but was not authorized. The .env file deliberately holds no keys — credentials come from IMDS:

ssh -i s3-webapp-demo.pem ubuntu@<public_ip>
cat /opt/webapp/.env   # S3_ENDPOINT / S3_BUCKET / S3_REGION only — no keys

Check that:

  • the managed policy is attached to the role (s3:ListBucket on the bucket, s3:GetObject/s3:PutObject on its objects), and the policy's bucket name matches bucket_name;
  • the assumed-role session's account matches the bucket owner's account — Predastore enforces bucket ownership. Both are created by the same operator identity, so they align by construction; a mismatch surfaces as 403.

NoCredentialsError / No Credentials

boto3 could not obtain credentials, which means the IMDS credential chain did not resolve. From the instance:

ssh -i s3-webapp-demo.pem ubuntu@<public_ip>

# IMDSv2: fetch a token, then the role name behind the instance profile
TOKEN=$(curl -sX PUT http://169.254.169.254/latest/api/token \
  -H 'X-aws-ec2-metadata-token-ttl-seconds: 60')
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/

# Confirm the resolved identity — expect assumed-role/s3-webapp-role/<instance-id>
aws sts get-caller-identity --endpoint-url https://<predastore_host>

If the role name is empty, the instance profile was not attached — confirm iam_instance_profile on the instance and that iam:PassRole is allowed for the operator.

AMI Not Found

Ensure you have imported an Ubuntu 26.04 image:

aws ec2 describe-images --owners 000000000000 --profile spinifex