Ephemeral demo environment deployed on AWS. Auto-destroys after 24 hours via a Lambda function scheduled by EventBridge. Re-running terraform apply resets the 24h timer.
Developers: review docs/ai-instructions.md for project structure, coding conventions, and linting rules.
flowchart TD
User([User])
User -->|HTTPS| CF
User -->|S3 API| S3Personal
CF["Cloudflare DNS"]
CF -->|CNAME| CDN
CDN["CloudFront <br/> CDN · TLS termination"]
CDN -->|"default · HTTPS + origin verify"| ALB
CDN -->|"/static/* · OAC"| S3Static
EB["EventBridge <br/> hourly schedule"] --> Destroyer["Lambda destroyer <br/> destroys only TTL-tagged resources (ALB, ECS, NAT Gateway etc.)"]
%% Destroyer -->|destroys| ALB
%% Destroyer -->|destroys| ECS
%% Destroyer -->|destroys| NATgw
subgraph VPC["VPC 10.0.0.0/16"]
ALB["Application Load Balancer"]
ALB -->|HTTP| ALBnodeA
subgraph AZa["AZ-a"]
subgraph Public["Public 10.0.1.0/24"]
ALBnodeA["ALB node"]
NATgw["NAT Gateway <br/> (optional)"]
ECS["ECS Fargate <br/> httpd:2.4"]
end
subgraph Private["Private 10.0.10.0/24"]
WebApp["EC2: Web app <br/> (optional)"]
RDS[("RDS <br/> (optional)")]
end
end
ALB -.->|none| ALBnodeB
subgraph AZb["AZ-b"]
subgraph PublicB["Public 10.0.2.0/24"]
ALBnodeB["ALB node"]
end
end
end
Obsidian["Obsidian <br/> Remotely Save plugin"]
Obsidian <-->|S3 API| S3Obsidian
subgraph S3["S3 buckets"]
S3Obsidian[("Obsidian vaults")]
S3Static[("Static site content")]
S3Personal[("00-personal")]
end
ALBnodeA -->|HTTP| WebApp
ALBnodeA -->|HTTP| ECS
The repo pins tool versions via mise and manages Python dev
dependencies (currently just pre-commit) with uv.
Common dev tasks are wrapped in the Makefile.
brew install mise # macOS · one-time, bootstraps uv + terraform
make install # uv sync --frozen — create .venv from uv.lock
mise exec -- uv run pre-commit install # enable git hooks (once per clone)Other handy targets:
make help # list all targets
make lint # run all pre-commit hooks against the whole repo
make lock # regenerate uv.lock after editing pyproject.tomlFor the full list of configured hooks and the Make targets reference, see docs/ai-instructions.md.
Bootstrap infrastructure must be applied first — see bootstrap/README.md. Bootstrap creates:
- S3 bucket and DynamoDB table for this module's Terraform state
- Lambda function that auto-destroys this environment after 24h
- EventBridge schedule that triggers the Lambda hourly
| Module | Source | Purpose |
|---|---|---|
networking |
./modules/networking |
VPC, subnets, IGW, optional NAT Gateway, S3 VPC endpoint |
application_load_balancer |
./modules/application-load-balancer |
ALB, target group, HTTPS listener, security groups |
ecs_fargate |
./modules/ecs-fargate |
ECS cluster, Fargate service, task definition |
ssl_certificates |
./modules/ssl-certificates |
ACM certificates (regional + us-east-1 for CloudFront) |
static_site |
./modules/static-site |
S3 origin bucket, CloudFront distribution |
dns_cloudflare |
./modules/dns-cloudflare |
Cloudflare DNS records, ACM DNS validation |
generic_storage |
./modules/generic-storage |
IAM user with MFA-enforced S3 role |
obsidian_vaults |
./modules/obsidian-vaults |
S3 bucket + IAM user for Obsidian vault sync |
export AWS_PROFILE=Cargonautica
aws sso login
# 1. Complete bootstrap setup (once)
cd bootstrap && terraform apply && cd ..
# 2. Configure backend.tf with bootstrap outputs (once)
# See bootstrap/README.md Step 5
# 3. Deploy demo environment
terraform init
terraform plan
terraform apply
# 4. Get the site URL
terraform output cloudfront_domain_name
# 5. Environment auto-destroys after 24h, or destroy manually:
terraform destroy| Name | Description | Type | Default | Required |
|---|---|---|---|---|
aws_region |
AWS region to deploy resources | string |
"eu-central-1" |
no |
environment |
Environment name | string |
"demo" |
no |
project_name |
Project name for resource naming and tagging | string |
"aws-demo" |
no |
vpc_cidr |
CIDR block for the VPC | string |
"10.0.0.0/16" |
no |
create_nat_gateway |
Create NAT Gateway for private subnet internet access (~$1/day) | bool |
false |
no |
domain_name |
Domain name for ACM certificate and CloudFront | string |
"turbobasic.dev" |
no |
cloudflare_api_token |
Cloudflare API token | string |
— | yes |
cloudflare_zone_id |
Cloudflare zone ID | string |
— | yes |
obsidian_bucket_name |
S3 bucket name suffix for Obsidian vaults | string |
"obsidian-sync" |
no |
obsidian_iam_user_name |
IAM user name for Obsidian vault access | string |
"obsidian-sync-user" |
no |
| Name | Description | Sensitive |
|---|---|---|
alb_dns_name |
DNS name of the Application Load Balancer | no |
vpc_id |
ID of the VPC | no |
ecs_cluster_name |
ECS cluster name | no |
ecs_service_name |
ECS service name | no |
cloudfront_distribution_id |
CloudFront distribution ID | no |
cloudfront_domain_name |
CloudFront distribution domain name | no |
s3_static_bucket_name |
S3 bucket name for static site content | no |
s3_static_bucket_arn |
S3 bucket ARN for static site content | no |
obsidian_vault_bucket_name |
S3 bucket name for Obsidian vaults | no |
obsidian_sync_access_key_id |
IAM access key ID for Obsidian sync | no |
obsidian_sync_secret_access_key |
IAM secret access key for Obsidian sync | yes |
generic_storage_access_key_id |
Access key ID for the s3-user IAM account | no |
generic_storage_secret_access_key |
Secret access key for the s3-user (MFA-backed) | yes |
generic_storage_role_arn |
ARN of the S3AccessRole with MFA enforcement | no |
Retrieve sensitive outputs with:
terraform output -raw obsidian_sync_secret_access_key
terraform output -raw generic_storage_secret_access_keyThe generic_storage module creates an IAM user with MFA-enforced S3 access. This setup provides:
- s3-user — IAM user for programmatic S3 access
- Access Keys — For AWS CLI and SDK authentication
- S3AccessRole — MFA-enforced role for elevated S3 permissions
- MFA Requirement — Role assumption requires Multi-Factor Authentication
After terraform apply, set up MFA for the s3-user:
# 1. Register MFA device (saves credentials to 1Password)
./scripts/setup-s3-user-mfa.sh --cleanup
# 2. Follow the prompted instructions to enable MFA
# 3. Access Terraform outputs
terraform output generic_storage_access_key_id
terraform output -raw generic_storage_secret_access_key
terraform output generic_storage_role_arnSee modules/generic-storage/README.md for AWS CLI profile configuration, usage examples, and the rationale for the two-profile setup.
The obsidian_vaults module creates a dedicated S3 bucket and IAM user for syncing Obsidian vaults using the Remotely Save plugin.
Unlike the rest of the demo environment, these resources are not tagged for auto-destruction — the vault data persists across terraform destroy / terraform apply cycles only if you manage state accordingly.
Configure the Remotely Save plugin with:
| Setting | Value |
|---|---|
| Remote service | S3 |
| Endpoint | s3.eu-central-1.amazonaws.com (or your region) |
| Region | eu-central-1 |
| Bucket name | value of obsidian_vault_bucket_name output |
| Access key ID | value of obsidian_sync_access_key_id output |
| Secret access key | value of obsidian_sync_secret_access_key output |
| Resource | Cost |
|---|---|
| ALB | ~$0.70/day |
| ECS Fargate (0.25 vCPU, 0.5 GB) | ~$0.10/day |
| CloudFront | ~$0.01/day (minimal traffic) |
| NAT Gateway (if enabled) | ~$1.10/day |
| S3 (state + static site + Obsidian) | <$0.01/day |
| Total (without NAT) | ~$0.81/day |
See docs/ai-instructions.md for the annotated project structure.
- Bootstrap README — first-time setup
- Coding conventions & project structure
- Architecture plan