445 lines
10 KiB
HCL
445 lines
10 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 = "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/*",
|
|
]
|
|
},
|
|
{
|
|
Effect = "Allow"
|
|
Action = ["cloudfront:CreateInvalidation", "cloudfront:ListDistributions"]
|
|
Resource = "*"
|
|
}
|
|
]
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|