Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions ec2.tf
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ resource "aws_instance" "app" {
instance_type = var.instance_type
subnet_id = local.chosen_subnet_id
vpc_security_group_ids = [aws_security_group.app_sg.id]
iam_instance_profile = aws_iam_instance_profile.app_ec2_profile.name

# assign ssh key
key_name = var.key_name != "" ? var.key_name : null
Expand All @@ -40,6 +41,22 @@ resource "aws_instance" "app" {
# ensure a public IP if your subnet doesn't auto-assign
associate_public_ip_address = var.subnet_tier == "public" ? true : false

user_data = templatefile("${path.module}/scripts/user_data.sh", {
app_user = var.app_user
app_group = var.app_group
app_dir = var.app_dir
service_name = var.service_name

db_host = aws_db_instance.db.address
db_port = aws_db_instance.db.port
db_name = var.db_name
db_username = var.db_username
db_password = var.db_password

aws_region = var.region
s3_bucket = aws_s3_bucket.images.bucket
})

tags = {
Name = "${var.name_prefix}-ec2"
Role = "webapp"
Expand Down
84 changes: 84 additions & 0 deletions iam_role.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
resource "aws_iam_role" "app_ec2_role" {
name = "${var.name_prefix}-ec2-role"

# Trust Policy
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Principal = { Service = "ec2.amazonaws.com" }, # Who can assume this role. The 'Service' here represents the EC2 service itself.
Action = "sts:AssumeRole"
}]
})
}
# Instance Profile is a container for the IAM Role.
# EC2 cannot directly attach an IAM Role — it must attach an Instance Profile instead.
# The profile allows EC2 to assume the role and get temporary credentials automatically.
resource "aws_iam_instance_profile" "app_ec2_profile" {
name = "${var.name_prefix}-ec2-profile"
role = aws_iam_role.app_ec2_role.name
}

# ----------------------
# Setup Least Privilage
# ----------------------
locals {
# Retrieve the name and ARN of the S3 bucket created in Terraform.
bucket_name = aws_s3_bucket.images.bucket
bucket_arn = "arn:aws:s3:::${local.bucket_name}"

# Define the object-level ARN (optionally scoped to a prefix).
objects_arn = "arn:aws:s3:::${local.bucket_name}/${var.s3_prefix}*"
}

# Generate a least-privilege S3 access policy for the EC2 IAM Role.
data "aws_iam_policy_document" "s3_app_least" {

# 1) Allow listing objects within the bucket.
statement {
sid = "ListBucket"
effect = "Allow"
actions = ["s3:ListBucket"]
resources = [local.bucket_arn]

# If a prefix is specified, restrict the listing to that prefix only.
condition {
test = "StringLike"
variable = "s3:prefix"
values = [var.s3_prefix == "" ? "*" : "${var.s3_prefix}*"]
}
}

# 2) Allow reading, uploading, and deleting objects.
statement {
sid = "ObjectRW"
effect = "Allow"
actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
resources = [local.objects_arn]
}

# 3) Optional: Allow multipart upload operations for large files.
statement {
sid = "Multipart"
effect = "Allow"
actions = [
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts",
"s3:ListBucketMultipartUploads"
]
resources = [local.bucket_arn, local.objects_arn]
}
}

# Create the custom least-privilege S3 policy.
resource "aws_iam_policy" "s3_app_policy" {
name = "${var.name_prefix}-s3-app-policy"
policy = data.aws_iam_policy_document.s3_app_least.json
}

# This tells the IAM role what it is allowed to do.
# In this case, Attach the custom least-privilege S3 policy to the EC2 role.
resource "aws_iam_role_policy_attachment" "s3_access" {
role = aws_iam_role.app_ec2_role.name
policy_arn = aws_iam_policy.s3_app_policy.arn
}
9 changes: 9 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,12 @@ output "application_sg_id" {
output "instance_id" {
value = aws_instance.app.public_ip
}

output "rds_endpoint" {
description = "RDS endpoint hostname"
value = aws_db_instance.db.address
}

output "rds_port" {
value = aws_db_instance.db.port
}
3 changes: 1 addition & 2 deletions providers.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Configure the AWS Provider
provider "aws" {
region = var.region
profile = var.profile
region = var.region
}
52 changes: 52 additions & 0 deletions rds.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
resource "aws_db_parameter_group" "postgres" {
name = "${var.name_prefix}-pg-param"
family = var.db_engine_family
description = "Custom parameter group for PostgreSQL ${var.db_engine_version}"

parameter {
name = "log_min_duration_statement"
value = "500"
}
}

resource "aws_db_subnet_group" "db_private" {
name = "${var.name_prefix}-db-subnet-group"
subnet_ids = [for s in aws_subnet.private : s.id] # multiple subnet (at least 2)
tags = {
Name = "${var.name_prefix}-db-subnet-group"
}
}

# RDS Instance
resource "aws_db_instance" "db" {
identifier = "${var.name_prefix}-rds"
engine = "postgres"
engine_version = var.db_engine_version # e.g., "16.3"
instance_class = var.db_instance_class # e.g., "db.t3.micro"
allocated_storage = var.db_allocated_storage # e.g., 20
storage_type = "gp3"

db_name = var.db_name
username = var.db_username
password = var.db_password

port = var.db_port # 5432
multi_az = false
publicly_accessible = false

vpc_security_group_ids = [aws_security_group.db_sg.id]
db_subnet_group_name = aws_db_subnet_group.db_private.name
parameter_group_name = aws_db_parameter_group.postgres.name

skip_final_snapshot = true

tags = {
Name = "${var.name_prefix}-rds-postgres"
}

depends_on = [
aws_db_subnet_group.db_private,
aws_db_parameter_group.postgres,
aws_security_group.db_sg
]
}
43 changes: 43 additions & 0 deletions s3.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
resource "aws_s3_bucket" "images" {
bucket = "${var.name_prefix}-${random_uuid.s3_suffix.result}"
force_destroy = true

tags = {
Name = "${var.name_prefix}-s3"
}
}

resource "random_uuid" "s3_suffix" {}

resource "aws_s3_bucket_public_access_block" "images" {
bucket = aws_s3_bucket.images.id

block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "images" {
bucket = aws_s3_bucket.images.id

rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}

resource "aws_s3_bucket_lifecycle_configuration" "images" {
bucket = aws_s3_bucket.images.id

rule {
id = "transition-standard-to-ia"
status = "Enabled"

transition {
days = 30
storage_class = "STANDARD_IA"
}
}
}
34 changes: 34 additions & 0 deletions scripts/user_data.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash
set -euo pipefail

ENV_FILE="${app_dir}/.env"
LOG_FILE="/var/log/user_data_setup.log"
exec > >(tee -a "$LOG_FILE") 2>&1

### ====== Configure log color ======
log() { echo -e "\033[1;32m[OK]\033[0m $*"; }
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
err() { echo -e "\033[1;31m[ERR]\033[0m $*" >&2; }

info "=== Setting up environment for web app in ${app_dir} ==="

mkdir -p "${app_dir}" # make sure the directory exist

cat >> "$ENV_FILE" <<EOF
DB_HOST=${db_host}
DB_PORT=${db_port}
DB_NAME=${db_name}
DB_USERNAME=${db_username}
DB_PASSWORD=${db_password}
AWS_REGION=${aws_region}
S3_BUCKET=${s3_bucket}
EOF

chown "${app_user}:${app_group}" "$ENV_FILE" # Change owner of the .env file to normal user
chmod 600 "$ENV_FILE" # only rw for owner (not even root can see this file)

systemctl daemon-reload # let systemd reload all the .service file
systemctl enable "${service_name}.service"
systemctl restart "${service_name}.service"

info "=== Web App started successfully ==="
35 changes: 33 additions & 2 deletions security-group.tf
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#----------------------
# Web App Security Group
#----------------------
resource "aws_security_group" "app_sg" {
name = "${var.name_prefix}-sg"
name = "${var.name_prefix}-app-sg"
description = "Web App SG: 22,80,443,app open to world"
vpc_id = aws_vpc.csye6225.id

tags = { Name = "${var.name_prefix}-sg" }
tags = { Name = "${var.name_prefix}-app-sg" }
}

locals {
Expand Down Expand Up @@ -42,3 +45,31 @@ resource "aws_vpc_security_group_egress_rule" "all_out_ipv6" {
cidr_ipv6 = "::/0"
ip_protocol = "-1"
}

#----------------------
# DB Security Group
#----------------------
resource "aws_security_group" "db_sg" {
name = "${var.name_prefix}-db-sg"
description = "Database Security Group: only allow access from EC2 app_sg"
vpc_id = aws_vpc.csye6225.id

tags = { Name = "${var.name_prefix}-db-sg" }
}

# Allow inbound DB access from app_sg
resource "aws_vpc_security_group_ingress_rule" "db_ingress_app_sg" {
security_group_id = aws_security_group.db_sg.id # apply to what sg
referenced_security_group_id = aws_security_group.app_sg.id
from_port = var.db_port
to_port = var.db_port
ip_protocol = "tcp"
description = "Allow TCP ${var.db_port} from anywhere (IPv4)"
}

# Allow outbound (for updates / AWS services)
resource "aws_vpc_security_group_egress_rule" "db_all_out_ipv4" {
security_group_id = aws_security_group.db_sg.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
Loading