terraform { required_version = ">= 1.5" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } tls = { source = "hashicorp/tls" version = "~> 4.0" } } } provider "aws" { region = var.region } # --- Variables --- variable "region" { default = "us-east-1" } variable "vpc_id" { description = "VPC ID to deploy into" type = string } variable "subnet_ids" { description = "Subnet IDs — both EC2 instances are placed in subnet_ids[0] (same AZ for low latency)" type = list(string) } variable "db_password" { description = "Unused — kept for tfvars compatibility. Local Postgres uses trust auth." type = string default = "" sensitive = true } variable "ssh_cidr" { description = "CIDR block for SSH access (e.g., 203.0.113.50/32)" type = string } variable "ec2_instance_type" { default = "c5.2xlarge" } variable "ec2_ami" { description = "EC2 AMI ID (leave empty for latest Amazon Linux 2023)" type = string default = "" } variable "scanning" { description = "Set to true during scanning phase, false for serving-only (tears down EC2 instances)" type = bool default = true } variable "domain" { description = "Domain name for the site (e.g., everytab.site)" type = string default = "everytab.site" } variable "repo_url" { description = "Git repo URL for the pipeline code (public)" type = string default = "" } # --- Data sources --- data "aws_ami" "al2023" { most_recent = true owners = ["amazon"] filter { name = "name" values = ["al2023-ami-2023*-x86_64"] } filter { name = "state" values = ["available"] } } # --- SSH Key --- resource "tls_private_key" "ec2" { count = var.scanning ? 1 : 0 algorithm = "ED25519" } resource "aws_key_pair" "ec2" { count = var.scanning ? 1 : 0 key_name = "everytab-key" public_key = tls_private_key.ec2[0].public_key_openssh } # --- Security Groups --- resource "aws_security_group" "ec2" { count = var.scanning ? 1 : 0 name = "everytab-ec2" description = "EveryTab EC2 instance" vpc_id = var.vpc_id ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = [var.ssh_cidr] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } resource "aws_security_group" "db" { count = var.scanning ? 1 : 0 name = "everytab-db" description = "EveryTab DB instance (Postgres on NVMe)" vpc_id = var.vpc_id ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = [var.ssh_cidr] } ingress { from_port = 5432 to_port = 5432 protocol = "tcp" security_groups = [aws_security_group.ec2[0].id] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } } # --- IAM --- resource "aws_iam_role" "ec2" { count = var.scanning ? 1 : 0 name = "everytab-ec2-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "ec2.amazonaws.com" } Action = "sts:AssumeRole" }] }) } resource "aws_iam_role_policy" "s3_access" { count = var.scanning ? 1 : 0 name = "everytab-s3-access" role = aws_iam_role.ec2[0].id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket", "s3:HeadObject"] Resource = [ aws_s3_bucket.site.arn, "${aws_s3_bucket.site.arn}/*", ] }, { Effect = "Allow" Action = ["s3:GetObject", "s3:ListBucket"] Resource = [ "arn:aws:s3:::commoncrawl", "arn:aws:s3:::commoncrawl/*", ] } ] }) } resource "aws_iam_instance_profile" "ec2" { count = var.scanning ? 1 : 0 name = "everytab-ec2-profile" role = aws_iam_role.ec2[0].name } # --- S3 --- resource "aws_s3_bucket" "site" { bucket = "everytab-site" } resource "aws_s3_bucket" "logs" { bucket = "everytab-logs" } resource "aws_s3_bucket_lifecycle_configuration" "logs" { bucket = aws_s3_bucket.logs.id rule { id = "expire-old-logs" status = "Enabled" filter {} expiration { days = 365 } } } resource "aws_s3_bucket_public_access_block" "logs" { bucket = aws_s3_bucket.logs.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } resource "aws_s3_bucket_ownership_controls" "logs" { bucket = aws_s3_bucket.logs.id rule { object_ownership = "BucketOwnerPreferred" } } # --- ACM Certificate (must be us-east-1 for CloudFront) --- resource "aws_acm_certificate" "site" { domain_name = var.domain validation_method = "DNS" lifecycle { create_before_destroy = true } } # This resource waits until the cert is validated (you must add the DNS record in Gandi first) resource "aws_acm_certificate_validation" "site" { certificate_arn = aws_acm_certificate.site.arn } # --- CloudFront --- resource "aws_cloudfront_origin_access_control" "site" { name = "everytab-site-oac" origin_access_control_origin_type = "s3" signing_behavior = "always" signing_protocol = "sigv4" } resource "aws_cloudfront_distribution" "site" { enabled = true default_root_object = "index.html" aliases = [var.domain] price_class = "PriceClass_100" # US + Europe only (cheapest) origin { domain_name = aws_s3_bucket.site.bucket_regional_domain_name origin_id = "s3-site" origin_access_control_id = aws_cloudfront_origin_access_control.site.id } default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] target_origin_id = "s3-site" viewer_protocol_policy = "redirect-to-https" compress = true forwarded_values { query_string = false cookies { forward = "none" } } # Long cache for bundles, short for HTML/JS during development min_ttl = 0 default_ttl = 86400 # 1 day max_ttl = 31536000 # 1 year } viewer_certificate { acm_certificate_arn = aws_acm_certificate_validation.site.certificate_arn ssl_support_method = "sni-only" minimum_protocol_version = "TLSv1.2_2021" } logging_config { bucket = aws_s3_bucket.logs.bucket_domain_name prefix = "cloudfront/" include_cookies = false } restrictions { geo_restriction { restriction_type = "none" } } } # S3 bucket policy: allow CloudFront OAC to read resource "aws_s3_bucket_policy" "site" { bucket = aws_s3_bucket.site.id policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "cloudfront.amazonaws.com" } Action = "s3:GetObject" Resource = "${aws_s3_bucket.site.arn}/*" Condition = { StringEquals = { "AWS:SourceArn" = aws_cloudfront_distribution.site.arn } } }] }) } # --- DB Instance (i3.large with local NVMe for Postgres) --- variable "db_instance_type" { default = "i3.large" } resource "aws_instance" "db" { count = var.scanning ? 1 : 0 ami = var.ec2_ami != "" ? var.ec2_ami : data.aws_ami.al2023.id instance_type = var.db_instance_type key_name = aws_key_pair.ec2[0].key_name vpc_security_group_ids = [aws_security_group.db[0].id] subnet_id = var.subnet_ids[0] user_data = file("${path.module}/db-setup.sh") tags = { Name = "everytab-db" } } # --- EC2 --- resource "aws_instance" "main" { count = var.scanning ? 1 : 0 ami = var.ec2_ami != "" ? var.ec2_ami : data.aws_ami.al2023.id instance_type = var.ec2_instance_type key_name = aws_key_pair.ec2[0].key_name vpc_security_group_ids = [aws_security_group.ec2[0].id] subnet_id = var.subnet_ids[0] iam_instance_profile = aws_iam_instance_profile.ec2[0].name user_data = templatefile("${path.module}/ec2-userdata.sh", { db_private_ip = aws_instance.db[0].private_ip repo_url = var.repo_url }) root_block_device { volume_size = 1000 volume_type = "gp3" } tags = { Name = "everytab" } } # --- Outputs --- output "ec2_public_ip" { value = var.scanning ? aws_instance.main[0].public_ip : null } output "db_private_ip" { value = var.scanning ? aws_instance.db[0].private_ip : null } output "db_public_ip" { value = var.scanning ? aws_instance.db[0].public_ip : null } output "database_url" { value = var.scanning ? "postgres://everytab@${aws_instance.db[0].private_ip}:5432/everytab" : null } output "ssh_private_key" { value = var.scanning ? tls_private_key.ec2[0].private_key_openssh : null sensitive = true } output "ssh_command" { value = var.scanning ? "ssh -i everytab-key ec2-user@${aws_instance.main[0].public_ip}" : null } output "ssh_command_db" { value = var.scanning ? "ssh -i everytab-key ec2-user@${aws_instance.db[0].public_ip}" : null } output "cloudfront_domain" { value = aws_cloudfront_distribution.site.domain_name } output "cloudfront_distribution_id" { value = aws_cloudfront_distribution.site.id } # DNS records to add in Gandi: # 1. ACM certificate validation (one-time, add this CNAME then wait for validation) # 2. Domain pointing to CloudFront (CNAME or ALIAS for bare domain) output "acm_validation_records" { value = { for dvo in aws_acm_certificate.site.domain_validation_options : dvo.domain_name => { type = dvo.resource_record_type name = dvo.resource_record_name value = dvo.resource_record_value } } } output "dns_cname_target" { description = "Point your domain to this CloudFront distribution (ALIAS/CNAME in Gandi)" value = aws_cloudfront_distribution.site.domain_name }