Skip to content

Commit 61f2c7a

Browse files
committed
VED-981 Add base infrastructure for the MNS Publisher feature (#1191)
1 parent 1acee0b commit 61f2c7a

File tree

15 files changed

+637
-1
lines changed

15 files changed

+637
-1
lines changed

.github/workflows/quality-checks.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,17 @@ jobs:
174174
poetry run coverage run --source=src -m unittest discover || echo "mesh_processor tests failed" >> ../../failed_tests.txt
175175
poetry run coverage xml -o ../../mesh_processor-coverage.xml
176176
177+
- name: Run unittest with mns_publisher
178+
working-directory: lambdas/mns_publisher
179+
id: mnspublisher
180+
env:
181+
PYTHONPATH: ${{ env.LAMBDA_PATH }}/mns_publisher/src:${{ env.LAMBDA_PATH }}/mns_publisher/tests:${{ env.SHARED_PATH }}/src
182+
continue-on-error: true
183+
run: |
184+
poetry install
185+
poetry run coverage run --source=src -m unittest discover || echo "mns_publisher tests failed" >> ../../failed_tests.txt
186+
poetry run coverage xml -o ../../mns_publisher-coverage.xml
187+
177188
- name: Run unittest with coverage-mns-subscription
178189
working-directory: lambdas/mns_subscription
179190
id: mns_subscription

infrastructure/instance/dynamodb.tf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ resource "aws_dynamodb_table" "delta-dynamodb-table" {
5656
name = "imms-${local.resource_scope}-delta"
5757
billing_mode = "PAY_PER_REQUEST"
5858
hash_key = "PK"
59+
stream_enabled = true
60+
stream_view_type = "NEW_IMAGE"
5961
deletion_protection_enabled = !local.is_temp
6062

6163
attribute {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# IAM Role for EventBridge Pipe
2+
resource "aws_iam_role" "mns_outbound_events_eb_pipe" {
3+
name = "${local.resource_scope}-mns-outbound-eventbridge-pipe-role"
4+
assume_role_policy = jsonencode({
5+
Version = "2012-10-17"
6+
Statement = [
7+
{
8+
Action = "sts:AssumeRole"
9+
Effect = "Allow"
10+
Principal = {
11+
Service = "pipes.amazonaws.com"
12+
}
13+
Condition = {
14+
StringEquals = {
15+
"aws:SourceAccount" = var.immunisation_account_id
16+
}
17+
}
18+
}
19+
]
20+
})
21+
}
22+
23+
resource "aws_iam_role_policy" "mns_outbound_events_eb_pipe_source_policy" {
24+
role = aws_iam_role.mns_outbound_events_eb_pipe.id
25+
policy = jsonencode({
26+
Version = "2012-10-17"
27+
Statement = [
28+
{
29+
"Effect" : "Allow",
30+
"Action" : [
31+
"dynamodb:DescribeStream",
32+
"dynamodb:GetRecords",
33+
"dynamodb:GetShardIterator",
34+
"dynamodb:ListStreams"
35+
],
36+
"Resource" : aws_dynamodb_table.delta-dynamodb-table.stream_arn
37+
},
38+
{
39+
"Effect" : "Allow",
40+
"Action" : [
41+
"kms:Decrypt",
42+
"kms:GenerateDataKey"
43+
],
44+
"Resource" : data.aws_kms_key.existing_dynamo_encryption_key.arn
45+
},
46+
]
47+
})
48+
}
49+
50+
resource "aws_iam_role_policy" "mns_outbound_events_eb_pipe_target_policy" {
51+
role = aws_iam_role.mns_outbound_events_eb_pipe.id
52+
policy = jsonencode({
53+
Version = "2012-10-17"
54+
Statement = [
55+
{
56+
Effect = "Allow"
57+
Action = [
58+
"sqs:GetQueueAttributes",
59+
"sqs:SendMessage",
60+
],
61+
Resource = [
62+
aws_sqs_queue.mns_outbound_events.arn,
63+
]
64+
},
65+
]
66+
})
67+
}
68+
69+
resource "aws_iam_role_policy" "mns_outbound_events_eb_pipe_cw_log_policy" {
70+
role = aws_iam_role.mns_outbound_events_eb_pipe.id
71+
policy = jsonencode({
72+
Version = "2012-10-17"
73+
Statement = [
74+
{
75+
Effect = "Allow"
76+
Action = [
77+
"logs:CreateLogGroup",
78+
"logs:CreateLogStream",
79+
"logs:PutLogEvents"
80+
],
81+
Resource = [
82+
"arn:aws:logs:${var.aws_region}:${var.immunisation_account_id}:log-group:/aws/vendedlogs/pipes/${local.resource_scope}-mns-outbound-event-pipe-logs:*",
83+
]
84+
},
85+
]
86+
})
87+
}
88+
89+
resource "aws_cloudwatch_log_group" "mns_outbound_events_eb_pipe" {
90+
name = "/aws/vendedlogs/pipes/${local.resource_scope}-mns-outbound-event-pipe-logs"
91+
retention_in_days = 30
92+
}
93+
94+
resource "aws_pipes_pipe" "mns_outbound_events" {
95+
depends_on = [
96+
aws_iam_role_policy.mns_outbound_events_eb_pipe_source_policy,
97+
aws_iam_role_policy.mns_outbound_events_eb_pipe_target_policy,
98+
aws_iam_role_policy.mns_outbound_events_eb_pipe_cw_log_policy,
99+
]
100+
name = "${local.resource_scope}-mns-outbound-events"
101+
role_arn = aws_iam_role.mns_outbound_events_eb_pipe.arn
102+
source = aws_dynamodb_table.delta-dynamodb-table.stream_arn
103+
target = aws_sqs_queue.mns_outbound_events.arn
104+
105+
source_parameters {
106+
dynamodb_stream_parameters {
107+
starting_position = "TRIM_HORIZON"
108+
}
109+
}
110+
111+
log_configuration {
112+
include_execution_data = ["ALL"]
113+
level = "ERROR"
114+
cloudwatch_logs_log_destination {
115+
log_group_arn = aws_cloudwatch_log_group.pipe_log_group.arn
116+
}
117+
}
118+
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
locals {
2+
mns_publisher_lambda_dir = abspath("${path.root}/../../lambdas/mns_publisher")
3+
mns_publisher_lambda_files = fileset(local.mns_publisher_lambda_dir, "**")
4+
mns_publisher_lambda_dir_sha = sha1(join("", [for f in local.mns_publisher_lambda_files : filesha1("${local.mns_publisher_lambda_dir}/${f}")]))
5+
mns_publisher_lambda_name = "${local.short_prefix}-mns-publisher-lambda"
6+
}
7+
8+
resource "aws_ecr_repository" "mns_publisher_lambda_repository" {
9+
image_scanning_configuration {
10+
scan_on_push = true
11+
}
12+
name = "${local.short_prefix}-mns-publisher-repo"
13+
force_delete = local.is_temp
14+
}
15+
16+
# Module for building and pushing Docker image to ECR
17+
module "mns_publisher_docker_image" {
18+
source = "terraform-aws-modules/lambda/aws//modules/docker-build"
19+
version = "8.5.0"
20+
docker_file_path = "./mns_publisher/Dockerfile"
21+
22+
create_ecr_repo = false
23+
ecr_repo = aws_ecr_repository.mns_publisher_lambda_repository.name
24+
ecr_repo_lifecycle_policy = jsonencode({
25+
"rules" : [
26+
{
27+
"rulePriority" : 1,
28+
"description" : "Keep only the last 2 images",
29+
"selection" : {
30+
"tagStatus" : "any",
31+
"countType" : "imageCountMoreThan",
32+
"countNumber" : 2
33+
},
34+
"action" : {
35+
"type" : "expire"
36+
}
37+
}
38+
]
39+
})
40+
41+
platform = "linux/amd64"
42+
use_image_tag = false
43+
source_path = abspath("${path.root}/../../lambdas")
44+
triggers = {
45+
dir_sha = local.mns_publisher_lambda_dir_sha
46+
shared_dir_sha = local.shared_dir_sha
47+
}
48+
}
49+
50+
resource "aws_ecr_repository_policy" "mns_publisher_lambda_ecr_image_retrieval_policy" {
51+
repository = aws_ecr_repository.mns_publisher_lambda_repository.name
52+
53+
policy = jsonencode({
54+
Version = "2012-10-17"
55+
Statement = [
56+
{
57+
"Sid" : "LambdaECRImageRetrievalPolicy",
58+
"Effect" : "Allow",
59+
"Principal" : {
60+
"Service" : "lambda.amazonaws.com"
61+
},
62+
"Action" : [
63+
"ecr:BatchGetImage",
64+
"ecr:DeleteRepositoryPolicy",
65+
"ecr:GetDownloadUrlForLayer",
66+
"ecr:GetRepositoryPolicy",
67+
"ecr:SetRepositoryPolicy"
68+
],
69+
"Condition" : {
70+
"StringLike" : {
71+
"aws:sourceArn" : "arn:aws:lambda:${var.aws_region}:${var.immunisation_account_id}:function:${local.mns_publisher_lambda_name}"
72+
}
73+
}
74+
}
75+
]
76+
})
77+
}
78+
79+
# IAM Role for Lambda
80+
resource "aws_iam_role" "mns_publisher_lambda_exec_role" {
81+
name = "${local.mns_publisher_lambda_name}-exec-role"
82+
assume_role_policy = jsonencode({
83+
Version = "2012-10-17",
84+
Statement = [{
85+
Effect = "Allow",
86+
Sid = "",
87+
Principal = {
88+
Service = "lambda.amazonaws.com"
89+
},
90+
Action = "sts:AssumeRole"
91+
}]
92+
})
93+
}
94+
95+
# Policy for Lambda execution role
96+
resource "aws_iam_policy" "mns_publisher_lambda_exec_policy" {
97+
name = "${local.mns_publisher_lambda_name}-exec-policy"
98+
policy = jsonencode({
99+
Version = "2012-10-17",
100+
Statement = [
101+
{
102+
Effect = "Allow"
103+
Action = [
104+
"logs:CreateLogGroup",
105+
"logs:CreateLogStream",
106+
"logs:PutLogEvents"
107+
]
108+
Resource = "arn:aws:logs:${var.aws_region}:${var.immunisation_account_id}:log-group:/aws/lambda/${local.mns_publisher_lambda_name}:*"
109+
},
110+
{
111+
Effect = "Allow",
112+
Action = [
113+
"ec2:CreateNetworkInterface",
114+
"ec2:DescribeNetworkInterfaces",
115+
"ec2:DeleteNetworkInterface"
116+
],
117+
Resource = "*"
118+
},
119+
{
120+
"Effect" : "Allow",
121+
"Action" : [
122+
"firehose:PutRecord",
123+
"firehose:PutRecordBatch"
124+
],
125+
"Resource" : "arn:aws:firehose:*:*:deliverystream/${module.splunk.firehose_stream_name}"
126+
},
127+
{
128+
Effect = "Allow",
129+
Action = [
130+
"sqs:ReceiveMessage",
131+
"sqs:DeleteMessage",
132+
"sqs:GetQueueAttributes"
133+
],
134+
Resource = aws_sqs_queue.mns_outbound_events.arn
135+
}
136+
]
137+
})
138+
}
139+
140+
resource "aws_iam_policy" "mns_publisher_lambda_kms_access_policy" {
141+
name = "${local.mns_publisher_lambda_name}-kms-policy"
142+
description = "Allow Lambda to decrypt environment variables"
143+
144+
policy = jsonencode({
145+
Version = "2012-10-17"
146+
Statement = [
147+
{
148+
Effect = "Allow"
149+
Action = [
150+
"kms:Decrypt"
151+
]
152+
Resource = data.aws_kms_key.existing_lambda_encryption_key.arn
153+
}
154+
]
155+
})
156+
}
157+
158+
# Attach the execution policy to the Lambda role
159+
resource "aws_iam_role_policy_attachment" "mns_publisher_lambda_exec_policy_attachment" {
160+
role = aws_iam_role.mns_publisher_lambda_exec_role.name
161+
policy_arn = aws_iam_policy.mns_publisher_lambda_exec_policy.arn
162+
}
163+
164+
# Attach the kms policy to the Lambda role
165+
resource "aws_iam_role_policy_attachment" "mns_publisher_lambda_kms_policy_attachment" {
166+
role = aws_iam_role.mns_publisher_lambda_exec_role.name
167+
policy_arn = aws_iam_policy.mns_publisher_lambda_kms_access_policy.arn
168+
}
169+
170+
# Lambda Function with Security Group and VPC.
171+
resource "aws_lambda_function" "mns_publisher_lambda" {
172+
function_name = local.mns_publisher_lambda_name
173+
role = aws_iam_role.mns_publisher_lambda_exec_role.arn
174+
package_type = "Image"
175+
image_uri = module.mns_publisher_docker_image.image_uri
176+
architectures = ["x86_64"]
177+
timeout = 120
178+
179+
vpc_config {
180+
subnet_ids = local.private_subnet_ids
181+
security_group_ids = [data.aws_security_group.existing_securitygroup.id]
182+
}
183+
184+
environment {
185+
variables = {
186+
SPLUNK_FIREHOSE_NAME = module.splunk.firehose_stream_name
187+
}
188+
}
189+
190+
kms_key_arn = data.aws_kms_key.existing_lambda_encryption_key.arn
191+
reserved_concurrent_executions = local.is_temp ? -1 : 20
192+
depends_on = [
193+
aws_cloudwatch_log_group.mns_publisher_lambda_log_group,
194+
aws_iam_policy.mns_publisher_lambda_exec_policy
195+
]
196+
}
197+
198+
resource "aws_cloudwatch_log_group" "mns_publisher_lambda_log_group" {
199+
name = "/aws/lambda/${local.mns_publisher_lambda_name}"
200+
retention_in_days = 30
201+
}
202+
203+
resource "aws_lambda_event_source_mapping" "mns_outbound_event_sqs_to_lambda" {
204+
event_source_arn = aws_sqs_queue.mns_outbound_events.arn
205+
function_name = aws_lambda_function.mns_publisher_lambda.arn
206+
batch_size = 10
207+
enabled = true
208+
}
209+
210+
resource "aws_cloudwatch_log_metric_filter" "mns_publisher_error_logs" {
211+
count = var.error_alarm_notifications_enabled ? 1 : 0
212+
213+
name = "${local.short_prefix}-MnsPublisherErrorLogsFilter"
214+
pattern = "%\\[ERROR\\]%"
215+
log_group_name = aws_cloudwatch_log_group.mns_publisher_lambda_log_group.name
216+
217+
metric_transformation {
218+
name = "${local.short_prefix}-MnsPublisherErrorLogs"
219+
namespace = "${local.short_prefix}-MnsPublisherLambda"
220+
value = "1"
221+
}
222+
}
223+
224+
resource "aws_cloudwatch_metric_alarm" "mns_publisher_error_alarm" {
225+
count = var.error_alarm_notifications_enabled ? 1 : 0
226+
227+
alarm_name = "${local.mns_publisher_lambda_name}-error"
228+
comparison_operator = "GreaterThanOrEqualToThreshold"
229+
evaluation_periods = 1
230+
metric_name = "${local.short_prefix}-MnsPublisherErrorLogs"
231+
namespace = "${local.short_prefix}-MnsPublisherLambda"
232+
period = 120
233+
statistic = "Sum"
234+
threshold = 1
235+
alarm_description = "This sets off an alarm for any error logs found in the MNS Publisher Lambda function"
236+
alarm_actions = [data.aws_sns_topic.imms_system_alert_errors.arn]
237+
treat_missing_data = "notBreaching"
238+
}

0 commit comments

Comments
 (0)