diff --git a/.github/workflows/actions/deploy-to-aws/action.yml b/.github/workflows/actions/deploy-to-aws/action.yml index ceb11873..e1ce40a1 100644 --- a/.github/workflows/actions/deploy-to-aws/action.yml +++ b/.github/workflows/actions/deploy-to-aws/action.yml @@ -66,7 +66,25 @@ runs: - name: Login to Amazon ECR uses: aws-actions/amazon-ecr-login@v2 - - name: Pull Docker image from GHCR + - name: Check ECR Image exists + id: ecr-check + shell: bash + run: | + IMAGE_TAG=${{ inputs.image_name }}-${{ inputs.short_sha}} + REPOSITORY_NAME=${{ inputs.app_name }}-ecr-repo-${{ inputs.environment }} + + IMAGE_EXISTS=$(aws ecr describe-images --repository-name $REPOSITORY_NAME --query "imageDetails[?contains(imageTags, '$IMAGE_TAG')]" --output text) + + if [ -z "$IMAGE_EXISTS" ]; then + echo "Image with tag $IMAGE_TAG does not exist." + echo "exists=false" >> $GITHUB_OUTPUT + else + echo "Image with tag $IMAGE_TAG already exists." + echo "exists=true" >> $GITHUB_OUTPUT + fi + + - name: Push if Docker image does not exist + if: steps.ecr-check.outputs.exists == 'false' shell: bash run: | docker pull ${{ inputs.github_image_repo }}/${{ inputs.image_name }}:${{ inputs.short_sha}} diff --git a/infrastructure/cloud/environments/dev/dev.tfvars b/infrastructure/cloud/environments/dev/dev.tfvars index 809509aa..1a02c1a4 100644 --- a/infrastructure/cloud/environments/dev/dev.tfvars +++ b/infrastructure/cloud/environments/dev/dev.tfvars @@ -5,3 +5,6 @@ app_subnet_names = ["App_Dev_aza_net", "App_Dev_azb_net"] data_subnet_names = ["Data_Dev_aza_net", "Data_Dev_azb_net"] openshift_iam_user = "openshiftuserdev" iam_user_table_name = "BCGOV_IAM_USER_TABLE" +lb_name = "default" +rds_db_ca_cert = "rds-ca-rsa2048-g1" +cert_domain_name = "*.example.ca" diff --git a/infrastructure/cloud/environments/dev/variables.tf b/infrastructure/cloud/environments/dev/variables.tf index e44f4cd6..007c64be 100644 --- a/infrastructure/cloud/environments/dev/variables.tf +++ b/infrastructure/cloud/environments/dev/variables.tf @@ -52,3 +52,19 @@ variable "iam_user_table_name" { description = "The BCGOV DynamoDb IAM user table" type = string } + +variable "lb_name" { + description = "The BCGOV provisioned Load Balancer name" + type = string +} + +variable "rds_db_ca_cert" { + description = "The Certifiate Authority identifier used in RDS" + type = string +} + +variable "cert_domain_name" { + description = "The BCGov provisioned certificate domain name" + type = string +} + diff --git a/infrastructure/cloud/environments/dev/webapp.tf b/infrastructure/cloud/environments/dev/webapp.tf index 49c31d50..273a8285 100644 --- a/infrastructure/cloud/environments/dev/webapp.tf +++ b/infrastructure/cloud/environments/dev/webapp.tf @@ -8,6 +8,7 @@ module "security" { ecr_repository_arn = module.container.ecr_repository_arn openshift_iam_user = var.openshift_iam_user iam_user_table_name = var.iam_user_table_name + cert_domain_name = var.cert_domain_name } module "storage" { @@ -16,18 +17,26 @@ module "storage" { app_name = var.app_name kms_key_name = module.security.kms_key_alias test_s3_bucket_name = var.test_s3_bucket_name + data_sg_id = module.networking.data_sg_id + db_username = module.security.db_username + db_password = module.security.db_password + vpc_id = var.vpc_id + kms_key_arn = module.security.kms_key_arn + rds_db_ca_cert = var.rds_db_ca_cert depends_on = [module.security] } module "networking" { - source = "../../modules/networking" - environment = var.environment - app_name = var.app_name - region = var.region - vpc_id = var.vpc_id - web_subnet_names = var.web_subnet_names - app_subnet_names = var.app_subnet_names - data_subnet_names = var.data_subnet_names + source = "../../modules/networking" + environment = var.environment + app_name = var.app_name + region = var.region + vpc_id = var.vpc_id + web_subnet_names = var.web_subnet_names + app_subnet_names = var.app_subnet_names + data_subnet_names = var.data_subnet_names + lb_name = var.lb_name + default_lb_cert_arn = module.security.default_lb_cert_arn } module "container" { @@ -36,16 +45,18 @@ module "container" { app_name = var.app_name region = var.region ecs_execution_role_arn = module.security.ecs_execution_role_arn - subnet_ids = module.networking.web_subnets_ids - ecs_sg_id = module.networking.ecs_sg_id - lb_tg_arn = module.networking.lb_tg_arn + web_subnet_ids = module.networking.web_subnets_ids + app_subnet_ids = module.networking.app_subnets_ids + web_sg_id = module.networking.web_sg_id + app_sg_id = module.networking.app_sg_id + web_tg_arn = module.networking.web_tg_arn + api_tg_arn = module.networking.api_tg_arn ecs_web_td_log_group_name = module.monitoring.ecs_web_td_log_group_name ecs_api_td_log_group_name = module.monitoring.ecs_api_td_log_group_name kms_key_id = module.security.kms_key_id - lb_dns_name = module.networking.lb_dns_name + default_lb_dns_name = module.networking.default_lb_dns_name api_secrets = module.security.api_secrets web_secrets = module.security.web_secrets - db_secrets = module.security.db_secrets depends_on = [module.monitoring] } diff --git a/infrastructure/cloud/modules/container/ecs.tf b/infrastructure/cloud/modules/container/ecs.tf index 85257ce0..99f15669 100644 --- a/infrastructure/cloud/modules/container/ecs.tf +++ b/infrastructure/cloud/modules/container/ecs.tf @@ -19,15 +19,22 @@ resource "aws_ecs_task_definition" "ecs_web_task_definition" { cpu = 256 memory = 512 execution_role_arn = var.ecs_execution_role_arn + task_role_arn = var.ecs_execution_role_arn + + lifecycle { + ignore_changes = [container_definitions] + } container_definitions = jsonencode([ { name = "${var.app_name}-web-container-${var.environment}" - image = "${aws_ecr_repository.ecr_repository.repository_url}:web" + image = "${aws_ecr_repository.ecr_repository.repository_url}:jasper-web" essential = true portMappings = [ { containerPort = 8080 + hostPort = 8080 + protocol = "tcp" } ] logConfiguration = { @@ -49,23 +56,28 @@ resource "aws_ecs_task_definition" "ecs_web_task_definition" { } resource "aws_ecs_service" "ecs_web_service" { - name = "${var.app_name}-ecs-web-service-${var.environment}" - cluster = aws_ecs_cluster.ecs_cluster.id - task_definition = aws_ecs_task_definition.ecs_web_task_definition.arn - launch_type = "FARGATE" - desired_count = 1 + name = "${var.app_name}-ecs-web-service-${var.environment}" + cluster = aws_ecs_cluster.ecs_cluster.id + task_definition = aws_ecs_task_definition.ecs_web_task_definition.arn + launch_type = "FARGATE" + desired_count = 1 + enable_execute_command = true network_configuration { - subnets = var.subnet_ids - security_groups = [var.ecs_sg_id] + subnets = var.web_subnet_ids + security_groups = [var.app_sg_id] assign_public_ip = true } load_balancer { - target_group_arn = var.lb_tg_arn + target_group_arn = var.web_tg_arn container_name = "${var.app_name}-web-container-${var.environment}" container_port = 8080 } + + lifecycle { + prevent_destroy = true + } } # API @@ -76,21 +88,28 @@ resource "aws_ecs_task_definition" "ecs_api_task_definition" { cpu = 256 memory = 512 execution_role_arn = var.ecs_execution_role_arn + task_role_arn = var.ecs_execution_role_arn + + lifecycle { + ignore_changes = [container_definitions] + } container_definitions = jsonencode([ { name = "${var.app_name}-api-container-${var.environment}" - image = "${aws_ecr_repository.ecr_repository.repository_url}:api" + image = "${aws_ecr_repository.ecr_repository.repository_url}:jasper-api" essential = true portMappings = [ { containerPort = 5000 + hostPort = 5000 + protocol = "tcp" } ] environment = [ { name = "CORS_DOMAIN" - value = var.lb_dns_name + value = var.default_lb_dns_name } ] secrets = [ @@ -119,14 +138,18 @@ resource "aws_ecs_service" "ecs_api_service" { desired_count = 1 network_configuration { - subnets = var.subnet_ids - security_groups = [var.ecs_sg_id] + subnets = var.app_subnet_ids + security_groups = [var.app_sg_id] assign_public_ip = true } load_balancer { - target_group_arn = var.lb_tg_arn + target_group_arn = var.api_tg_arn container_name = "${var.app_name}-api-container-${var.environment}" container_port = 5000 } + + lifecycle { + prevent_destroy = true + } } diff --git a/infrastructure/cloud/modules/container/variables.tf b/infrastructure/cloud/modules/container/variables.tf index 08b35334..7b623789 100644 --- a/infrastructure/cloud/modules/container/variables.tf +++ b/infrastructure/cloud/modules/container/variables.tf @@ -18,18 +18,33 @@ variable "ecs_execution_role_arn" { type = string } -variable "subnet_ids" { - description = "Subnet IDs in which ECS will deploy the tasks" +variable "web_subnet_ids" { + description = "The Web Subnet IDs" type = list(string) } -variable "ecs_sg_id" { - description = "Load Balancer Security Group ID" +variable "app_subnet_ids" { + description = "The App Subnet IDs" + type = list(string) +} + +variable "web_sg_id" { + description = "The BCGOV provisioned Web Security Group" + type = string +} + +variable "app_sg_id" { + description = "The BCGOV provisioned App Security Group" type = string } -variable "lb_tg_arn" { - description = "Load Balancer Target Group ARN" +variable "web_tg_arn" { + description = "The Web Target Group ARN" + type = string +} + +variable "api_tg_arn" { + description = "The API Target Group ARN" type = string } @@ -48,8 +63,8 @@ variable "kms_key_id" { type = string } -variable "lb_dns_name" { - description = "Load Balancer DNS Name" +variable "default_lb_dns_name" { + description = "The BCGov Load Balancer DNS Name" type = string } @@ -62,8 +77,3 @@ variable "web_secrets" { description = "List if env variable secrets used in Web" type = list(list(string)) } - -variable "db_secrets" { - description = "List if env variable secrets used in Database" - type = list(list(string)) -} diff --git a/infrastructure/cloud/modules/networking/alb.tf b/infrastructure/cloud/modules/networking/alb.tf index 04b7c063..4457cdf9 100644 --- a/infrastructure/cloud/modules/networking/alb.tf +++ b/infrastructure/cloud/modules/networking/alb.tf @@ -1,49 +1,85 @@ -resource "aws_lb" "lb" { - name = "${var.app_name}-lb-${var.environment}" - subnets = local.web_subnets - security_groups = [aws_security_group.lb_sg.id] - internal = true - load_balancer_type = "application" - enable_http2 = true - drop_invalid_header_fields = true +# +# Default Load Balancer +# +data "aws_lb" "default_lb" { + name = var.lb_name +} - tags = { - Name = "${var.app_name}-lb-${var.environment}" +# HTTP Listener +resource "aws_lb_listener" "http_listener" { + load_balancer_arn = data.aws_lb.default_lb.arn + port = 80 + protocol = "HTTP" + + default_action { + type = "redirect" + + redirect { + host = "#{host}" + path = "/" + port = "443" + protocol = "HTTPS" + query = "#{query}" + status_code = "HTTP_301" # Use HTTP_302 for temporary redirects + } } } -resource "aws_lb_target_group" "lb_target_group" { - name = "${var.app_name}-lb-tg-${var.environment}" - port = 8080 - protocol = "HTTP" - vpc_id = data.aws_vpc.vpc.id - target_type = "ip" - deregistration_delay = 5 - - health_check { - enabled = true - interval = 15 - path = "/" - port = 8080 - protocol = "HTTP" - timeout = 10 - healthy_threshold = 2 - unhealthy_threshold = 3 - matcher = "200" - } - - lifecycle { - create_before_destroy = true +# HTTPS Listener +resource "aws_lb_listener" "https_listener" { + load_balancer_arn = data.aws_lb.default_lb.arn + port = 443 + protocol = "HTTPS" + certificate_arn = var.default_lb_cert_arn + + default_action { + type = "fixed-response" + + fixed_response { + content_type = "application/json" + status_code = 200 + } } } -resource "aws_lb_listener" "lb_listener" { - load_balancer_arn = aws_lb.lb.arn - port = 80 - protocol = "HTTP" +# HTTPS Listener Rules +resource "aws_lb_listener_rule" "web_lr" { + listener_arn = aws_lb_listener.https_listener.arn + priority = 200 - default_action { + condition { + path_pattern { + values = ["/*"] + } + } + + action { type = "forward" - target_group_arn = aws_lb_target_group.lb_target_group.arn + target_group_arn = aws_lb_target_group.web_target_group.arn + } + + tags = { + Name = "${var.app_name}-web-lr-${var.environment}" + } +} + +resource "aws_lb_listener_rule" "api_path_lr" { + listener_arn = aws_lb_listener.https_listener.arn + priority = 100 + + condition { + path_pattern { + values = ["/api/*"] + } + } + + action { + type = "forward" + target_group_arn = aws_lb_target_group.api_target_group.arn + } + + tags = { + Name = "${var.app_name}-api-lr-${var.environment}" } } + diff --git a/infrastructure/cloud/modules/networking/outputs.tf b/infrastructure/cloud/modules/networking/outputs.tf index 4f22e94c..313ef830 100644 --- a/infrastructure/cloud/modules/networking/outputs.tf +++ b/infrastructure/cloud/modules/networking/outputs.tf @@ -1,15 +1,34 @@ -output "lb_tg_arn" { - value = aws_lb_target_group.lb_target_group.arn +output "web_tg_arn" { + value = aws_lb_target_group.web_target_group.arn } -output "ecs_sg_id" { - value = aws_security_group.ecs_sg.id +output "api_tg_arn" { + value = aws_lb_target_group.api_target_group.arn +} + +output "web_sg_id" { + value = data.aws_security_group.web_sg.id +} + +output "app_sg_id" { + value = data.aws_security_group.app_sg.id } output "web_subnets_ids" { value = local.web_subnets } -output "lb_dns_name" { - value = aws_lb.lb.dns_name +output "app_subnets_ids" { + value = local.app_subnets +} +output "data_subnets_ids" { + value = local.data_subnets +} + +output "default_lb_dns_name" { + value = data.aws_lb.default_lb.dns_name +} + +output "data_sg_id" { + value = data.aws_security_group.data_sg.id } diff --git a/infrastructure/cloud/modules/networking/securitygroup.tf b/infrastructure/cloud/modules/networking/securitygroup.tf index 21eeb80e..0dc0f892 100644 --- a/infrastructure/cloud/modules/networking/securitygroup.tf +++ b/infrastructure/cloud/modules/networking/securitygroup.tf @@ -1,146 +1,161 @@ # -# Load Balancer Security Group +# BCGOV provisioned security groups # -resource "aws_security_group" "lb_sg" { - name = "${var.app_name}-lb-sg-${var.environment}" - vpc_id = data.aws_vpc.vpc.id - description = "Security Group for the Application Load Balancer" - - tags = { - Name = "${var.app_name}_lb_sg_${var.environment}" - } +data "aws_security_group" "web_sg" { + name = "Web_sg" } -# Load Balancer Ingress Rules. This will change once we get the public load balancer details from cloud team. -resource "aws_vpc_security_group_ingress_rule" "lb_sg_ingress_http_allow_80" { - security_group_id = aws_security_group.lb_sg.id - description = "Allow inbound HTTP traffic on port 80" - ip_protocol = "tcp" - from_port = 80 - to_port = 80 - cidr_ipv4 = "0.0.0.0/0" +data "aws_security_group" "app_sg" { + name = "App_sg" } -resource "aws_vpc_security_group_ingress_rule" "lb_sg_ingress_http_allow_8080" { - security_group_id = aws_security_group.lb_sg.id - description = "Allow inbound HTTP traffic on port 8080" - ip_protocol = "tcp" - from_port = 8080 - to_port = 8080 - cidr_ipv4 = "0.0.0.0/0" +data "aws_security_group" "data_sg" { + name = "Data_sg" } -# Load Balancer Egress Rules -resource "aws_vpc_security_group_egress_rule" "lb_sg_egress_allow_to_ecs_sg" { - security_group_id = aws_security_group.lb_sg.id - referenced_security_group_id = aws_security_group.ecs_sg.id - description = "Allow all outbound traffic to ECS SG from Load Balancer SG" - ip_protocol = "-1" -} - -# -# ECS Security Group -# -resource "aws_security_group" "ecs_sg" { - name = "${var.app_name}-ecs-sg-${var.environment}" - vpc_id = data.aws_vpc.vpc.id - description = "Security Group for ECS services" - - tags = { - Name = "${var.app_name}_ecs_sg_${var.environment}" - } -} - -# ECS Ingress Rules -# Remove ecs_sg_ingress_allow_icmp and ecs_sg_ingress_allow_ssh once the JASPER -# is publicly accessible. These ingress rules is for tesing SG-SG connectivity using -# EC2 Instance and EC2 Instance Connect Endpoint -resource "aws_vpc_security_group_ingress_rule" "ecs_sg_ingress_allow_from_lb_sg" { - security_group_id = aws_security_group.ecs_sg.id - referenced_security_group_id = aws_security_group.lb_sg.id - description = "Allow all inbound traffic from ECS SG" - ip_protocol = -1 -} - -resource "aws_vpc_security_group_ingress_rule" "ecs_sg_ingress_allow_from_lambda_sg" { - security_group_id = aws_security_group.ecs_sg.id - referenced_security_group_id = aws_security_group.lambda_sg.id - description = "Allow all inbound traffic from Lambda SG" - ip_protocol = -1 -} - -resource "aws_vpc_security_group_ingress_rule" "ecs_sg_ingress_allow_icmp" { - security_group_id = aws_security_group.ecs_sg.id - description = "Allow inbound ICMP traffic to ECS SG to allow pinging the Lambda SG" - ip_protocol = "icmp" - from_port = -1 - to_port = -1 - cidr_ipv4 = "0.0.0.0/0" -} - -resource "aws_vpc_security_group_ingress_rule" "ecs_sg_ingress_allow_ssh" { - security_group_id = aws_security_group.ecs_sg.id - description = "Allow inbound SSH traffic to ECS SG" - ip_protocol = "tcp" - from_port = 22 - to_port = 22 - cidr_ipv4 = data.aws_vpc.vpc.cidr_block -} - -# ECS Egress Rules -resource "aws_vpc_security_group_egress_rule" "ecs_sg_egress_allow_to_anywhere" { - security_group_id = aws_security_group.ecs_sg.id - description = "Unrestricted" - ip_protocol = "-1" - cidr_ipv4 = "0.0.0.0/0" -} - -# -# Lambda Security Group -# -resource "aws_security_group" "lambda_sg" { - name = "${var.app_name}-lambda-sg-${var.environment}" - vpc_id = data.aws_vpc.vpc.id - description = "Security Group for Lambda functions" - - tags = { - Name = "${var.app_name}_lambda_sg_${var.environment}" - } -} - -# Lambda Ingress Rules -# Remove lambda_sg_ingress_allow_icmp and lambda_sg_ingress_allow_ssh once the JASPER -# is publicly accessible. These ingress rules is for tesing SG-SG connectivity using -# EC2 Instance and EC2 Instance Connect Endpoint -resource "aws_vpc_security_group_ingress_rule" "lambda_sg_ingress_allow_from_ecs_sg" { - security_group_id = aws_security_group.lambda_sg.id - referenced_security_group_id = aws_security_group.ecs_sg.id - description = "Allow all inbound traffic from ECS SG" - ip_protocol = -1 -} - -resource "aws_vpc_security_group_ingress_rule" "lambda_sg_ingress_allow_icmp" { - security_group_id = aws_security_group.lambda_sg.id - description = "Allow inbound ICMP traffic to Lambda SG to allow pinging the ECS SG" - ip_protocol = "icmp" - from_port = -1 - to_port = -1 - cidr_ipv4 = "0.0.0.0/0" -} - -resource "aws_vpc_security_group_ingress_rule" "lambda_sg_ingress_allow_ssh" { - security_group_id = aws_security_group.lambda_sg.id - description = "Allow inbound SSH traffic to Lambda SG" - ip_protocol = "tcp" - from_port = 22 - to_port = 22 - cidr_ipv4 = data.aws_vpc.vpc.cidr_block -} - -# Lambda Egress Rules -resource "aws_vpc_security_group_egress_rule" "lambda_sg_egress_allow_to_anywhere" { - security_group_id = aws_security_group.lambda_sg.id - description = "Unrestricted" - ip_protocol = "-1" - cidr_ipv4 = "0.0.0.0/0" -} +# # +# # Load Balancer Security Group +# # +# resource "aws_security_group" "lb_sg" { +# name = "${var.app_name}-lb-sg-${var.environment}" +# vpc_id = data.aws_vpc.vpc.id +# description = "Security Group for the Application Load Balancer" + +# tags = { +# Name = "${var.app_name}_lb_sg_${var.environment}" +# } +# } + +# # Load Balancer Ingress Rules. This will change once we get the public load balancer details from cloud team. +# resource "aws_vpc_security_group_ingress_rule" "lb_sg_ingress_http_allow_80" { +# security_group_id = aws_security_group.lb_sg.id +# description = "Allow inbound HTTP traffic on port 80" +# ip_protocol = "tcp" +# from_port = 80 +# to_port = 80 +# cidr_ipv4 = "0.0.0.0/0" +# } + +# resource "aws_vpc_security_group_ingress_rule" "lb_sg_ingress_http_allow_8080" { +# security_group_id = aws_security_group.lb_sg.id +# description = "Allow inbound HTTP traffic on port 8080" +# ip_protocol = "tcp" +# from_port = 8080 +# to_port = 8080 +# cidr_ipv4 = "0.0.0.0/0" +# } + +# # Load Balancer Egress Rules +# resource "aws_vpc_security_group_egress_rule" "lb_sg_egress_allow_to_ecs_sg" { +# security_group_id = aws_security_group.lb_sg.id +# referenced_security_group_id = aws_security_group.ecs_sg.id +# description = "Allow all outbound traffic to ECS SG from Load Balancer SG" +# ip_protocol = "-1" +# } + +# # +# # ECS Security Group +# # +# resource "aws_security_group" "ecs_sg" { +# name = "${var.app_name}-ecs-sg-${var.environment}" +# vpc_id = data.aws_vpc.vpc.id +# description = "Security Group for ECS services" + +# tags = { +# Name = "${var.app_name}_ecs_sg_${var.environment}" +# } +# } + +# # ECS Ingress Rules +# # Remove ecs_sg_ingress_allow_icmp and ecs_sg_ingress_allow_ssh once the JASPER +# # is publicly accessible. These ingress rules is for tesing SG-SG connectivity using +# # EC2 Instance and EC2 Instance Connect Endpoint +# resource "aws_vpc_security_group_ingress_rule" "ecs_sg_ingress_allow_from_web_sg" { +# security_group_id = aws_security_group.ecs_sg.id +# referenced_security_group_id = data.aws_security_group.web_sg.id +# description = "Allow all inbound traffic from Web SG" +# ip_protocol = -1 +# } + +# resource "aws_vpc_security_group_ingress_rule" "ecs_sg_ingress_allow_from_lambda_sg" { +# security_group_id = aws_security_group.ecs_sg.id +# referenced_security_group_id = aws_security_group.lambda_sg.id +# description = "Allow all inbound traffic from Lambda SG" +# ip_protocol = -1 +# } + +# resource "aws_vpc_security_group_ingress_rule" "ecs_sg_ingress_allow_icmp" { +# security_group_id = aws_security_group.ecs_sg.id +# description = "Allow inbound ICMP traffic to ECS SG to allow pinging the Lambda SG" +# ip_protocol = "icmp" +# from_port = -1 +# to_port = -1 +# cidr_ipv4 = "0.0.0.0/0" +# } + +# resource "aws_vpc_security_group_ingress_rule" "ecs_sg_ingress_allow_ssh" { +# security_group_id = aws_security_group.ecs_sg.id +# description = "Allow inbound SSH traffic to ECS SG" +# ip_protocol = "tcp" +# from_port = 22 +# to_port = 22 +# cidr_ipv4 = data.aws_vpc.vpc.cidr_block +# } + +# # ECS Egress Rules +# resource "aws_vpc_security_group_egress_rule" "ecs_sg_egress_allow_to_anywhere" { +# security_group_id = aws_security_group.ecs_sg.id +# description = "Unrestricted" +# ip_protocol = "-1" +# cidr_ipv4 = "0.0.0.0/0" +# } + +# # +# # Lambda Security Group +# # +# resource "aws_security_group" "lambda_sg" { +# name = "${var.app_name}-lambda-sg-${var.environment}" +# vpc_id = data.aws_vpc.vpc.id +# description = "Security Group for Lambda functions" + +# tags = { +# Name = "${var.app_name}_lambda_sg_${var.environment}" +# } +# } + +# # Lambda Ingress Rules +# # Remove lambda_sg_ingress_allow_icmp and lambda_sg_ingress_allow_ssh once the JASPER +# # is publicly accessible. These ingress rules is for tesing SG-SG connectivity using +# # EC2 Instance and EC2 Instance Connect Endpoint +# resource "aws_vpc_security_group_ingress_rule" "lambda_sg_ingress_allow_from_ecs_sg" { +# security_group_id = aws_security_group.lambda_sg.id +# referenced_security_group_id = aws_security_group.ecs_sg.id +# description = "Allow all inbound traffic from ECS SG" +# ip_protocol = -1 +# } + +# resource "aws_vpc_security_group_ingress_rule" "lambda_sg_ingress_allow_icmp" { +# security_group_id = aws_security_group.lambda_sg.id +# description = "Allow inbound ICMP traffic to Lambda SG to allow pinging the ECS SG" +# ip_protocol = "icmp" +# from_port = -1 +# to_port = -1 +# cidr_ipv4 = "0.0.0.0/0" +# } + +# resource "aws_vpc_security_group_ingress_rule" "lambda_sg_ingress_allow_ssh" { +# security_group_id = aws_security_group.lambda_sg.id +# description = "Allow inbound SSH traffic to Lambda SG" +# ip_protocol = "tcp" +# from_port = 22 +# to_port = 22 +# cidr_ipv4 = data.aws_vpc.vpc.cidr_block +# } + +# # Lambda Egress Rules +# resource "aws_vpc_security_group_egress_rule" "lambda_sg_egress_allow_to_anywhere" { +# security_group_id = aws_security_group.lambda_sg.id +# description = "Unrestricted" +# ip_protocol = "-1" +# cidr_ipv4 = "0.0.0.0/0" +# } diff --git a/infrastructure/cloud/modules/networking/targetgroups.tf b/infrastructure/cloud/modules/networking/targetgroups.tf new file mode 100644 index 00000000..cf6faff2 --- /dev/null +++ b/infrastructure/cloud/modules/networking/targetgroups.tf @@ -0,0 +1,42 @@ +# Web +resource "aws_lb_target_group" "web_target_group" { + name = "${var.app_name}-web-tg-${var.environment}" + port = 8080 + protocol = "HTTPS" + vpc_id = data.aws_vpc.vpc.id + target_type = "ip" + deregistration_delay = 5 + + health_check { + protocol = "HTTPS" + path = "/" + port = 8080 + interval = 30 + timeout = 5 + enabled = true + healthy_threshold = 3 + unhealthy_threshold = 3 + matcher = "200" + } +} + +# API +resource "aws_lb_target_group" "api_target_group" { + name = "${var.app_name}-api-tg-${var.environment}" + port = 5000 + protocol = "HTTP" + vpc_id = data.aws_vpc.vpc.id + target_type = "ip" + deregistration_delay = 5 + + health_check { + path = "/api/test/headers" + port = 5000 + interval = 30 + timeout = 5 + enabled = true + healthy_threshold = 3 + unhealthy_threshold = 3 + matcher = "200" + } +} diff --git a/infrastructure/cloud/modules/networking/variables.tf b/infrastructure/cloud/modules/networking/variables.tf index f3b53b07..6638249f 100644 --- a/infrastructure/cloud/modules/networking/variables.tf +++ b/infrastructure/cloud/modules/networking/variables.tf @@ -32,3 +32,13 @@ variable "data_subnet_names" { description = "List of Subnets for Data" type = list(string) } + +variable "lb_name" { + description = "The BCGOV provisioned Load Balancer name" + type = string +} + +variable "default_lb_cert_arn" { + description = "The default Load Balancer certificate ARN" + type = string +} diff --git a/infrastructure/cloud/modules/security/acm.tf b/infrastructure/cloud/modules/security/acm.tf new file mode 100644 index 00000000..e8369d8d --- /dev/null +++ b/infrastructure/cloud/modules/security/acm.tf @@ -0,0 +1,5 @@ +data "aws_acm_certificate" "default_lb_cert" { + domain = var.cert_domain_name + most_recent = true + statuses = ["ISSUED"] +} diff --git a/infrastructure/cloud/modules/security/acmpca.tf b/infrastructure/cloud/modules/security/acmpca.tf deleted file mode 100644 index c673248e..00000000 --- a/infrastructure/cloud/modules/security/acmpca.tf +++ /dev/null @@ -1,46 +0,0 @@ -# resource "aws_acmpca_certificate_authority" "acmpca_ca" { -# type = "ROOT" -# usage_mode = "GENERAL_PURPOSE" -# key_storage_security_standard = "FIPS_140_2_LEVEL_3_OR_HIGHER" -# certificate_authority_configuration { -# key_algorithm = "RSA_2048" -# signing_algorithm = "SHA256WITHRSA" -# subject { -# country = "CA" -# organization = "bcgov" -# organizational_unit = "bccourts" -# distinguished_name_qualifier = "${var.app_name}-ca-${var.environment}" -# common_name = "${var.app_name}-ca-${var.environment}" -# state = "BC" -# locality = "Vancouver" -# } -# } - -# tags = { -# Name = "${var.app_name}-acmpca-${var.environment}" -# } -# } - -# resource "aws_acmpca_permission" "acmpca_permission" { -# certificate_authority_arn = aws_acmpca_certificate_authority.acmpca_ca.arn -# actions = ["IssueCertificate", "GetCertificate", "ListPermissions"] -# principal = "acm.amazonaws.com" -# } - -# resource "aws_acmpca_certificate" "acmpca_certificate" { -# certificate_authority_arn = aws_acmpca_certificate_authority.acmpca_ca.arn -# certificate_signing_request = aws_acmpca_certificate_authority.acmpca_ca.certificate_signing_request -# signing_algorithm = "SHA256WITHRSA" -# template_arn = "arn:aws:acm-pca:::template/RootCACertificate/V1" -# validity { -# type = "YEARS" -# value = 3 -# } -# } - -# resource "aws_acmpca_certificate_authority_certificate" "acmpca_cac" { -# certificate_authority_arn = aws_acmpca_certificate_authority.acmpca_ca.arn - -# certificate = aws_acmpca_certificate.acmpca_certificate.certificate -# certificate_chain = aws_acmpca_certificate.acmpca_certificate.certificate_chain -# } diff --git a/infrastructure/cloud/modules/security/iam.tf b/infrastructure/cloud/modules/security/iam.tf index 19ecc188..e1343e5e 100644 --- a/infrastructure/cloud/modules/security/iam.tf +++ b/infrastructure/cloud/modules/security/iam.tf @@ -89,34 +89,41 @@ resource "aws_iam_role_policy" "ecs_execution_policy" { ], Effect = "Allow", Resource = aws_kms_key.kms_key.arn + }, + { + "Effect" : "Allow", + "Action" : [ + "rds:DescribeDBInstances", + "rds:Connect", + "rds:DescribeDBSecurityGroups" + ], + "Resource" : "arn:aws:rds:*:*:db:${var.app_name}-postgres-db-${var.environment}" } ] }) } -# -# RolesAnywhere -# -# resource "aws_iam_role" "rolesanywhere_role" { -# name = "${var.app_name}-rolesanywhere-role-${var.environment}" - -# assume_role_policy = jsonencode({ -# Version = "2012-10-17" -# Statement = [{ -# Action = [ -# "sts:AssumeRole", -# "sts:TagSession", -# "sts:SetSourceIdentity" -# ] -# Principal = { -# Service = "rolesanywhere.amazonaws.com" -# } -# Effect = "Allow" -# }] -# }) -# } +# Attach the AmazonECSTaskExecutionRolePolicy +resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_policy" { + role = aws_iam_role.ecs_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +# Attach the AmazonSSMManagedInstanceCore policy for ECS Exec +resource "aws_iam_role_policy_attachment" "ecs_task_ssm_policy" { + role = aws_iam_role.ecs_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" +} +# (Optional) Attach CloudWatch Logs policy if logging is needed +resource "aws_iam_role_policy_attachment" "ecs_task_cloudwatch_policy" { + role = aws_iam_role.ecs_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" +} + +# # Openshift +# # https://developer.gov.bc.ca/docs/default/component/public-cloud-techdocs/design-build-and-deploy-an-application/iam-user-service/ # Step 1: Add opeshiftuser if not exist data "aws_dynamodb_table" "iam_user_table" { diff --git a/infrastructure/cloud/modules/security/outputs.tf b/infrastructure/cloud/modules/security/outputs.tf index 39fb8440..add54138 100644 --- a/infrastructure/cloud/modules/security/outputs.tf +++ b/infrastructure/cloud/modules/security/outputs.tf @@ -18,6 +18,7 @@ output "kms_key_arn" { output "api_secrets" { value = [ ["ASPNETCORE_URLS", "${aws_secretsmanager_secret.aspnet_core_secret.arn}:urls::"], + ["ASPNETCORE_ENVIRONMENT", "${aws_secretsmanager_secret.aspnet_core_secret.arn}:environment::"], ["Auth__UserId", "${aws_secretsmanager_secret.auth_secret.arn}:userId::"], ["Auth__UserPassword", "${aws_secretsmanager_secret.auth_secret.arn}:userPassword::"], ["Auth__AllowSiteMinderUserType", "${aws_secretsmanager_secret.auth_secret.arn}:allowSiteMinderUserType::"], @@ -63,10 +64,16 @@ output "web_secrets" { ] } -output "db_secrets" { - value = [ - ["POSTGRES_USER", "${aws_secretsmanager_secret.database_secret.arn}:user::"], - ["POSTGRES_PASSWORD", "${aws_secretsmanager_secret.database_secret.arn}:password::"], - ["POSTGRES_DB", "${aws_secretsmanager_secret.database_secret.arn}:database::"] - ] +output "db_username" { + value = jsondecode(data.aws_secretsmanager_secret_version.current_db_secret_value.secret_string).user + sensitive = true +} + +output "db_password" { + value = jsondecode(data.aws_secretsmanager_secret_version.current_db_secret_value.secret_string).password + sensitive = true +} + +output "default_lb_cert_arn" { + value = data.aws_acm_certificate.default_lb_cert.arn } diff --git a/infrastructure/cloud/modules/security/rolesanywhere.tf b/infrastructure/cloud/modules/security/rolesanywhere.tf deleted file mode 100644 index b6c79e0b..00000000 --- a/infrastructure/cloud/modules/security/rolesanywhere.tf +++ /dev/null @@ -1,29 +0,0 @@ -# resource "aws_rolesanywhere_trust_anchor" "ra_ta" { -# name = "${var.app_name}-ra-ta-${var.environment}" -# enabled = true - - -# source { -# source_data { -# acm_pca_arn = aws_acmpca_certificate_authority.acmpca_ca.arn -# } -# source_type = "AWS_ACM_PCA" -# } - -# depends_on = [aws_acmpca_certificate_authority.acmpca_ca] - -# tags = { -# Name = "${var.app_name}-ra-ta-${var.environment}" -# } -# } - -# resource "aws_rolesanywhere_profile" "ra_profile" { -# name = "${var.app_name}-ra-profile-${var.environment}" -# enabled = true -# require_instance_properties = false -# role_arns = [aws_iam_role.rolesanywhere_role.arn] - -# tags = { -# Name = "${var.app_name}-ra-profile-${var.environment}" -# } -# } diff --git a/infrastructure/cloud/modules/security/secretsmanager.tf b/infrastructure/cloud/modules/security/secretsmanager.tf index 369ae535..78e99e46 100644 --- a/infrastructure/cloud/modules/security/secretsmanager.tf +++ b/infrastructure/cloud/modules/security/secretsmanager.tf @@ -116,6 +116,10 @@ resource "aws_secretsmanager_secret_version" "database_secret_value" { }) } +data "aws_secretsmanager_secret_version" "current_db_secret_value" { + secret_id = aws_secretsmanager_secret.database_secret.id +} + resource "aws_secretsmanager_secret" "aspnet_core_secret" { name = "external/${var.app_name}-aspnet-core-secret-${var.environment}" kms_key_id = aws_kms_key.kms_key.arn diff --git a/infrastructure/cloud/modules/security/variables.tf b/infrastructure/cloud/modules/security/variables.tf index 110e92b0..651ba3d2 100644 --- a/infrastructure/cloud/modules/security/variables.tf +++ b/infrastructure/cloud/modules/security/variables.tf @@ -37,3 +37,8 @@ variable "iam_user_table_name" { description = "The BCGOV IAM User DynamoDb table name" type = string } + +variable "cert_domain_name" { + description = "The BCGov provisioned certificate domain name" + type = string +} diff --git a/infrastructure/cloud/modules/storage/rds.tf b/infrastructure/cloud/modules/storage/rds.tf new file mode 100644 index 00000000..9a990874 --- /dev/null +++ b/infrastructure/cloud/modules/storage/rds.tf @@ -0,0 +1,23 @@ +resource "aws_db_instance" "postgres_db_instance" { + allocated_storage = 20 + storage_type = "gp2" + engine = "postgres" + engine_version = "16.3" + instance_class = "db.t3.micro" + db_name = "${var.app_name}postgresdb${var.environment}" + username = var.db_username + password = var.db_password + parameter_group_name = "default.postgres16" + vpc_security_group_ids = [var.data_sg_id] + db_subnet_group_name = "default-${var.vpc_id}" + storage_encrypted = true + kms_key_id = var.kms_key_arn + ca_cert_identifier = var.rds_db_ca_cert + identifier = "${var.app_name}-postgres-db-${var.environment}" + skip_final_snapshot = true + backup_retention_period = 7 + performance_insights_enabled = true + performance_insights_kms_key_id = var.kms_key_arn + iam_database_authentication_enabled = true + deletion_protection = true +} diff --git a/infrastructure/cloud/modules/storage/variables.tf b/infrastructure/cloud/modules/storage/variables.tf index b84b2529..0d464bda 100644 --- a/infrastructure/cloud/modules/storage/variables.tf +++ b/infrastructure/cloud/modules/storage/variables.tf @@ -18,3 +18,35 @@ variable "environment" { description = "The AWS environment to deploy to" type = string } + +variable "data_sg_id" { + description = "The Data Security Group Id that will be used by database" + type = string +} + +variable "db_username" { + description = "Database username from Secrets Manager" + type = string + sensitive = true +} + +variable "db_password" { + description = "Database password from Secrets Manager" + type = string + sensitive = true +} + +variable "vpc_id" { + description = "The provisioned VPC ID" + type = string +} + +variable "kms_key_arn" { + description = "The custom KMS Key ARN" + type = string +} + +variable "rds_db_ca_cert" { + description = "The Certifiate Authority identifier used in RDS" + type = string +}