diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 4bd4bea..b647b64 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -60,5 +60,5 @@ jobs: --no-confirm-changeset --no-fail-on-empty-changeset --stack-name fastapi-backend-lambda - --parameter-overrides "SecretKeyArn=${{ vars.SECRET_ARN }} DBSecretArn=${{ vars.DB_SECRET_ARN }} DBEndpoint=${{ vars.DB_ENDPOINT }}" + --parameter-overrides "LambdaSecurityGroupId=${{ vars.LAMBDA_SG_ID }} LambdaSubnet1Id=${{ vars.LAMBDA_SUBNET_1_ID }} LambdaSubnet2Id=${{ vars.LAMBDA_SUBNET_2_ID }} SecretKeyArn=${{ vars.SECRET_ARN }} DBSecretArn=${{ vars.DB_SECRET_ARN }} DBEndpoint=${{ vars.DB_ENDPOINT }}" --on-failure DELETE diff --git a/backend/template.yaml b/backend/template.yaml index 3ddaa57..782d103 100644 --- a/backend/template.yaml +++ b/backend/template.yaml @@ -18,6 +18,18 @@ Parameters: Type: String Default: example Description: ARN for the secret in SM + LambdaSecurityGroupId: + Type: String + Default: example + Description: Security group id for the lambda + LambdaSubnet1Id: + Type: String + Default: example + Description: Lambda subnet 1 id + LambdaSubnet2Id: + Type: String + Default: example + Description: Lambda subnet 2 id DBSecretArn: Type: String Default: example @@ -48,6 +60,12 @@ Resources: DB_ENDPOINT: !Ref DBEndpoint DB_NAME: !Ref DBName Role: !GetAtt FastApiBackendRole.Arn + VpcConfig: + SecurityGroupIds: + - !Ref LambdaVpcSecurityGroupId + SubnetIds: + - !Ref LambdaSubnet1Id + - !Ref LambdaSubnet2Id Events: HttpApiEvent: Type: HttpApi diff --git a/terraform/main.tf b/terraform/main.tf index bb65f07..5f2e7de 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -21,33 +21,58 @@ provider "aws" { region = var.aws_region } -resource "aws_ecr_repository" "ecr_repository" { - name = "fastapi-lambda-ecr-repo" - force_delete = true +data "aws_caller_identity" "current" {} + +locals { + account_id = data.aws_caller_identity.current.account_id } -resource "aws_ecr_lifecycle_policy" "ecr_repository_policy" { - repository = aws_ecr_repository.ecr_repository.name +################################################################################ +# VPC +################################################################################ - policy = jsonencode({ - rules = [ - { - rulePriority = 1, - description = "Keep only the last 5 images", - selection = { - tagStatus = "untagged", - countType = "imageCountMoreThan", - countNumber = 5, - }, - action = { - type = "expire" - } - } - ] - }) +resource "aws_vpc" "backend_vpc" { + cidr_block = var.vpc_cidr +} + +resource "aws_subnet" "db_subnet" { + count = length(var.db_cidr) + vpc_id = aws_vpc.backend_vpc.id + cidr_block = element(var.db_cidr, count.index) + availability_zone = element(var.db_azs, count.index) + tags = { + Name = "db-subnet-${count.index + 1}" + } +} + +resource "aws_subnet" "lambda_subnet" { + count = length(var.lambda_cidr) + vpc_id = aws_vpc.backend_vpc.id + cidr_block = element(var.lambda_cidr, count.index) + availability_zone = element(var.lambda_azs, count.index) + tags = { + Name = "lambda-subnet-${count.index + 1}" + } +} + +resource "aws_security_group" "db_sg" { + vpc_id = aws_vpc.backend_vpc.id } -resource "aws_db_instance" "fastapi-db" { +resource "aws_security_group" "lambda_sg" { + vpc_id = aws_vpc.backend_vpc.id +} + +################################################################################ +# RDS +################################################################################ + +resource "aws_db_subnet_group" "db_subnet_group" { + name = "FastAPIDBSubnetGroup" + subnet_ids = [aws_subnet.db_subnet[0].id, aws_subnet.db_subnet[1].id] +} + +resource "aws_db_instance" "fastapi_db" { allocated_storage = 5 db_name = "fastapidb" engine = "postgres" @@ -58,4 +83,102 @@ resource "aws_db_instance" "fastapi-db" { parameter_group_name = "default.postgres15" skip_final_snapshot = true # final_snapshot_identifier = "final-snapshot" + + vpc_security_group_ids = [aws_security_group.db_sg.id] + db_subnet_group_name = aws_db_subnet_group.db_subnet_group.id + + multi_az = true # Enable multi-AZ deployment for high availability +} + +################################################################################ +# RDS Proxy +################################################################################ + +resource "random_password" "db_proxy_password" { + length = 24 +} + +resource "aws_secretsmanager_secret" "db_proxy_secret" { + name = "DBProxySecret" +} + +resource "aws_secretsmanager_secret_version" "db_version" { + secret_id = aws_secretsmanager_secret.db_proxy_secret.id + # https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/rds-proxy-setup.html#rds-proxy-secrets-arns + secret_string = jsonencode({ + "username" = var.lambda_db_username + "password" = random_password.db_proxy_password.result + }) +} + +resource "aws_iam_role" "db_proxy_role" { + name = "DBProxyRole" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Sid = "" + Principal = { + Service = "rds.amazonaws.com" + } + }] + }) +} + +resource "aws_iam_policy" "db_proxy_policy" { + name = "DBProxyPolicy" + + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Action = "secretsmanager:GetSecretValue", + Effect = "Allow", + Resource = aws_secretsmanager_secret.db_proxy_secret.arn + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "db_proxy_role_policy_attachment" { + policy_arn = aws_iam_policy.db_proxy_policy.arn + role = aws_iam_role.db_proxy_role.name +} + +resource "aws_db_proxy" "db_proxy" { + name = "DBProxy" + debug_logging = true + idle_client_timeout = 1800 + require_tls = true + role_arn = aws_iam_role.db_proxy_role.arn + engine_family = "POSTGRESQL" + + vpc_security_group_ids = [aws_security_group.db_sg.id] + vpc_subnet_ids = [aws_subnet.db_subnet.id] + + auth { + auth_scheme = "SECRETS" + description = "using secret manager" + iam_auth = "DISABLED" + secret_arn = aws_secretsmanager_secret.db_proxy_secret.arn + } + + depends_on = [aws_cloudwatch_log_group.db_proxy_log_group] +} + +resource "aws_db_proxy_default_target_group" "default" { + db_proxy_name = aws_db_proxy.db_proxy.name +} + +resource "aws_db_proxy_target" "example" { + db_instance_identifier = aws_db_instance.fastapi_db.identifier + db_proxy_name = aws_db_proxy.db_proxy.name + target_group_name = aws_db_proxy_default_target_group.default.name +} + +resource "aws_cloudwatch_log_group" "db_proxy_log_group" { + name = "/aws/rds/proxy/fastapidb" + retention_in_days = 30 } diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 831ed7c..8f03fd6 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -1,11 +1,28 @@ -output "ecr_repository_url" { - value = aws_ecr_repository.ecr_repository.repository_url +output "lambda_security_group" { + description = "security group to be assigned to lambda function" + value = aws_security_group.lambda_sg.id } -output "db_endpoint" { - value = aws_db_instance.fastapi-db.endpoint +output "lambda_subnet_1" { + value = aws_subnet.lambda_subnet[0].id } -output "db_master_user_secret" { - value = aws_db_instance.fastapi-db.master_user_secret -} \ No newline at end of file +output "lambda_subnet_2" { + value = aws_subnet.lambda_subnet[1].id +} + +output "lambda_db_username" { + value = var.lambda_db_username +} + +output "rds_db_name" { + value = aws_db_instance.fastapi_db.db_name +} + +output "rds_proxy_endpoint" { + value = aws_db_proxy.db_proxy.endpoint +} + +output "rds_proxy_secret_arn" { + value = aws_secretsmanager_secret.db_proxy_secret.arn +} diff --git a/terraform/variables.tf b/terraform/variables.tf index 35c9dfd..99bcf68 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -2,4 +2,36 @@ variable "aws_region" { type = string default = "ap-southeast-2" description = "AWS region for all resources" -} \ No newline at end of file +} + +variable "vpc_cidr" { + description = "CIDR block for the vpc" + type = string + default = "10.0.0.0/16" +} + +variable "db_cidr" { + type = list + default = ["10.0.11.0/24", "10.0.12.0/24"] +} + +variable "lambda_cidr" { + type = list + default = ["10.0.21.0/24", "10.0.22.0/24"] +} + +variable "db_azs" { + type = list + default = ["ap-southeast-2a", "ap-southeast-2b"] +} + +variable "lambda_azs" { + type = list + default = ["ap-southeast-2a", "ap-southeast-2b"] +} + +variable "lambda_db_username" { + type = string + default = "app_db_username" + description = "DB username for the app" # Needs to be created manually: https://github.com/aws-samples/serverless-patterns/blob/main/apigw-http-api-lambda-rds-proxy-terraform/vpc-rds-setup/main.tf#L236-L239 +}