diff --git a/infrastructure/aws/README.md b/infrastructure/aws/README.md index c52d8c9..41a7fd9 100644 --- a/infrastructure/aws/README.md +++ b/infrastructure/aws/README.md @@ -11,6 +11,7 @@ | Name | Version | |------|---------| +| [archive](#provider\_archive) | n/a | | [aws](#provider\_aws) | 6.14.1 | | [infisical](#provider\_infisical) | n/a | @@ -22,10 +23,28 @@ No modules. | Name | Type | |------|------| +| [aws_api_gateway_deployment.branch_deployment](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_deployment) | resource | +| [aws_api_gateway_integration.lambda_integrations](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_integration) | resource | +| [aws_api_gateway_method.lambda_methods](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_method) | resource | +| [aws_api_gateway_resource.lambda_resources](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_resource) | resource | +| [aws_api_gateway_rest_api.branch_api](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_rest_api) | resource | +| [aws_api_gateway_stage.branch_stage](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/api_gateway_stage) | resource | | [aws_cognito_user_pool.branch_user_pool](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/cognito_user_pool) | resource | | [aws_cognito_user_pool_client.branch_client](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/cognito_user_pool_client) | resource | | [aws_db_instance.branch_rds](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/db_instance) | resource | +| [aws_iam_role.lambda_role](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.lambda_basic](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambda_function.functions](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/lambda_function) | resource | +| [aws_lambda_permission.api_gateway_permissions](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/lambda_permission) | resource | +| [aws_s3_bucket.lambda_deployments](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket) | resource | | [aws_s3_bucket.reports_bucket](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket) | resource | +| [aws_s3_bucket_policy.reports_bucket_policy](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_policy) | resource | +| [aws_s3_bucket_public_access_block.reports_bucket_public_access](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_public_access_block) | resource | +| [aws_s3_bucket_server_side_encryption_configuration.lambda_deployments](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | +| [aws_s3_bucket_versioning.lambda_deployments](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_bucket_versioning) | resource | +| [aws_s3_object.lambda_placeholder](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/resources/s3_object) | resource | +| [archive_file.lambda_placeholder](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/6.14.1/docs/data-sources/caller_identity) | data source | | [infisical_secrets.rds_folder](https://registry.terraform.io/providers/infisical/infisical/latest/docs/data-sources/secrets) | data source | ## Inputs @@ -40,9 +59,9 @@ No modules. | Name | Description | |------|-------------| -| [cognito\_client\_id](#output\_cognito\_client\_id) | Cognito User Pool Client ID | +| [api\_gateway\_url](#output\_api\_gateway\_url) | The URL of the API Gateway | | [cognito\_region](#output\_cognito\_region) | AWS Region for Cognito | | [cognito\_user\_pool\_arn](#output\_cognito\_user\_pool\_arn) | Cognito User Pool ARN | | [cognito\_user\_pool\_endpoint](#output\_cognito\_user\_pool\_endpoint) | Cognito User Pool Endpoint | -| [cognito\_user\_pool\_id](#output\_cognito\_user\_pool\_id) | Cognito User Pool ID | +| [reports\_bucket\_name](#output\_reports\_bucket\_name) | Name of the S3 bucket for generated reports | diff --git a/infrastructure/aws/api_gateway.tf b/infrastructure/aws/api_gateway.tf new file mode 100644 index 0000000..f333514 --- /dev/null +++ b/infrastructure/aws/api_gateway.tf @@ -0,0 +1,109 @@ +# API Gateway for Lambda functions +resource "aws_api_gateway_rest_api" "branch_api" { + name = "branch-api" + description = "API Gateway for Branch Lambda functions" + + endpoint_configuration { + types = ["REGIONAL"] + } +} + +# Define supported HTTP methods per Lambda function based on handlers +# NOTE: Must be kept in sync with actual Lambda handlers in apps/backend/lambdas/*/openapi.yaml +locals { + lambda_methods = { + auth = ["GET", "POST"] + donors = ["GET"] + expenditures = ["GET", "POST"] + projects = ["GET", "POST"] + reports = ["GET"] + users = ["GET", "POST", "DELETE", "PATCH"] + } +} + +# Create a resource for each Lambda function +resource "aws_api_gateway_resource" "lambda_resources" { + for_each = local.lambda_functions + + rest_api_id = aws_api_gateway_rest_api.branch_api.id + parent_id = aws_api_gateway_rest_api.branch_api.root_resource_id + path_part = each.key +} + +# Create methods for each resource based on supported methods +resource "aws_api_gateway_method" "lambda_methods" { + for_each = merge([ + for lambda, methods in local.lambda_methods : { + for method in methods : + "${lambda}-${method}" => { + lambda = lambda + method = method + } + } + ]...) + + rest_api_id = aws_api_gateway_rest_api.branch_api.id + resource_id = aws_api_gateway_resource.lambda_resources[each.value.lambda].id + http_method = each.value.method + authorization = "COGNITO_USER_POOLS" +} + +# Create Lambda integrations +resource "aws_api_gateway_integration" "lambda_integrations" { + for_each = merge([ + for lambda, methods in local.lambda_methods : { + for method in methods : + "${lambda}-${method}" => { + lambda = lambda + method = method + } + } + ]...) + + rest_api_id = aws_api_gateway_rest_api.branch_api.id + resource_id = aws_api_gateway_resource.lambda_resources[each.value.lambda].id + http_method = aws_api_gateway_method.lambda_methods[each.key].http_method + + integration_http_method = "POST" + type = "AWS_PROXY" + uri = aws_lambda_function.functions[each.value.lambda].invoke_arn +} + +# Allow API Gateway to invoke Lambda functions +resource "aws_lambda_permission" "api_gateway_permissions" { + for_each = local.lambda_functions + + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.functions[each.key].function_name + principal = "apigateway.amazonaws.com" + + # Grant permission for the specific API Gateway and stage + source_arn = "${aws_api_gateway_rest_api.branch_api.execution_arn}/*/*" +} + +# Create deployment +resource "aws_api_gateway_deployment" "branch_deployment" { + depends_on = [ + aws_api_gateway_integration.lambda_integrations + ] + + rest_api_id = aws_api_gateway_rest_api.branch_api.id + + lifecycle { + create_before_destroy = true + } +} + +# Create stage +resource "aws_api_gateway_stage" "branch_stage" { + deployment_id = aws_api_gateway_deployment.branch_deployment.id + rest_api_id = aws_api_gateway_rest_api.branch_api.id + stage_name = "prod" +} + +# Output the API Gateway URL +output "api_gateway_url" { + description = "The URL of the API Gateway" + value = aws_api_gateway_stage.branch_stage.invoke_url +} \ No newline at end of file diff --git a/infrastructure/aws/lambda.tf b/infrastructure/aws/lambda.tf new file mode 100644 index 0000000..4aa36d0 --- /dev/null +++ b/infrastructure/aws/lambda.tf @@ -0,0 +1,108 @@ +# IAM role for Lambda functions +resource "aws_iam_role" "lambda_role" { + name = "branch-lambda-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { Service = "lambda.amazonaws.com" } + }] + }) +} + +# Attach basic execution policy for CloudWatch Logs +resource "aws_iam_role_policy_attachment" "lambda_basic" { + role = aws_iam_role.lambda_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +# Get AWS account ID for unique bucket naming +data "aws_caller_identity" "current" {} + +resource "aws_s3_bucket" "lambda_deployments" { + bucket = "branch-lambda-deployments-${data.aws_caller_identity.current.account_id}" +} + +resource "aws_s3_bucket_versioning" "lambda_deployments" { + bucket = aws_s3_bucket.lambda_deployments.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "lambda_deployments" { + bucket = aws_s3_bucket.lambda_deployments.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +# Define all Lambda functions +locals { + lambda_functions = toset([ + "auth", + "donors", + "expenditures", + "projects", + "reports", + "users", + ]) +} + +# Minimal placeholder that will be replaced by GitHub Actions on first deployment +data "archive_file" "lambda_placeholder" { + type = "zip" + output_path = "${path.module}/lambda-placeholder.zip" + source { + content = "exports.handler = async () => ({ statusCode: 200, body: JSON.stringify({ message: 'Placeholder - will be replaced by CI/CD' }) });" + filename = "handler.js" + } +} + +# This allows Terraform to create the Lambda functions initially +resource "aws_s3_object" "lambda_placeholder" { + for_each = local.lambda_functions + + bucket = aws_s3_bucket.lambda_deployments.id + key = "${each.key}/initial.zip" + source = data.archive_file.lambda_placeholder.output_path + + content_type = "application/zip" +} + +# Create all Lambda functions with a single resource block +resource "aws_lambda_function" "functions" { + for_each = local.lambda_functions + + function_name = "branch-${each.key}" + runtime = "nodejs20.x" + handler = "handler.handler" + timeout = 30 + memory_size = 256 + role = aws_iam_role.lambda_role.arn + + # Use S3 for deployment (initial placeholder, replaced by GitHub Actions) + s3_bucket = aws_s3_bucket.lambda_deployments.id + s3_key = aws_s3_object.lambda_placeholder[each.key].key + + # Prevent Terraform from reverting code deployments made by GitHub Actions + lifecycle { + ignore_changes = [s3_key] + } + + environment { + variables = { + NODE_ENV = "production" + DB_HOST = aws_db_instance.branch_rds.address + DB_USER = data.infisical_secrets.rds_folder.secrets["username"].value + DB_PASSWORD = data.infisical_secrets.rds_folder.secrets["password"].value + DB_PORT = try(data.infisical_secrets.rds_folder.secrets["db_port"].value, "5432") + DB_NAME = try(data.infisical_secrets.rds_folder.secrets["db_name"].value, aws_db_instance.branch_rds.db_name) + } + } +} \ No newline at end of file