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: 14 additions & 5 deletions terraform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,21 @@ All components live inside a private VPC (no NAT gateway, no internet gateway).

### IAM (`main.tf`)

- **Lambda IAM Role** (`aws_iam_role.lambda_role`): Shared by all Lambda functions (TTC, index, and augmentation). Attached policies:
Each Lambda function has its own IAM role scoped to least-privilege S3 permissions:

- **TTC Lambda IAM Role** (`aws_iam_role.ttc_lambda_role`): Attached policies:
- `AWSLambdaVPCAccessExecutionRole` — allows ENI creation for VPC placement
- `AWSLambdaBasicExecutionRole` — allows CloudWatch Logs writes
- Inline S3 policy — `s3:GetObject`/`s3:HeadObject` on `eCRMessageV2/` and `ValidationResponseV2/` prefixes; `s3:PutObject` on `TTCAugmentationMetadataV2/` and `TTCMetadataV2/` prefixes
- Inline OpenSearch policy — grants OpenSearch HTTP actions (`ESHttpGet/Post/Put/Delete/Head/Patch/Options`)
- **Index Lambda IAM Role** (`aws_iam_role.index_lambda_role`): Attached policies:
- `AWSLambdaVPCAccessExecutionRole` — allows ENI creation for VPC placement
- `AWSLambdaBasicExecutionRole` — allows CloudWatch Logs writes
- Inline OpenSearch policy — grants OpenSearch HTTP actions (no S3 access needed)
- **Augmentation Lambda IAM Role** (`aws_iam_role.augmentation_lambda_role`): Attached policies:
- `AWSLambdaVPCAccessExecutionRole` — allows ENI creation for VPC placement
- `AWSLambdaBasicExecutionRole` — allows CloudWatch Logs writes
- `AmazonS3FullAccess` — S3 read/write (TODO: scope down to specific bucket/prefix)
- Inline policy — grants OpenSearch HTTP actions (`ESHttpGet/Post/Put/Delete/Head/Patch/Options`)
- Inline S3 policy — `s3:PutObject` on `AugmentationEICRV2/` and `AugmentationMetadataV2/` prefixes (no OpenSearch access needed)
- **Ingestion Pipeline IAM Role** (`aws_iam_role.os_ingestion_pipeline_role`): Assumed by the OSIS pipeline service. Grants S3 `ListBucket`/`GetObject` on the data bucket and full OpenSearch HTTP access on the domain.

### Lambda Functions (`main.tf`, `lambda/`)
Expand Down Expand Up @@ -110,7 +120,7 @@ Terraform manages dependency ordering automatically, but conceptually the sequen
2. ECR repositories created (TTC lambda, index lambda, augmentation lambda)
3. Docker images built and pushed to ECR (in CI/CD, before full `terraform apply`)
4. OpenSearch domain and VPC endpoint created
5. Lambda IAM role created
5. Lambda IAM roles created (one per Lambda function)
6. Index bootstrap Lambda deployed and **immediately invoked** — creates the KNN index in OpenSearch
7. Ingestion pipeline deployed — begins polling S3 for NDJSON embeddings to load
8. Main TTC Lambda deployed with container image from ECR — loads model at cold start, ready to serve KNN queries
Expand Down Expand Up @@ -158,6 +168,5 @@ Before running `terraform apply`:
## Known TODOs

- OpenSearch error logs should be sent to CloudWatch Logs (noted in `main.tf`)
- S3 IAM policy should be scoped down to the specific bucket and prefix instead of `AmazonS3FullAccess`
- Polling frequency for the OSIS pipeline is set to monthly since LOINC updates infrequently, but can be adjusted as needed
- The `/ingestion/` prefix in the `dibbs-text-to-code` S3 bucket should be created as part of Terraform rather than manually
16 changes: 13 additions & 3 deletions terraform/_outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@ output "lambda_function_name" {
description = "The name of the main TTC lambda function"
}

output "lambda_role_arn" {
value = aws_iam_role.lambda_role.arn
description = "The ARN of the IAM role attached to the main and index TTC lambda functions"
output "ttc_lambda_role_arn" {
value = aws_iam_role.ttc_lambda_role.arn
description = "The ARN of the IAM role attached to the TTC lambda function"
}

output "index_lambda_role_arn" {
value = aws_iam_role.index_lambda_role.arn
description = "The ARN of the IAM role attached to the index lambda function"
}

output "augmentation_lambda_role_arn" {
value = aws_iam_role.augmentation_lambda_role.arn
description = "The ARN of the IAM role attached to the augmentation lambda function"
}

output "opensearch_vpc_endpoint" {
Expand Down
128 changes: 111 additions & 17 deletions terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ data "aws_iam_policy_document" "opensearch_access_policy" {
effect = "Allow"
principals {
type = "AWS"
identifiers = [aws_iam_role.lambda_role.arn, aws_iam_role.os_ingestion_pipeline_role.arn, data.aws_caller_identity.current.arn]
identifiers = [aws_iam_role.ttc_lambda_role.arn, aws_iam_role.index_lambda_role.arn, aws_iam_role.os_ingestion_pipeline_role.arn, data.aws_caller_identity.current.arn]
}
actions = var.lambda_os_actions
resources = ["arn:aws:es:${var.region}:${data.aws_caller_identity.current.account_id}:domain/${var.opensearch_domain_name}/*"]
Expand Down Expand Up @@ -249,7 +249,7 @@ resource "aws_cloudwatch_log_resource_policy" "opensearch_log_publishing" {
}

#############
# IAM Role for Lambda
# IAM Roles for Lambda Functions
#############
data "aws_iam_policy_document" "lambda_assume_role" {
statement {
Expand All @@ -262,31 +262,88 @@ data "aws_iam_policy_document" "lambda_assume_role" {
}
}

resource "aws_iam_role" "lambda_role" {
# TTC Lambda Role
resource "aws_iam_role" "ttc_lambda_role" {
name = "ttc-lambda-role"
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
tags = { Name = "ttc-lambda-role" }
}

resource "aws_iam_role_policy_attachment" "vpc_access" {
role = aws_iam_role.lambda_role.name
resource "aws_iam_role_policy_attachment" "ttc_vpc_access" {
role = aws_iam_role.ttc_lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}

#TODO: Limit S3 access to specific bucket and prefix
resource "aws_iam_role_policy_attachment" "s3_access" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
resource "aws_iam_role_policy_attachment" "ttc_cloudwatch_logs" {
role = aws_iam_role.ttc_lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "ttc_lambda_s3_policy" {
name = "ttc-lambda-s3-inline-policy"
role = aws_iam_role.ttc_lambda_role.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowS3Read"
Effect = "Allow"
Action = ["s3:GetObject", "s3:HeadObject"]
Resource = [
"arn:aws:s3:::${var.s3_bucket}/${var.eicr_input_prefix}*",
"arn:aws:s3:::${var.s3_bucket}/${var.schematron_error_prefix}*"
]
},
{
Sid = "AllowS3Write"
Effect = "Allow"
Action = ["s3:PutObject"]
Resource = [
"arn:aws:s3:::${var.s3_bucket}/${var.ttc_output_prefix}*",
"arn:aws:s3:::${var.s3_bucket}/${var.ttc_metadata_prefix}*"
]
}
]
})
}

resource "aws_iam_role_policy" "ttc_lambda_opensearch_policy" {
name = "ttc-lambda-opensearch-inline-policy"
role = aws_iam_role.ttc_lambda_role.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = var.lambda_os_actions
Resource = "arn:aws:es:${var.region}:${data.aws_caller_identity.current.account_id}:domain/${var.opensearch_domain_name}/*"
}
]
})
}

resource "aws_iam_role_policy_attachment" "cloudwatch_logs" {
role = aws_iam_role.lambda_role.name
# Index Lambda Role
resource "aws_iam_role" "index_lambda_role" {
name = "ttc-index-lambda-role"
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
tags = { Name = "ttc-index-lambda-role" }
}

resource "aws_iam_role_policy_attachment" "index_vpc_access" {
role = aws_iam_role.index_lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}

resource "aws_iam_role_policy_attachment" "index_cloudwatch_logs" {
role = aws_iam_role.index_lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "lambda_opensearch_policy" {
name = "lambda-opensearch-inline-policy"
role = aws_iam_role.lambda_role.id
resource "aws_iam_role_policy" "index_lambda_opensearch_policy" {
name = "index-lambda-opensearch-inline-policy"
role = aws_iam_role.index_lambda_role.id

policy = jsonencode({
Version = "2012-10-17"
Expand All @@ -300,13 +357,50 @@ resource "aws_iam_role_policy" "lambda_opensearch_policy" {
})
}

# Augmentation Lambda Role
resource "aws_iam_role" "augmentation_lambda_role" {
name = "ttc-augmentation-lambda-role"
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
tags = { Name = "ttc-augmentation-lambda-role" }
}

resource "aws_iam_role_policy_attachment" "augmentation_vpc_access" {
role = aws_iam_role.augmentation_lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}

resource "aws_iam_role_policy_attachment" "augmentation_cloudwatch_logs" {
role = aws_iam_role.augmentation_lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "augmentation_lambda_s3_policy" {
name = "augmentation-lambda-s3-inline-policy"
role = aws_iam_role.augmentation_lambda_role.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowS3Write"
Effect = "Allow"
Action = ["s3:PutObject"]
Resource = [
"arn:aws:s3:::${var.s3_bucket}/${var.augmented_eicr_prefix}*",
"arn:aws:s3:::${var.s3_bucket}/${var.augmentation_metadata_prefix}*"
]
}
]
})
}

#############
# Lambda Function
#############

resource "aws_lambda_function" "lambda" {
function_name = var.lambda_function_name
role = aws_iam_role.lambda_role.arn
role = aws_iam_role.ttc_lambda_role.arn
package_type = "Image"
image_uri = "${aws_ecr_repository.ttc_lambda.repository_url}:${var.ttc_lambda_image_tag}"
timeout = var.lambda_timeout
Expand Down Expand Up @@ -520,7 +614,7 @@ resource "aws_lambda_invocation" "index_bootstrap" {

resource "aws_lambda_function" "index_lambda" {
function_name = var.index_lambda_function_name
role = aws_iam_role.lambda_role.arn
role = aws_iam_role.index_lambda_role.arn
package_type = "Image"
image_uri = "${aws_ecr_repository.index_lambda.repository_url}:${var.index_lambda_image_tag}"
timeout = var.lambda_timeout
Expand Down Expand Up @@ -548,7 +642,7 @@ resource "aws_lambda_function" "index_lambda" {

resource "aws_lambda_function" "augmentation_lambda" {
function_name = var.augmentation_lambda_function_name
role = aws_iam_role.lambda_role.arn
role = aws_iam_role.augmentation_lambda_role.arn
package_type = "Image"
image_uri = "${aws_ecr_repository.augmentation_lambda.repository_url}:${var.augmentation_lambda_image_tag}"
timeout = var.augmentation_lambda_timeout
Expand Down
Loading