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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,8 @@ override.tf.json
# Ignore CLI configuration files
.terraformrc
terraform.rc

# ---------------
# lambda build output
# ---------------
dist/
52 changes: 36 additions & 16 deletions .terraform.lock.hcl

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

24 changes: 24 additions & 0 deletions AWS_TF_permission.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,30 @@
"arn:aws:s3:::awsserverlessrepo-changesets*",
"arn:aws:s3:::secrets-manager-rotation-apps-*/*"
]
},
{
"Sid": "FullKMSAccess",
"Effect": "Allow",
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "FullDynamoDBAccess",
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "*"
},
{
"Sid": "FullLambdaAccess",
"Effect": "Allow",
"Action": "lambda:*",
"Resource": "*"
},
{
"Sid": "FullACMAccess",
"Effect": "Allow",
"Action": "acm:*",
"Resource": "*"
}
]
}
11 changes: 11 additions & 0 deletions dynamodb.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
resource "aws_dynamodb_table" "sent_emails" {
name = "SentEmails"
billing_mode = "PAY_PER_REQUEST"
hash_key = "messageId"

# defines which attributes exist and their types.
attribute {
name = "messageId"
type = "S"
}
}
79 changes: 17 additions & 62 deletions ec2.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,66 +3,6 @@ locals {
for az, s in aws_subnet.public :
az => s.id
}

az_to_private_subnet_id = {
for az, s in aws_subnet.private :
az => s.id
}

default_az = sort(keys(local.az_to_public_subnet_id))[0]
chosen_az = coalesce(var.target_az, local.default_az)

chosen_subnet_id = (var.subnet_tier == "public"
? lookup(local.az_to_public_subnet_id, local.chosen_az, null)
: lookup(local.az_to_private_subnet_id, local.chosen_az, null)
)
}

# Only if you want to create an isolated EC2 not part of ASG to use this
resource "aws_instance" "app" {
count = var.create_ec2_instance ? 1 : 0
ami = var.ami_id
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

# Do NOT protect from accidental termination
disable_api_termination = false

# Root volume requirements
root_block_device {
volume_type = "gp2"
volume_size = 25
delete_on_termination = true
}

# 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 = random_password.rds.result

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

tags = {
Name = "${var.name_prefix}-ec2"
Role = "webapp"
}
}

# ------------------
Expand All @@ -86,6 +26,20 @@ resource "aws_launch_template" "app" {
associate_public_ip_address = true
}

# Encrypt the root EBS volume with KMS key
# block_device_mappings {
# device_name = "/dev/xvda" # Root volume device name (varies by AMI)
#
# ebs {
# volume_size = 20 # or var.root_volume_size if you have one
# volume_type = "gp3"
# encrypted = true
#
# # Use the customer-managed KMS key dedicated for EC2 EBS encryption.
# kms_key_id = aws_kms_alias.ec2_key_alias.arn
# }
# }

user_data = base64encode(templatefile("${path.module}/scripts/user_data.sh", {
app_user = var.app_user
app_group = var.app_group
Expand All @@ -98,8 +52,9 @@ resource "aws_launch_template" "app" {
db_username = var.db_username
db_password = random_password.rds.result

aws_region = var.region
s3_bucket = aws_s3_bucket.images.bucket
aws_region = var.region
s3_bucket = aws_s3_bucket.images.bucket
sns_topic_arn = aws_sns_topic.user_signup.arn
}))

tag_specifications {
Expand Down
123 changes: 123 additions & 0 deletions iam_role.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ resource "aws_iam_role" "app_ec2_role" {
}]
})
}

# 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.
Expand Down Expand Up @@ -100,3 +101,125 @@ resource "aws_iam_role_policy_attachment" "ssm_core" {
role = aws_iam_role.app_ec2_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

# ------------------------
# EC2 publish to SNS Topic
# ------------------------

# policy for EC2 can publish msg to subscribed SND Topic
resource "aws_iam_role_policy" "app_publish_sns" {
name = "app-publish-sns"
role = aws_iam_role.app_ec2_role.name

policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Action = ["sns:Publish"],
Resource = aws_sns_topic.user_signup.arn
}]
})
}

# -------------------
# Email Lambda Role
# -------------------

# Assume role
resource "aws_iam_role" "lambda_email_role" {
name = "lambda-email-sender-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Principal = { Service = "lambda.amazonaws.com" },
Action = "sts:AssumeRole"
}]
})
}

# Get current AWS account identity (used to build ARNs)
data "aws_caller_identity" "me" {}

# Logs
# Basic logging permissions for the Lambda function.
# Allows the function to create log groups/streams and send log events to CloudWatch Logs.
resource "aws_iam_role_policy" "lambda_logs" {
name = "lambda-basic-logs"
role = aws_iam_role.lambda_email_role.id

# Lambda will automatically create a log group in CloudWatch e.g. /aws/lambda/<function-name>
policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
Resource = "arn:aws:logs:${var.region}:${data.aws_caller_identity.me.account_id}:log-group:/aws/lambda/*"
}]
})
}

# Secrets
# Allow the Lambda function to read the Mailgun API key
# stored in AWS Secrets Manager.
resource "aws_iam_role_policy" "lambda_secrets_read" {
name = "lambda-secrets-read"
role = aws_iam_role.lambda_email_role.id

policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Action = ["secretsmanager:GetSecretValue"],
Resource = aws_secretsmanager_secret.mailgun.arn
}]
})
}

# KMS decrypt (for that secret's CMK)
resource "aws_iam_role_policy" "lambda_kms_decrypt" {
name = "lambda-kms-decrypt"
role = aws_iam_role.lambda_email_role.id

policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Action = ["kms:Decrypt"],
Resource = aws_kms_alias.secrets_key_alias.arn
}]
})
}

# SES send
resource "aws_iam_role_policy" "lambda_ses_send" {
name = "lambda-ses-send"
role = aws_iam_role.lambda_email_role.id

policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Action = ["ses:SendEmail", "ses:SendRawEmail"],
Resource = "*"
}]
})
}

# Allow lambda to r/w items in the DynamoDB table
# to perform deduplication
resource "aws_iam_role_policy" "lambda_dedup_policy" {
name = "lambda-dedup-policy"
role = aws_iam_role.lambda_email_role.id

policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = ["dynamodb:PutItem", "dynamodb:GetItem"],
Resource = aws_dynamodb_table.sent_emails.arn
}
]
})
}
Loading