everytab/infra/main.tf

409 lines
9.7 KiB
HCL

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 = "At least 2 subnet IDs in different AZs (required for RDS subnet group)"
type = list(string)
}
variable "db_password" {
description = "Postgres master password"
type = string
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.xlarge"
}
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, RDS, icons bucket)"
type = bool
default = true
}
variable "domain" {
description = "Domain name for the site (e.g., everytab.site)"
type = string
default = "everytab.site"
}
# --- 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" "rds" {
count = var.scanning ? 1 : 0
name = "everytab-rds"
description = "EveryTab RDS instance"
vpc_id = var.vpc_id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.ec2[0].id]
}
}
# --- 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.icons[0].arn,
"${aws_s3_bucket.icons[0].arn}/*",
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" "icons" {
count = var.scanning ? 1 : 0
bucket = "everytab-icons"
force_destroy = true
}
resource "aws_s3_bucket_public_access_block" "icons" {
count = var.scanning ? 1 : 0
bucket = aws_s3_bucket.icons[0].id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket" "site" {
bucket = "everytab-site"
}
resource "aws_s3_bucket" "logs" {
bucket = "everytab-logs"
}
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
}
}
}]
})
}
# --- RDS ---
resource "aws_db_subnet_group" "main" {
count = var.scanning ? 1 : 0
name = "everytab"
subnet_ids = var.subnet_ids
}
resource "aws_db_instance" "main" {
count = var.scanning ? 1 : 0
identifier = "everytab"
engine = "postgres"
engine_version = "16"
instance_class = "db.t3.medium"
allocated_storage = 20
storage_type = "gp3"
db_name = "everytab"
username = "everytab"
password = var.db_password
db_subnet_group_name = aws_db_subnet_group.main[0].name
vpc_security_group_ids = [aws_security_group.rds[0].id]
publicly_accessible = false
multi_az = false
backup_retention_period = 0
skip_final_snapshot = true
}
# --- 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
root_block_device {
volume_size = 300
volume_type = "gp3"
}
tags = {
Name = "everytab"
}
}
# --- Outputs ---
output "ec2_public_ip" {
value = var.scanning ? aws_instance.main[0].public_ip : null
}
output "rds_endpoint" {
value = var.scanning ? aws_db_instance.main[0].endpoint : null
}
output "database_url" {
value = var.scanning ? "postgres://everytab:${var.db_password}@${aws_db_instance.main[0].endpoint}/everytab" : null
sensitive = true
}
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 "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
}