diff --git a/services/terraform/modules/shared/outputs.tf b/services/terraform/modules/shared/outputs.tf index da7b64059..ceaecdc67 100644 --- a/services/terraform/modules/shared/outputs.tf +++ b/services/terraform/modules/shared/outputs.tf @@ -1,14 +1,15 @@ locals { exported_dynamodb_tables = [ aws_dynamodb_table.feature-flags, aws_dynamodb_table.backup-service-backup, + aws_dynamodb_table.reports-service-reports, ] } # map table names to their resources output "dynamodb_tables" { value = { for table in local.exported_dynamodb_tables : table.name => table } } diff --git a/services/terraform/remote/aws_iam.tf b/services/terraform/remote/aws_iam.tf index 0ea636006..71f4877ae 100644 --- a/services/terraform/remote/aws_iam.tf +++ b/services/terraform/remote/aws_iam.tf @@ -1,168 +1,196 @@ ### General AWS Utility IAM resources # Docs: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/instance_IAM_role.html resource "aws_iam_role" "ecs_instance_role" { name = "ecsInstanceRole" description = "Allows EC2 instances to call AWS services on your behalf." assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ec2.amazonaws.com" } } ] }) managed_policy_arns = [ "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role", # Let instances download Docker images from ECR "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" ] } # ECS Task execution role # Docs: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html resource "aws_iam_role" "ecs_task_execution" { name = "ecsTaskExecutionRole" assume_role_policy = jsonencode({ Version = "2008-10-17" Statement = [ { Sid = "" Action = "sts:AssumeRole" Effect = "Allow" Principal = { Service = "ecs-tasks.amazonaws.com" } } ] }) managed_policy_arns = [ "arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess", "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", # Let ECS write logs to CloudWatch "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess", # Let ECS tasks access secrets to expose them as env vars "arn:aws:iam::aws:policy/SecretsManagerReadWrite", ] } # Assume Role Policy Document for EC2 and ECS # This policy allows ECS and EC2 use roles that it is assigned to data "aws_iam_policy_document" "assume_role_ecs_ec2" { statement { effect = "Allow" actions = [ "sts:AssumeRole", ] principals { type = "Service" identifiers = [ "ec2.amazonaws.com", "ecs-tasks.amazonaws.com" ] } } } # Allows ECS Exec to SSH into service task containers resource "aws_iam_policy" "allow_ecs_exec" { name = "allow-ecs-exec" description = "Adds SSM permissions to enable ECS Exec" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "ssmmessages:CreateControlChannel", "ssmmessages:CreateDataChannel", "ssmmessages:OpenControlChannel", "ssmmessages:OpenDataChannel" ] Resource = "*" } ] }) } ### App IAM resources # Our app role - this is to give access to DynamoDB etc # Has trust policy with EC2 and ECS # Also allows to SSH into containers resource "aws_iam_role" "services_ddb_full_access" { name = "dynamodb-s3-full-access" description = "Full RW access to DDB and S3. Allows to SSH into ECS containers" assume_role_policy = data.aws_iam_policy_document.assume_role_ecs_ec2.json managed_policy_arns = [ aws_iam_policy.allow_ecs_exec.arn, "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess", "arn:aws:iam::aws:policy/AmazonS3FullAccess", ] } # Feature Flags IAM data "aws_iam_policy_document" "read_feature_flags" { statement { sid = "FeatureFlagsDDBReadAccess" effect = "Allow" actions = [ "dynamodb:BatchGetItem", "dynamodb:GetItem", "dynamodb:Query", "dynamodb:Scan", ] resources = [ module.shared.dynamodb_tables["feature-flags"].arn ] } } resource "aws_iam_policy" "read_feature_flags" { name = "feature-flags-ddb-read-access" policy = data.aws_iam_policy_document.read_feature_flags.json description = "Allows full read access to feature-flags DynamoDB table" } resource "aws_iam_role" "feature_flags_service" { name = "feature-flags-service-role" assume_role_policy = data.aws_iam_policy_document.assume_role_ecs_ec2.json managed_policy_arns = [ aws_iam_policy.read_feature_flags.arn ] } # Backup Service IAM data "aws_iam_policy_document" "manage_backup_ddb" { statement { sid = "BackupFullDDBAccess" effect = "Allow" actions = [ "dynamodb:*", ] resources = [ module.shared.dynamodb_tables["backup-service-backup"].arn, "${module.shared.dynamodb_tables["backup-service-backup"].arn}/index/*" ] } } resource "aws_iam_policy" "manage_backup_ddb" { name = "backup-ddb-full-access" policy = data.aws_iam_policy_document.manage_backup_ddb.json description = "Allows full access to backup DynamoDB table" } resource "aws_iam_role" "backup_service" { name = "backup-service-role" assume_role_policy = data.aws_iam_policy_document.assume_role_ecs_ec2.json managed_policy_arns = [ aws_iam_policy.allow_ecs_exec.arn, aws_iam_policy.manage_backup_ddb.arn ] } + +# Reports Service IAM +data "aws_iam_policy_document" "manage_reports_ddb" { + statement { + sid = "ReportsFullDDBAccess" + effect = "Allow" + actions = [ + "dynamodb:*", + ] + resources = [ + module.shared.dynamodb_tables["reports-service-reports"].arn + ] + } +} +resource "aws_iam_policy" "manage_reports_ddb" { + name = "reports-ddb-full-access" + policy = data.aws_iam_policy_document.manage_reports_ddb.json + description = "Allows full access to reports DynamoDB table" +} +resource "aws_iam_role" "reports_service" { + name = "reports-service-role" + assume_role_policy = data.aws_iam_policy_document.assume_role_ecs_ec2.json + + managed_policy_arns = [ + aws_iam_policy.allow_ecs_exec.arn, + aws_iam_policy.manage_reports_ddb.arn + ] +} diff --git a/services/terraform/remote/service_reports.tf b/services/terraform/remote/service_reports.tf new file mode 100644 index 000000000..d46f4d24b --- /dev/null +++ b/services/terraform/remote/service_reports.tf @@ -0,0 +1,196 @@ +locals { + reports_service_image_tag = local.is_staging ? "latest" : "0.1.0" + reports_service_container_name = "reports-service-server" + reports_service_server_image = "commapp/reports-server:${local.reports_service_image_tag}" + reports_service_container_http_port = 50056 + reports_service_domain_name = "reports.${local.root_domain}" +} + +resource "aws_secretsmanager_secret" "email_config" { + name_prefix = "email_config" + description = "E-mail configuration for the reports service" +} +resource "aws_secretsmanager_secret_version" "email_config" { + secret_id = aws_secretsmanager_secret.email_config.id + secret_string = jsonencode(local.secrets["emailConfig"]) +} + +resource "aws_ecs_task_definition" "reports_service" { + family = "reports-service-task-def" + container_definitions = jsonencode([ + { + name = local.reports_service_container_name + image = local.reports_service_server_image + essential = true + portMappings = [ + { + containerPort = local.reports_service_container_http_port + protocol = "tcp" + appProtocol = "http" + }, + ] + environment = [ + { + name = "RUST_LOG" + value = "info" + }, + { + name = "PUBLIC_URL", + value = "https://${local.reports_service_domain_name}" + }, + { + name = "BLOB_SERVICE_URL", + value = local.blob_local_url + # If this ever fails, we can fallback to blob public URL: + # "https://${local.blob_service_domain_name}" + }, + ] + # Don't enable e-mails on staging. + secrets = local.is_staging ? [] : [ + { + # This is exposed as an environment variable in the container + name = "EMAIL_CONFIG" + valueFrom = aws_secretsmanager_secret.email_config.arn + } + ] + logConfiguration = { + "logDriver" = "awslogs" + "options" = { + "awslogs-create-group" = "true" + "awslogs-group" = "/ecs/reports-service-task-def" + "awslogs-region" = "us-east-2" + "awslogs-stream-prefix" = "ecs" + } + } + } + ]) + task_role_arn = aws_iam_role.reports_service.arn + execution_role_arn = aws_iam_role.ecs_task_execution.arn + network_mode = "bridge" + cpu = "256" + memory = "256" + requires_compatibilities = ["EC2"] + + # Set this to true if you want to keep old revisions + # when this definition is changed + skip_destroy = false +} + +resource "aws_ecs_service" "reports_service" { + name = "reports-service" + cluster = aws_ecs_cluster.comm_services.id + launch_type = "EC2" + + task_definition = aws_ecs_task_definition.reports_service.arn + force_new_deployment = true + + desired_count = 1 + lifecycle { + ignore_changes = [desired_count] + } + + service_connect_configuration { + # to be able to reach Blob service by DNS name + enabled = true + } + + # HTTP + load_balancer { + target_group_arn = aws_lb_target_group.reports_service_http.arn + container_name = local.reports_service_container_name + container_port = local.reports_service_container_http_port + } + + deployment_circuit_breaker { + enable = true + rollback = true + } + + enable_execute_command = true + enable_ecs_managed_tags = true +} + +# Security group to configure access to the service +resource "aws_security_group" "reports_service" { + name = "reports-service-ecs-sg" + vpc_id = aws_vpc.default.id + + ingress { + from_port = local.reports_service_container_http_port + to_port = local.reports_service_container_http_port + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + description = "HTTP port" + } + + # Allow all outbound traffic + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_lb_target_group" "reports_service_http" { + name = "reports-service-ecs-http-tg" + port = local.reports_service_container_http_port + protocol = "HTTP" + vpc_id = aws_vpc.default.id + + # ECS Fargate requires target type set to IP + target_type = "instance" + + health_check { + enabled = true + healthy_threshold = 2 + unhealthy_threshold = 3 + + protocol = "HTTP" + path = "/health" + matcher = "200-204" + } +} + +# Load Balancer +resource "aws_lb" "reports_service" { + load_balancer_type = "application" + name = "reports-service-lb" + internal = false + subnets = [ + aws_subnet.public_a.id, + aws_subnet.public_b.id, + aws_subnet.public_c.id, + ] +} + +resource "aws_lb_listener" "reports_service_https" { + load_balancer_arn = aws_lb.reports_service.arn + port = "443" + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" + certificate_arn = data.aws_acm_certificate.reports_service.arn + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.reports_service_http.arn + } + + lifecycle { + # Target group cannot be destroyed if it is used + replace_triggered_by = [aws_lb_target_group.reports_service_http] + + # Required to avoid no-op plan differences + ignore_changes = [default_action[0].forward[0].stickiness[0].duration] + } +} + +# SSL Certificate +data "aws_acm_certificate" "reports_service" { + domain = local.reports_service_domain_name + statuses = ["ISSUED"] +}