diff --git a/.gitleaksignore b/.gitleaksignore
index 374d0269..70ce96c8 100644
--- a/.gitleaksignore
+++ b/.gitleaksignore
@@ -1,6 +1,8 @@
# SEE: https://github.com/gitleaks/gitleaks/blob/master/README.md#gitleaksignore
-9469a5a10e20b5c3275ba055e65ba98e7d11e9d2:infrastructure/terraform/components/reporting/README.md:ipv4:16
-9469a5a10e20b5c3275ba055e65ba98e7d11e9d2:infrastructure/terraform/components/reporting/variables.tf:ipv4:109
+39565cc5ab1245e4e6a6368c19fd0aa9a187733a:infrastructure/terraform/components/reporting/README.md:ipv4:16
+39565cc5ab1245e4e6a6368c19fd0aa9a187733a:infrastructure/terraform/components/reporting/variables.tf:ipv4:109
ca243cb73d3804a14f3eeefa8073c96802420c52:infrastructure/terraform/etc/env_eu-west-2_int.tfvars:generic-api-key:29
ca243cb73d3804a14f3eeefa8073c96802420c52:infrastructure/terraform/etc/env_eu-west-2_prod.tfvars:generic-api-key:43
+d38af4e4f6c36ca9c3d843193b434386a9bad5ee:infrastructure/terraform/etc/env_eu-west-2_int.tfvars:generic-api-key:29
+d38af4e4f6c36ca9c3d843193b434386a9bad5ee:infrastructure/terraform/etc/env_eu-west-2_prod.tfvars:generic-api-key:43
diff --git a/docs/diagrams/reporting.drawio b/docs/diagrams/reporting.drawio
index 49c1c5ed..c62e304d 100644
--- a/docs/diagrams/reporting.drawio
+++ b/docs/diagrams/reporting.drawio
@@ -157,4 +157,4 @@
-
\ No newline at end of file
+
diff --git a/infrastructure/terraform/bin/test_mandatory_tfvars.sh b/infrastructure/terraform/bin/test_mandatory_tfvars.sh
index fc8c4845..db1c9dc0 100755
--- a/infrastructure/terraform/bin/test_mandatory_tfvars.sh
+++ b/infrastructure/terraform/bin/test_mandatory_tfvars.sh
@@ -81,6 +81,3 @@ for tfvars_file in ./etc/env_eu-west-2_*; do
fi
done
done
-
-
-
diff --git a/infrastructure/terraform/components/powerbi-gateway/.tool-versions b/infrastructure/terraform/components/powerbi-gateway/.tool-versions
new file mode 100644
index 00000000..3dd74c72
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/.tool-versions
@@ -0,0 +1 @@
+terraform 1.10.1
diff --git a/infrastructure/terraform/components/powerbi-gateway/README.md b/infrastructure/terraform/components/powerbi-gateway/README.md
new file mode 100644
index 00000000..a0afbf57
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/README.md
@@ -0,0 +1,44 @@
+
+
+
+
+## Requirements
+
+No requirements.
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [account\_ids](#input\_account\_ids) | All AWS Account IDs for this project | `map(string)` | `{}` | no |
+| [account\_name](#input\_account\_name) | The name of the AWS Account to deploy into (see globals.tfvars) | `string` | n/a | yes |
+| [athena\_driver\_url](#input\_athena\_driver\_url) | Amazon Athena ODBC MSI download URL for PowerBI gateway bootstrap | `string` | `"https://downloads.athena.us-east-1.amazonaws.com/drivers/ODBC/v2.1.0.0/Windows/AmazonAthenaODBC-2.1.0.0.msi"` | no |
+| [aws\_account\_id](#input\_aws\_account\_id) | The AWS Account ID (numeric) | `string` | n/a | yes |
+| [component](#input\_component) | The name of the component | `string` | `"powerbi-gateway"` | no |
+| [core\_account\_id](#input\_core\_account\_id) | The core account that contains the corresponding Glue Catalog | `string` | `1234567890` | no |
+| [core\_account\_ids](#input\_core\_account\_ids) | List of all corresponding core account id's that exist in the Non-Prod domain | `list(string)` | `[]` | no |
+| [default\_kms\_deletion\_window\_in\_days](#input\_default\_kms\_deletion\_window\_in\_days) | Default number of days to set KMS key deletion window | `number` | `14` | no |
+| [default\_tags](#input\_default\_tags) | A map of default tags to apply to all taggable resources within the component | `map(string)` | `{}` | no |
+| [enable\_powerbi\_gateway](#input\_enable\_powerbi\_gateway) | Deploy EC2 instance for PowerBI On-Premises Gateway | `bool` | `true` | no |
+| [enable\_spot](#input\_enable\_spot) | run Power BI On-Premises Gateway as spot instances | `bool` | `false` | no |
+| [environment](#input\_environment) | The name of the environment | `string` | n/a | yes |
+| [group](#input\_group) | The group variables are being inherited from (often synonmous with account short-name) | `string` | `"n/a"` | no |
+| [instance\_type](#input\_instance\_type) | The EC2 instance type. | `string` | `"t3.medium"` | no |
+| [module](#input\_module) | The variable encapsulating the name of this module | `string` | `"n/a"` | no |
+| [powerbi\_gateway\_instance\_count](#input\_powerbi\_gateway\_instance\_count) | Number of standalone Power BI On-Premises Gateway instances created directly from the launch template. | `number` | `2` | no |
+| [private\_subnet\_cidrs](#input\_private\_subnet\_cidrs) | List of CIDR blocks for private subnets. | `list(string)` | `[]` | no |
+| [project](#input\_project) | The name of the Project we are bootstrapping tfscaffold for | `string` | n/a | yes |
+| [public\_subnet\_cidrs](#input\_public\_subnet\_cidrs) | List of CIDR blocks for public subnets. | `list(string)` | `[]` | no |
+| [region](#input\_region) | The AWS Region | `string` | n/a | yes |
+| [root\_volume\_size](#input\_root\_volume\_size) | Size of root volume for the Power BI On-Premises Gateway instances - 30GB minimum for Windows Server | `number` | `80` | no |
+| [spot\_max\_price](#input\_spot\_max\_price) | max spot price for Power BI On-Premises Gateway instances | `string` | `"0.3"` | no |
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [powerbi\_gateway\_vpc](#module\_powerbi\_gateway\_vpc) | terraform-aws-modules/vpc/aws | 5.5.1 |
+## Outputs
+
+No outputs.
+
+
+
diff --git a/infrastructure/terraform/components/powerbi-gateway/cloudwatch_metric_alarm_patch_task_failed.tf b/infrastructure/terraform/components/powerbi-gateway/cloudwatch_metric_alarm_patch_task_failed.tf
new file mode 100644
index 00000000..bf230769
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/cloudwatch_metric_alarm_patch_task_failed.tf
@@ -0,0 +1,18 @@
+resource "aws_cloudwatch_metric_alarm" "patch_task_failed" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ alarm_name = "${local.csi}-patch-task-failed"
+ comparison_operator = "GreaterThanOrEqualToThreshold"
+ evaluation_periods = 1
+ metric_name = "FailedCommands"
+ namespace = "AWS/SSM-RunCommand"
+ period = 300
+ statistic = "Sum"
+ threshold = 1
+ alarm_description = "Alarm when the AWS-RunPatchBaseline maintenance window task reports a failed run"
+ treat_missing_data = "notBreaching"
+
+ dimensions = {
+ DocumentName = "AWS-RunPatchBaseline"
+ }
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/cloudwatch_metric_alarm_powerbi_gateway_standalone_status.tf b/infrastructure/terraform/components/powerbi-gateway/cloudwatch_metric_alarm_powerbi_gateway_standalone_status.tf
new file mode 100644
index 00000000..91d4e855
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/cloudwatch_metric_alarm_powerbi_gateway_standalone_status.tf
@@ -0,0 +1,25 @@
+resource "aws_cloudwatch_metric_alarm" "powerbi_gateway_standalone_status_check_failed" {
+ for_each = var.enable_powerbi_gateway ? {
+ for idx, instance in aws_instance.powerbi_gateway_standalone :
+ idx => {
+ id = instance.id
+ name = format("%s-standalone-%02d-status-check-failed", local.csi, idx + 1)
+ }
+ } : {}
+
+ alarm_name = each.value.name
+ comparison_operator = "GreaterThanOrEqualToThreshold"
+ evaluation_periods = 2
+ datapoints_to_alarm = 2
+ metric_name = "StatusCheckFailed"
+ namespace = "AWS/EC2"
+ period = 300
+ statistic = "Maximum"
+ threshold = 1
+ alarm_description = "Instance or system status check failed for a standalone Power BI gateway host"
+ treat_missing_data = "breaching"
+
+ dimensions = {
+ InstanceId = each.value.id
+ }
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/data_cloudinit_config_powerbi_gateway.tf b/infrastructure/terraform/components/powerbi-gateway/data_cloudinit_config_powerbi_gateway.tf
new file mode 100644
index 00000000..0d36a054
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/data_cloudinit_config_powerbi_gateway.tf
@@ -0,0 +1,11 @@
+data "cloudinit_config" "powerbi_gateway" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ gzip = false
+ base64_encode = true
+
+ part {
+ content_type = "text/cloud-config"
+ content = local.powerbi_gateway_script
+ }
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/ec2_instances_powerbi_gateway.tf b/infrastructure/terraform/components/powerbi-gateway/ec2_instances_powerbi_gateway.tf
new file mode 100644
index 00000000..aff7991f
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/ec2_instances_powerbi_gateway.tf
@@ -0,0 +1,14 @@
+resource "aws_instance" "powerbi_gateway_standalone" {
+ count = var.enable_powerbi_gateway ? var.powerbi_gateway_instance_count : 0
+
+ associate_public_ip_address = false
+ launch_template {
+ id = aws_launch_template.powerbi_gateway_standalone[0].id
+ version = "$Latest"
+ }
+
+ tags = {
+ "Name" = format("%s-standalone-%02d", local.csi, count.index + 1)
+ "Patch Group" = aws_ssm_patch_group.windows_patch_group[0].patch_group
+ }
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/iam_instance_profile_powerbi_gateway.tf b/infrastructure/terraform/components/powerbi-gateway/iam_instance_profile_powerbi_gateway.tf
new file mode 100644
index 00000000..4d340697
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/iam_instance_profile_powerbi_gateway.tf
@@ -0,0 +1,226 @@
+resource "aws_iam_instance_profile" "powerbi_gateway" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ name = local.csi
+ role = aws_iam_role.powerbi_gateway_role[0].name
+}
+
+data "aws_iam_policy_document" "powerbi_gateway_assume_role_policy" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ statement {
+ effect = "Allow"
+
+ principals {
+ type = "Service"
+ identifiers = ["ec2.amazonaws.com"]
+ }
+
+ actions = ["sts:AssumeRole"]
+ }
+}
+
+resource "aws_iam_role" "powerbi_gateway_role" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ name = local.csi
+ description = "PowerBI Gateway Instance Role"
+ path = "/"
+ assume_role_policy = data.aws_iam_policy_document.powerbi_gateway_assume_role_policy[0].json
+}
+
+
+resource "aws_iam_policy" "powerbi_gateway_permissions_policy" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ name = local.csi
+ description = "PowerBI Gateway Instance Permissions"
+ path = "/"
+ policy = data.aws_iam_policy_document.powerbi_gateway_permissions_policy[0].json
+}
+
+resource "aws_iam_role_policy_attachment" "powerbi_gateway_permissions_policy_attachment" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ role = aws_iam_role.powerbi_gateway_role[0].name
+ policy_arn = aws_iam_policy.powerbi_gateway_permissions_policy[0].arn
+}
+
+resource "aws_iam_role_policy_attachment" "powerbi_gateway_ssm_policy_attachment" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ role = aws_iam_role.powerbi_gateway_role[0].name
+ policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
+}
+
+data "aws_iam_policy_document" "powerbi_gateway_permissions_policy" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ statement {
+ sid = "AllowLogs"
+ effect = "Allow"
+
+ actions = [
+ "logs:CreateLogGroup",
+ "logs:CreateLogStream",
+ "logs:PutLogEvents",
+ ]
+
+ resources = [
+ "arn:aws:logs:${local.parameter_bundle.region}:${local.this_account}:log-group:*",
+ ]
+ }
+
+ statement {
+ sid = "AllowS3DataBucket"
+ effect = "Allow"
+
+ actions = [
+ "s3:GetBucketLocation",
+ "s3:GetObject",
+ "s3:ListBucket"
+ ]
+
+ resources = [
+ local.reporting.powerbi_data_bucket_arn,
+ "${local.reporting.powerbi_data_bucket_arn}/*"
+ ]
+ }
+
+ statement {
+ sid = "AllowS3ResultsBucket"
+ effect = "Allow"
+
+ actions = [
+ "s3:GetBucketLocation",
+ "s3:GetObject",
+ "s3:ListBucket",
+ "s3:PutObject"
+ ]
+
+ resources = [
+ local.reporting.powerbi_results_bucket_arn,
+ "${local.reporting.powerbi_results_bucket_arn}/*"
+ ]
+ }
+
+ statement {
+ sid = "AllowAthenaAccess1"
+ effect = "Allow"
+
+ actions = [
+ "athena:GetQueryResults",
+ "athena:GetQueryResultsStream",
+ "athena:GetQueryExecution",
+ "athena:StartQueryExecution",
+ "athena:GetWorkGroup",
+ "athena:GetNamedQuery"
+ ]
+
+ resources = [
+ local.reporting.powerbi_user_workgroup_arn
+ ]
+ }
+
+ statement {
+ sid = "AllowAthenaAccess2"
+ effect = "Allow"
+
+ actions = [
+ "athena:GetDatabase",
+ "athena:GetTableMetadata",
+ "athena:GetDataCatalog",
+ "athena:GetTable"
+ ]
+
+ resources = [
+ "arn:aws:athena:${var.region}:${local.this_account}:datacatalog/AWSDataCatalog"
+ ]
+ }
+
+ statement {
+ sid = "AllowAthenaAccess3"
+ effect = "Allow"
+
+ actions = [
+ "athena:ListDataCatalogs",
+ "athena:ListDatabases",
+ "athena:ListTableMetadata",
+ "athena:ListWorkGroups"
+ ]
+
+ resources = ["*"] # Access to List all above is required. Condition keys not supported for these resources.
+ }
+
+ statement {
+ sid = "AllowGlueAccess"
+ effect = "Allow"
+
+ actions = [
+ "glue:GetTable",
+ "glue:GetTables",
+ "glue:BatchGetTable",
+ "glue:GetDatabase",
+ "glue:GetDatabases",
+ "glue:GetPartition",
+ "glue:GetPartitions"
+ ]
+
+ resources = concat(
+ local.core_glue_catalog_resources, # Access to all core account catalogs is required as they are all accessible via the default catalog in the environment's account
+ [
+ local.reporting.powerbi_reporting_database_arn,
+ "arn:aws:glue:${var.region}:${var.core_account_id}:catalog",
+ "arn:aws:glue:${var.region}:${local.this_account}:catalog",
+ # Tables
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_plan_completed_summary",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_plan_completed_summary_batch",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_plan_status",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_status",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_status_summary",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_status_summary_batch",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/client_latest_name",
+ # Views
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_plan_completed_summary_all",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_status_summary_all",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_status_summary_all_email_filter",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_status_smsnudge_staging",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_plan_status_smsnudge",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_plan_read_status_smsnudge",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/request_item_status_smsnudge",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/dates",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/letters_invoice_units_monthly",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/letters_invoice_units_weekly",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/latency_percentiles",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/daily_recipient_count",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/daily_recipient_distribution",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/raw_latency_3m",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/delivered_messages",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/monthly_app_recipients_distribution",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/monthly_app_recipients_multiple_clients",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/monthly_messages_per_recipient",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/monthly_recipient_with_more_than_five_messages",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/monthly_recipients_distribution",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/monthly_recipients_distribution_by_integrator",
+ "arn:aws:glue:${var.region}:${local.this_account}:table/${local.reporting.powerbi_reporting_database_name}/billing_transactions",
+ ]
+ )
+ }
+
+ statement {
+ sid = "AllowS3KMSAccess"
+ effect = "Allow"
+
+ actions = [
+ "kms:Decrypt",
+ "kms:Encrypt",
+ "kms:GenerateDataKey",
+ "kms:GenerateDataKeyWithoutPlaintext",
+ "kms:DescribeKey"
+ ]
+
+ resources = [
+ local.reporting.powerbi_s3_kms_key_arn
+ ]
+ }
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/kms_key_ebs.tf b/infrastructure/terraform/components/powerbi-gateway/kms_key_ebs.tf
new file mode 100644
index 00000000..1e72bb7c
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/kms_key_ebs.tf
@@ -0,0 +1,107 @@
+resource "aws_kms_key" "ebs" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ description = "CMK for encrypting EBS volumes"
+ deletion_window_in_days = local.parameter_bundle.default_kms_deletion_window_in_days
+ enable_key_rotation = true
+ policy = data.aws_iam_policy_document.ebs[0].json
+}
+
+resource "aws_kms_alias" "ebs" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ name = "alias/${local.csi}-ebs"
+ target_key_id = aws_kms_key.ebs[0].key_id
+}
+
+
+data "aws_iam_policy_document" "ebs" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ statement {
+ sid = "AllowLocalIAMAdministration"
+ effect = "Allow"
+
+ actions = [
+ "kms:Create*",
+ "kms:Describe*",
+ "kms:Enable*",
+ "kms:List*",
+ "kms:Put*",
+ "kms:Update*",
+ "kms:Revoke*",
+ "kms:Disable*",
+ "kms:Get*",
+ "kms:Delete*",
+ "kms:ScheduleKeyDeletion",
+ "kms:CancelKeyDeletion",
+ "kms:Decrypt",
+ "kms:Encrypt",
+ "kms:GenerateDataKey",
+ "kms:TagResource"
+ ]
+
+ resources = [
+ "*",
+ ]
+
+ principals {
+ type = "AWS"
+ identifiers = [
+ local.parameter_bundle.iam_resource_arns.any_authorised_user_in_this_account,
+
+ ]
+ }
+ }
+
+ statement {
+ sid = "AllowUsageAccess"
+ effect = "Allow"
+
+ actions = [
+ "kms:Decrypt",
+ "kms:DescribeKey",
+ "kms:Encrypt",
+ "kms:GenerateDataKey*",
+ "kms:ReEncrypt*",
+ ]
+
+ resources = [
+ "*",
+ ]
+
+ principals {
+ type = "AWS"
+ identifiers = [
+ local.parameter_bundle.iam_resource_arns.any_authorised_user_in_this_account,
+ "arn:aws:iam::${local.this_account}:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling"
+
+ ]
+ }
+ }
+ statement {
+ sid = "AllowAutoscalingToUse"
+ effect = "Allow"
+
+ actions = [
+ "kms:CreateGrant",
+ ]
+
+ resources = [
+ "*",
+ ]
+
+ principals {
+ type = "AWS"
+ identifiers = [
+ "arn:aws:iam::${local.this_account}:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling",
+ ]
+ }
+
+ condition {
+ test = "Bool"
+ variable = "kms:GrantIsForAWSResource"
+ values = ["true"]
+ }
+ }
+}
diff --git a/infrastructure/terraform/components/reporting/launch_template_powerbi_gateway_asg.tf b/infrastructure/terraform/components/powerbi-gateway/launch_template_powerbi_gateway_standalone.tf
similarity index 84%
rename from infrastructure/terraform/components/reporting/launch_template_powerbi_gateway_asg.tf
rename to infrastructure/terraform/components/powerbi-gateway/launch_template_powerbi_gateway_standalone.tf
index 00c0a657..8a5946e2 100644
--- a/infrastructure/terraform/components/reporting/launch_template_powerbi_gateway_asg.tf
+++ b/infrastructure/terraform/components/powerbi-gateway/launch_template_powerbi_gateway_standalone.tf
@@ -1,10 +1,10 @@
-resource "aws_launch_template" "powerbi_gateway" {
+resource "aws_launch_template" "powerbi_gateway_standalone" {
count = var.enable_powerbi_gateway ? 1 : 0
- name = local.csi
- description = "Template for the Power BI On-Premises Gateway"
+ name = "${local.csi}-standalone"
+ description = "Template for the Power BI On-Premises Gateway (standalone instances)"
update_default_version = true
- image_id = "resolve:ssm:/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base"
+ image_id = "resolve:ssm:/aws/service/ami-windows-latest/Windows_Server-2025-English-Full-Base"
instance_type = var.instance_type
user_data = data.cloudinit_config.powerbi_gateway[0].rendered
instance_initiated_shutdown_behavior = var.enable_spot ? "terminate" : "stop"
@@ -57,11 +57,7 @@ resource "aws_launch_template" "powerbi_gateway" {
tag_specifications {
resource_type = "instance"
- tags = merge(local.deployment_default_tags,
- {
- "Patch Group" = "${local.csi}-windows-group"
- }
- )
+ tags = local.deployment_default_tags
}
tag_specifications {
diff --git a/infrastructure/terraform/components/powerbi-gateway/locals.tf b/infrastructure/terraform/components/powerbi-gateway/locals.tf
new file mode 100644
index 00000000..8289c8b4
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/locals.tf
@@ -0,0 +1,68 @@
+locals {
+ # Compound Scope Identifier
+ csi = replace(
+ format(
+ "%s-%s-%s",
+ var.project,
+ var.environment,
+ var.component,
+ ),
+ "_",
+ ""
+ )
+
+ base_parameter_bundle = {
+ project = var.project
+ environment = var.environment
+ component = var.component
+ group = var.group
+ region = var.region
+ account_ids = var.account_ids
+ account_name = var.account_name
+ default_kms_deletion_window_in_days = var.default_kms_deletion_window_in_days
+ default_tags = local.deployment_default_tags
+ }
+
+ parameter_bundle = merge(
+ local.base_parameter_bundle, {
+ iam_resource_arns = {
+ any_authorised_user_in_this_account = "arn:aws:iam::${local.this_account}:root"
+ },
+ }
+ )
+
+ deployment_default_tags = {
+ AccountId = var.account_ids[var.account_name]
+ AccountName = var.account_name
+ Project = var.project
+ Environment = var.environment
+ Component = var.component
+ Group = var.group
+ Module = var.module
+ }
+
+ this_account = local.base_parameter_bundle.account_ids[local.base_parameter_bundle.account_name]
+
+ # Create the powerbi_gateway_script only if var.enable_powerbi_gateway is true
+ powerbi_gateway_script = var.enable_powerbi_gateway ? templatefile("${path.module}/templates/cloudinit_config.tmpl", {
+ odbc_dsn_name = "${local.csi}-dsn"
+ odbc_description = "AWS Simba Athena ODBC Connection for ${local.csi}"
+ athena_driver_url = var.athena_driver_url
+ region = var.region
+ catalog = "AWSDataCatalog"
+ database = data.terraform_remote_state.reporting.outputs.powerbi_reporting_database_name
+ workgroup = data.terraform_remote_state.reporting.outputs.powerbi_user_workgroup_name
+ authentication_type = "Instance Profile"
+ gateway_name = "${local.csi}-gateway"
+ }) : null
+
+ use_core_glue_catalog_resources = length(var.core_account_ids) > 0 ? true : false
+
+ core_glue_catalog_resources = local.use_core_glue_catalog_resources ? flatten([
+ for account_id in var.core_account_ids : [
+ "arn:aws:glue:${var.region}:${account_id}:catalog",
+ ]
+ ]) : []
+
+ reporting = data.terraform_remote_state.reporting.outputs
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/locals_reporting_remote_state.tf b/infrastructure/terraform/components/powerbi-gateway/locals_reporting_remote_state.tf
new file mode 100644
index 00000000..44710645
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/locals_reporting_remote_state.tf
@@ -0,0 +1,16 @@
+data "terraform_remote_state" "reporting" {
+ backend = "s3"
+
+ config = {
+ bucket = local.terraform_state_bucket
+ key = format(
+ "%s/%s/%s/%s/%s.tfstate",
+ var.project,
+ var.aws_account_id,
+ var.region,
+ var.environment,
+ "reporting"
+ )
+ region = var.region
+ }
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/locals_tfscaffold.tf b/infrastructure/terraform/components/powerbi-gateway/locals_tfscaffold.tf
new file mode 100644
index 00000000..bf93f3a6
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/locals_tfscaffold.tf
@@ -0,0 +1,27 @@
+locals {
+ terraform_state_bucket = format(
+ "%s-tfscaffold-%s-%s",
+ var.project,
+ var.aws_account_id,
+ var.region,
+ )
+
+ # Central account component deployed with "nhs" project name, not "nhs-notify" for legacy reasons.
+ terraform_state_bucket_acct = format(
+ "%s-tfscaffold-%s-%s",
+ "nhs",
+ var.aws_account_id,
+ var.region,
+ )
+
+ default_tags = merge(
+ var.default_tags,
+ {
+ Project = var.project
+ Environment = var.environment
+ Component = var.component
+ Group = var.group
+ Name = local.csi
+ },
+ )
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/module_powerbi_gateway_vpc.tf b/infrastructure/terraform/components/powerbi-gateway/module_powerbi_gateway_vpc.tf
new file mode 100644
index 00000000..55939f92
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/module_powerbi_gateway_vpc.tf
@@ -0,0 +1,67 @@
+module "powerbi_gateway_vpc" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ source = "terraform-aws-modules/vpc/aws"
+ version = "5.5.1" # Adjust to the latest version
+
+ create_vpc = var.enable_powerbi_gateway
+
+ name = "${local.csi}-vpc"
+ cidr = "10.0.0.0/16"
+
+ azs = data.aws_availability_zones.available[0].names
+ public_subnets = var.public_subnet_cidrs
+ private_subnets = var.private_subnet_cidrs
+
+ enable_nat_gateway = true
+ single_nat_gateway = false
+ enable_dns_support = true
+ enable_dns_hostnames = true
+ create_igw = true
+}
+
+data "aws_availability_zones" "available" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ state = "available"
+}
+
+#trivy:ignore:aws-ec2-no-public-egress-sgr
+resource "aws_security_group" "powerbi_gateway" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ name = "${local.csi}-security-group"
+ vpc_id = module.powerbi_gateway_vpc[0].vpc_id
+
+ egress {
+ from_port = 80
+ to_port = 80
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ egress {
+ from_port = 443
+ to_port = 443
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ egress {
+ from_port = 5671
+ to_port = 5672
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ egress {
+ from_port = 9350
+ to_port = 9354
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ tags = {
+ Name = "${local.csi}-sg"
+ }
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/provider.tf b/infrastructure/terraform/components/powerbi-gateway/provider.tf
new file mode 100644
index 00000000..e5b9a49e
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/provider.tf
@@ -0,0 +1,23 @@
+###
+# Default provider, used for all deployments to eu-west-2
+# If no provider name is supplied for a resource then it uses this one
+###
+provider "aws" {
+ region = var.region
+
+ default_tags {
+ tags = local.deployment_default_tags
+ }
+}
+
+###
+# Provider specifically for deploying to us-east-1, e.g. for cloudfront, ACM, etc.
+###
+provider "aws" {
+ alias = "us-east-1"
+ region = "us-east-1"
+
+ default_tags {
+ tags = local.deployment_default_tags
+ }
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/ssm_maintenance_window_patch_window.tf b/infrastructure/terraform/components/powerbi-gateway/ssm_maintenance_window_patch_window.tf
new file mode 100644
index 00000000..e5f35a5e
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/ssm_maintenance_window_patch_window.tf
@@ -0,0 +1,21 @@
+resource "aws_ssm_maintenance_window" "patch_window_sunday" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ name = "${local.csi}-windows-patch-window-sun"
+ description = "Windows Server 2025 Sunday Patch Window"
+ schedule = "cron(0 3 ? * SUN *)" # Every Sunday at 3 AM
+ duration = 4
+ cutoff = 1
+ allow_unassociated_targets = true
+}
+
+resource "aws_ssm_maintenance_window" "patch_window_wednesday" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ name = "${local.csi}-windows-patch-window-wed"
+ description = "Windows Server 2025 Wednesday Patch Window"
+ schedule = "cron(0 3 ? * WED *)" # Every Wednesday at 3 AM
+ duration = 4
+ cutoff = 1
+ allow_unassociated_targets = true
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/ssm_maintenance_window_target_windows_instances.tf b/infrastructure/terraform/components/powerbi-gateway/ssm_maintenance_window_target_windows_instances.tf
new file mode 100644
index 00000000..d4193bc1
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/ssm_maintenance_window_target_windows_instances.tf
@@ -0,0 +1,27 @@
+resource "aws_ssm_maintenance_window_target" "windows_instances_sunday" {
+ count = var.enable_powerbi_gateway && var.powerbi_gateway_instance_count >= 1 ? 1 : 0
+
+ description = "Windows Server 2025 Sunday Maintenance Window Target "
+ window_id = aws_ssm_maintenance_window.patch_window_sunday[0].id
+ resource_type = "INSTANCE"
+ name = "${local.csi}-maintenance-window-target-sun"
+
+ targets {
+ key = "InstanceIds"
+ values = [aws_instance.powerbi_gateway_standalone[0].id]
+ }
+}
+
+resource "aws_ssm_maintenance_window_target" "windows_instances_wednesday" {
+ count = var.enable_powerbi_gateway && var.powerbi_gateway_instance_count >= 2 ? 1 : 0
+
+ description = "Windows Server 2025 Wednesday Maintenance Window Target"
+ window_id = aws_ssm_maintenance_window.patch_window_wednesday[0].id
+ resource_type = "INSTANCE"
+ name = "${local.csi}-maintenance-window-target-wed"
+
+ targets {
+ key = "InstanceIds"
+ values = [aws_instance.powerbi_gateway_standalone[1].id]
+ }
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/ssm_maintenance_window_task_patch_task.tf b/infrastructure/terraform/components/powerbi-gateway/ssm_maintenance_window_task_patch_task.tf
new file mode 100644
index 00000000..3e0e5aa5
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/ssm_maintenance_window_task_patch_task.tf
@@ -0,0 +1,63 @@
+resource "aws_ssm_maintenance_window_task" "patch_task_sunday" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ description = "Windows Server 2025 Sunday Patch Task"
+ window_id = aws_ssm_maintenance_window.patch_window_sunday[0].id
+ task_arn = "AWS-RunPatchBaseline"
+ task_type = "RUN_COMMAND"
+
+ targets {
+ key = "WindowTargetIds"
+ values = [aws_ssm_maintenance_window_target.windows_instances_sunday[0].id]
+ }
+
+ task_invocation_parameters {
+ run_command_parameters {
+ comment = "Patching Sunday instances"
+ parameter {
+ name = "Operation"
+ values = ["Install"]
+ }
+ parameter {
+ name = "RebootOption"
+ values = ["RebootIfNeeded"]
+ }
+ }
+ }
+
+ priority = 1
+ max_concurrency = "1"
+ max_errors = "1"
+}
+
+resource "aws_ssm_maintenance_window_task" "patch_task_wednesday" {
+ count = var.enable_powerbi_gateway && var.powerbi_gateway_instance_count >= 2 ? 1 : 0
+
+ description = "Windows Server 2025 Wednesday Patch Task"
+ window_id = aws_ssm_maintenance_window.patch_window_wednesday[0].id
+ task_arn = "AWS-RunPatchBaseline"
+ task_type = "RUN_COMMAND"
+
+ targets {
+ key = "WindowTargetIds"
+ values = [aws_ssm_maintenance_window_target.windows_instances_wednesday[0].id]
+ }
+
+ task_invocation_parameters {
+ run_command_parameters {
+ comment = "Patching Wednesday instance"
+ parameter {
+ name = "Operation"
+ values = ["Install"]
+ }
+ parameter {
+ name = "RebootOption"
+ values = ["RebootIfNeeded"]
+ }
+ }
+ }
+
+ priority = 1
+ max_concurrency = "1"
+ max_errors = "1"
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/ssm_parameter_powerbi_gateway_recovery_key.tf b/infrastructure/terraform/components/powerbi-gateway/ssm_parameter_powerbi_gateway_recovery_key.tf
new file mode 100644
index 00000000..3a01dc64
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/ssm_parameter_powerbi_gateway_recovery_key.tf
@@ -0,0 +1,14 @@
+resource "aws_ssm_parameter" "powerbi_gateway_recovery_key" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ name = "/${local.csi}/gateway-recovery-key"
+ description = "The Recovery Key for the On-Premises Gateway - Updated manually with the actual key value after deployment"
+ type = "SecureString"
+ value = "RECOVERY_KEY_PLACEHOLDER"
+
+ lifecycle {
+ ignore_changes = [
+ value,
+ ]
+ }
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/ssm_patch_baseline_windows_patch_baseline.tf b/infrastructure/terraform/components/powerbi-gateway/ssm_patch_baseline_windows_patch_baseline.tf
new file mode 100644
index 00000000..59a8fd1c
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/ssm_patch_baseline_windows_patch_baseline.tf
@@ -0,0 +1,24 @@
+resource "aws_ssm_patch_baseline" "windows_patch_baseline" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ name = "${local.csi}-windows-patch-baseline"
+ description = "Windows Server 2025 Patch Baseline"
+ operating_system = "WINDOWS"
+ approval_rule {
+ patch_filter {
+ key = "PRODUCT"
+ values = ["WindowsServer2025"]
+ }
+ patch_filter {
+ key = "CLASSIFICATION"
+ values = ["SecurityUpdates", "CriticalUpdates"]
+ }
+ patch_filter {
+ key = "MSRC_SEVERITY"
+ values = [
+ "Critical",
+ "Important",
+ ]
+ }
+ }
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/ssm_patch_group_windows_patch_group.tf b/infrastructure/terraform/components/powerbi-gateway/ssm_patch_group_windows_patch_group.tf
new file mode 100644
index 00000000..d316a3cf
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/ssm_patch_group_windows_patch_group.tf
@@ -0,0 +1,6 @@
+resource "aws_ssm_patch_group" "windows_patch_group" {
+ count = var.enable_powerbi_gateway ? 1 : 0
+
+ baseline_id = aws_ssm_patch_baseline.windows_patch_baseline[0].id
+ patch_group = "${local.csi}-windows-group"
+}
diff --git a/infrastructure/terraform/components/powerbi-gateway/templates/cloudinit_config.tmpl b/infrastructure/terraform/components/powerbi-gateway/templates/cloudinit_config.tmpl
new file mode 100644
index 00000000..399b8d98
--- /dev/null
+++ b/infrastructure/terraform/components/powerbi-gateway/templates/cloudinit_config.tmpl
@@ -0,0 +1,117 @@
+
[| no | | [cloudtrail\_log\_group\_name](#input\_cloudtrail\_log\_group\_name) | The name of the Cloudtrail log group name on the account (see globals.tfvars) | `string` | n/a | yes | @@ -23,10 +24,9 @@ No requirements. | [core\_env](#input\_core\_env) | The core environment that contains the corresponding Glue table/S3 buckets etc. | `string` | `"internal-dev"` | no | | [default\_kms\_deletion\_window\_in\_days](#input\_default\_kms\_deletion\_window\_in\_days) | Default number of days to set KMS key deletion window | `number` | `14` | no | | [default\_tags](#input\_default\_tags) | A map of default tags to apply to all taggable resources within the component | `map(string)` | `{}` | no | -| [desired\_capacity](#input\_desired\_capacity) | The desired number of instances in the Power BI On-Premises Gateway Auto Scaling group. | `number` | `1` | no | | [destination\_backup\_vault\_arn](#input\_destination\_backup\_vault\_arn) | ARN of the destination backup vault to copy periodic backups to | `string` | `""` | no | | [email\_filter\_client\_ids](#input\_email\_filter\_client\_ids) | List of client ids that need email-only sending groups to be hidden in certain views | `list(string)` |
"NULL"
]
[| no | -| [enable\_powerbi\_gateway](#input\_enable\_powerbi\_gateway) | Deploy EC2 instance for PowerBI On-Premises Gateway | `bool` | `true` | no | +| [enable\_powerbi\_gateway](#input\_enable\_powerbi\_gateway) | Deploy supporting resources for the legacy PowerBI On-Premises Gateway | `bool` | `true` | no | | [enable\_s3\_backup](#input\_enable\_s3\_backup) | Enable AWS S3 Backup of the data bucket | `bool` | `true` | no | | [enable\_spot](#input\_enable\_spot) | run Power BI On-Premises Gateway as spot instances | `bool` | `false` | no | | [enable\_vault\_lock\_configuration](#input\_enable\_vault\_lock\_configuration) | Enable vault lock, preventing the deletion of a vault that contains 1 or more Recovery Points | `bool` | `false` | no | @@ -34,8 +34,6 @@ No requirements. | [group](#input\_group) | The group variables are being inherited from (often synonmous with account short-name) | `string` | `"n/a"` | no | | [instance\_type](#input\_instance\_type) | The EC2 instance type. | `string` | `"t3.medium"` | no | | [log\_retention\_days](#input\_log\_retention\_days) | How many days to retain the logs generated by the step function | `number` | `30` | no | -| [max\_size](#input\_max\_size) | The maximum number of instances in the Power BI On-Premises Gateway Auto Scaling group. | `number` | `1` | no | -| [min\_size](#input\_min\_size) | The minimum number of instances in the Power BI On-Premises Gateway Auto Scaling group. | `number` | `1` | no | | [module](#input\_module) | The variable encapsulating the name of this module | `string` | `"n/a"` | no | | [parent\_acct\_environment](#input\_parent\_acct\_environment) | Name of the environment responsible for the acct resources used, affects things like DNS zone. Useful for named dev environments | `string` | `"main"` | no | | [periodic\_s3backup\_copy\_retention\_days](#input\_periodic\_s3backup\_copy\_retention\_days) | number of days to retain weekly s3 backups in the destination vault | `number` | `31` | no | @@ -47,8 +45,6 @@ No requirements. | [public\_subnet\_cidrs](#input\_public\_subnet\_cidrs) | List of CIDR blocks for public subnets. | `list(string)` | `[]` | no | | [region](#input\_region) | The AWS Region | `string` | n/a | yes | | [root\_volume\_size](#input\_root\_volume\_size) | Size of root volume for the Power BI On-Premises Gateway instances - 30GB minimum for Windows Server | `number` | `80` | no | -| [scale\_in\_recurrence\_schedule](#input\_scale\_in\_recurrence\_schedule) | The cron expression for the scale in schedule. Set to null if no recurrence is needed. | `string` | `null` | no | -| [scale\_out\_recurrence\_schedule](#input\_scale\_out\_recurrence\_schedule) | The cron expression for the scale out schedule. Set to null if no recurrence is needed. | `string` | `null` | no | | [shared\_infra\_account\_id](#input\_shared\_infra\_account\_id) | The AWS Account ID of the shared infrastructure account | `string` | `"000000000000"` | no | | [sms\_nudge\_client\_id](#input\_sms\_nudge\_client\_id) | Client id for the SMS Nudge umbrella client used to filter smsnudge views | `string` | `"NULL"` | no | | [spot\_max\_price](#input\_spot\_max\_price) | max spot price for Power BI On-Premises Gateway instances | `string` | `"0.3"` | no | @@ -60,7 +56,15 @@ No requirements. | [powerbi\_gateway\_vpc](#module\_powerbi\_gateway\_vpc) | terraform-aws-modules/vpc/aws | 5.5.1 | ## Outputs -No outputs. +| Name | Description | +|------|-------------| +| [powerbi\_data\_bucket\_arn](#output\_powerbi\_data\_bucket\_arn) | Data bucket ARN used by the Power BI gateway component | +| [powerbi\_reporting\_database\_arn](#output\_powerbi\_reporting\_database\_arn) | Glue reporting database ARN for Power BI gateway access policy | +| [powerbi\_reporting\_database\_name](#output\_powerbi\_reporting\_database\_name) | Glue reporting database name for Power BI gateway access policy | +| [powerbi\_results\_bucket\_arn](#output\_powerbi\_results\_bucket\_arn) | Results bucket ARN used by the Power BI gateway component | +| [powerbi\_s3\_kms\_key\_arn](#output\_powerbi\_s3\_kms\_key\_arn) | KMS key ARN for S3 data encryption used by Power BI gateway access policy | +| [powerbi\_user\_workgroup\_arn](#output\_powerbi\_user\_workgroup\_arn) | Athena user workgroup ARN used by the Power BI gateway access policy | +| [powerbi\_user\_workgroup\_name](#output\_powerbi\_user\_workgroup\_name) | Athena user workgroup name used by the Power BI gateway bootstrap | diff --git a/infrastructure/terraform/components/reporting/autoscaling_group_powerbi_gateway.tf b/infrastructure/terraform/components/reporting/autoscaling_group_powerbi_gateway.tf deleted file mode 100644 index 5e7c6872..00000000 --- a/infrastructure/terraform/components/reporting/autoscaling_group_powerbi_gateway.tf +++ /dev/null @@ -1,25 +0,0 @@ -resource "aws_autoscaling_group" "powerbi_gateway" { - count = var.enable_powerbi_gateway ? 1 : 0 - - name = local.csi - - launch_template { - id = aws_launch_template.powerbi_gateway[0].id - version = "$Latest" - } - - vpc_zone_identifier = module.powerbi_gateway_vpc[0].private_subnets - desired_capacity = var.desired_capacity - min_size = var.min_size - max_size = var.max_size - - tag { - key = "Name" - value = "${local.csi}-powerbi-gateway-instance" - propagate_at_launch = true - } - - health_check_type = "EC2" - health_check_grace_period = 300 - wait_for_capacity_timeout = "0" -} diff --git a/infrastructure/terraform/components/reporting/autoscaling_schedule_scale_in.tf b/infrastructure/terraform/components/reporting/autoscaling_schedule_scale_in.tf deleted file mode 100644 index 57d330a2..00000000 --- a/infrastructure/terraform/components/reporting/autoscaling_schedule_scale_in.tf +++ /dev/null @@ -1,11 +0,0 @@ -resource "aws_autoscaling_schedule" "scale_in" { - count = var.enable_powerbi_gateway && var.scale_in_recurrence_schedule != null ? 1 : 0 - - scheduled_action_name = "${local.csi}-scale-in" - desired_capacity = 0 - min_size = 0 - max_size = -1 - autoscaling_group_name = aws_autoscaling_group.powerbi_gateway[0].name - - recurrence = coalesce(var.scale_in_recurrence_schedule, null) -} diff --git a/infrastructure/terraform/components/reporting/autoscaling_schedule_scale_out.tf b/infrastructure/terraform/components/reporting/autoscaling_schedule_scale_out.tf deleted file mode 100644 index 50c71f55..00000000 --- a/infrastructure/terraform/components/reporting/autoscaling_schedule_scale_out.tf +++ /dev/null @@ -1,11 +0,0 @@ -resource "aws_autoscaling_schedule" "scale_out" { - count = var.enable_powerbi_gateway && var.scale_out_recurrence_schedule != null ? 1 : 0 - - scheduled_action_name = "${local.csi}-scale-out" - desired_capacity = var.desired_capacity - min_size = var.min_size - max_size = var.max_size - autoscaling_group_name = aws_autoscaling_group.powerbi_gateway[0].name - - recurrence = coalesce(var.scale_in_recurrence_schedule, null) -} diff --git a/infrastructure/terraform/components/reporting/iam_instance_profile_powerbi_gateway.tf b/infrastructure/terraform/components/reporting/iam_instance_profile_powerbi_gateway.tf index 53b16b72..e2a1efee 100644 --- a/infrastructure/terraform/components/reporting/iam_instance_profile_powerbi_gateway.tf +++ b/infrastructure/terraform/components/reporting/iam_instance_profile_powerbi_gateway.tf @@ -223,22 +223,4 @@ data "aws_iam_policy_document" "powerbi_gateway_permissions_policy" { aws_kms_key.s3.arn ] } - - statement { - sid = "AllowSSMAccess" - effect = "Allow" - - actions = [ - "ssm:GetParameter", - "ssm:GetParameters", - "ssm:GetParameterHistory", - ] - - resources = [ - aws_ssm_parameter.powerbi_gateway_recovery_key[0].arn, - aws_ssm_parameter.powerbi_gateway_client_id[0].arn, - aws_ssm_parameter.powerbi_gateway_client_secret[0].arn, - aws_ssm_parameter.powerbi_gateway_tenant_id[0].arn - ] - } } diff --git a/infrastructure/terraform/components/reporting/locals.tf b/infrastructure/terraform/components/reporting/locals.tf index 3d4c48ab..761faa69 100644 --- a/infrastructure/terraform/components/reporting/locals.tf +++ b/infrastructure/terraform/components/reporting/locals.tf @@ -54,26 +54,17 @@ locals { this_account = local.base_parameter_bundle.account_ids[local.base_parameter_bundle.account_name] - # Check if each required SSM parameter exists individually - recovery_key = length(aws_ssm_parameter.powerbi_gateway_recovery_key) > 0 ? aws_ssm_parameter.powerbi_gateway_recovery_key[0].name : null - client_secret = length(aws_ssm_parameter.powerbi_gateway_client_secret) > 0 ? aws_ssm_parameter.powerbi_gateway_client_secret[0].name : null - client_id = length(aws_ssm_parameter.powerbi_gateway_client_id) > 0 ? aws_ssm_parameter.powerbi_gateway_client_id[0].name : null - tenant_id = length(aws_ssm_parameter.powerbi_gateway_tenant_id) > 0 ? aws_ssm_parameter.powerbi_gateway_tenant_id[0].name : null - # Create the powerbi_gateway_script only if var.enable_powerbi_gateway is true powerbi_gateway_script = var.enable_powerbi_gateway ? templatefile("${path.module}/templates/cloudinit_config.tmpl", { odbc_dsn_name = "${local.csi}-dsn" odbc_description = "AWS Simba Athena ODBC Connection for ${local.csi}" + athena_driver_url = var.athena_driver_url region = var.region catalog = "AWSDataCatalog" database = aws_glue_catalog_database.reporting.name workgroup = aws_athena_workgroup.user.name authentication_type = "Instance Profile" gateway_name = "${local.csi}-gateway" - recovery_key = local.recovery_key - client_secret = local.client_secret - client_id = local.client_id - tenant_id = local.tenant_id }) : null use_core_glue_catalog_resources = length(var.core_account_ids) > 0 ? true : false diff --git a/infrastructure/terraform/components/reporting/outputs.tf b/infrastructure/terraform/components/reporting/outputs.tf index 9dcc2f39..7dfcbf14 100644 --- a/infrastructure/terraform/components/reporting/outputs.tf +++ b/infrastructure/terraform/components/reporting/outputs.tf @@ -1 +1,36 @@ # Define the outputs for the component. The outputs may well be referenced by other component in the same or different environments using terraform_remote_state data sources... + +output "powerbi_data_bucket_arn" { + description = "Data bucket ARN used by the Power BI gateway component" + value = aws_s3_bucket.data.arn +} + +output "powerbi_results_bucket_arn" { + description = "Results bucket ARN used by the Power BI gateway component" + value = aws_s3_bucket.results.arn +} + +output "powerbi_reporting_database_name" { + description = "Glue reporting database name for Power BI gateway access policy" + value = aws_glue_catalog_database.reporting.name +} + +output "powerbi_reporting_database_arn" { + description = "Glue reporting database ARN for Power BI gateway access policy" + value = aws_glue_catalog_database.reporting.arn +} + +output "powerbi_user_workgroup_name" { + description = "Athena user workgroup name used by the Power BI gateway bootstrap" + value = aws_athena_workgroup.user.name +} + +output "powerbi_user_workgroup_arn" { + description = "Athena user workgroup ARN used by the Power BI gateway access policy" + value = aws_athena_workgroup.user.arn +} + +output "powerbi_s3_kms_key_arn" { + description = "KMS key ARN for S3 data encryption used by Power BI gateway access policy" + value = aws_kms_key.s3.arn +} diff --git a/infrastructure/terraform/components/reporting/s3_bucket_access_logs.tf b/infrastructure/terraform/components/reporting/s3_bucket_access_logs.tf index 631f7c24..764c76f6 100644 --- a/infrastructure/terraform/components/reporting/s3_bucket_access_logs.tf +++ b/infrastructure/terraform/components/reporting/s3_bucket_access_logs.tf @@ -146,5 +146,3 @@ resource "aws_s3_bucket_lifecycle_configuration" "access_logs" { } } } - - diff --git a/infrastructure/terraform/components/reporting/scripts/sql/views/monthly_recipient_with_more_than_five_messages.sql b/infrastructure/terraform/components/reporting/scripts/sql/views/monthly_recipient_with_more_than_five_messages.sql index 5b08ffa4..0ade1c36 100644 --- a/infrastructure/terraform/components/reporting/scripts/sql/views/monthly_recipient_with_more_than_five_messages.sql +++ b/infrastructure/terraform/components/reporting/scripts/sql/views/monthly_recipient_with_more_than_five_messages.sql @@ -21,4 +21,3 @@ FROM ( ) WHERE monthlytotalmessages >= 5 GROUP BY 1, 2, 3, 4 - diff --git a/infrastructure/terraform/components/reporting/ssm_parameter_completed_batch_report_client_ids.tf b/infrastructure/terraform/components/reporting/ssm_parameter_completed_batch_report_client_ids.tf index 3e0d80cb..9ddbfdbe 100644 --- a/infrastructure/terraform/components/reporting/ssm_parameter_completed_batch_report_client_ids.tf +++ b/infrastructure/terraform/components/reporting/ssm_parameter_completed_batch_report_client_ids.tf @@ -8,4 +8,3 @@ resource "aws_ssm_parameter" "completed_batch_report_client_ids" { ignore_changes = [value] } } - diff --git a/infrastructure/terraform/components/reporting/ssm_parameter_completed_comms_report_client_ids.tf b/infrastructure/terraform/components/reporting/ssm_parameter_completed_comms_report_client_ids.tf index 633c5dd0..984e3682 100644 --- a/infrastructure/terraform/components/reporting/ssm_parameter_completed_comms_report_client_ids.tf +++ b/infrastructure/terraform/components/reporting/ssm_parameter_completed_comms_report_client_ids.tf @@ -8,4 +8,3 @@ resource "aws_ssm_parameter" "completed_comms_report_client_ids" { ignore_changes = [value] } } - diff --git a/infrastructure/terraform/components/reporting/ssm_parameter_powerbi_gateway_client_id.tf b/infrastructure/terraform/components/reporting/ssm_parameter_powerbi_gateway_client_id.tf deleted file mode 100644 index 581498a8..00000000 --- a/infrastructure/terraform/components/reporting/ssm_parameter_powerbi_gateway_client_id.tf +++ /dev/null @@ -1,14 +0,0 @@ -resource "aws_ssm_parameter" "powerbi_gateway_client_id" { - count = var.enable_powerbi_gateway ? 1 : 0 - - name = "/${local.csi}/powerbi-gateway-client-id" - description = "The Client (Application) ID for the Service Principal" - type = "SecureString" - value = "CLIENT_ID_PLACEHOLDER" - - lifecycle { - ignore_changes = [ - value, - ] - } -} diff --git a/infrastructure/terraform/components/reporting/ssm_parameter_powerbi_gateway_client_secret.tf b/infrastructure/terraform/components/reporting/ssm_parameter_powerbi_gateway_client_secret.tf deleted file mode 100644 index abff954e..00000000 --- a/infrastructure/terraform/components/reporting/ssm_parameter_powerbi_gateway_client_secret.tf +++ /dev/null @@ -1,14 +0,0 @@ -resource "aws_ssm_parameter" "powerbi_gateway_client_secret" { - count = var.enable_powerbi_gateway ? 1 : 0 - - name = "/${local.csi}/powerbi-gateway-client-secret" - description = "The Client Secret for the Service Principal" - type = "SecureString" - value = "CLIENT_SECRET_PLACEHOLDER" - - lifecycle { - ignore_changes = [ - value, - ] - } -} diff --git a/infrastructure/terraform/components/reporting/ssm_parameter_powerbi_gateway_tenant_id.tf b/infrastructure/terraform/components/reporting/ssm_parameter_powerbi_gateway_tenant_id.tf deleted file mode 100644 index eac5550c..00000000 --- a/infrastructure/terraform/components/reporting/ssm_parameter_powerbi_gateway_tenant_id.tf +++ /dev/null @@ -1,14 +0,0 @@ -resource "aws_ssm_parameter" "powerbi_gateway_tenant_id" { - count = var.enable_powerbi_gateway ? 1 : 0 - - name = "/${local.csi}/powerbi-gateway-tenant-id" - description = "The Tenant ID for the Service Principal" - type = "SecureString" - value = "TENANT_ID_PLACEHOLDER" - - lifecycle { - ignore_changes = [ - value, - ] - } -} diff --git a/infrastructure/terraform/components/reporting/templates/cloudinit_config.tmpl b/infrastructure/terraform/components/reporting/templates/cloudinit_config.tmpl index 5555c9f1..399b8d98 100644 --- a/infrastructure/terraform/components/reporting/templates/cloudinit_config.tmpl +++ b/infrastructure/terraform/components/reporting/templates/cloudinit_config.tmpl @@ -18,19 +18,12 @@ if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) } -# Install PowerBI On-Premises Gateway and Desktop -choco install -y powerbigateway --version=3000.298.8 --ignore-checksums -choco install -y powerbi --ignore-checksums - -# Install vim -choco install -y vim - # Install Powershell 7 choco install -y powershell-core # Install Amazon Athena ODBC 2.x Driver -`$athenaDriverUrl = "https://downloads.athena.us-east-1.amazonaws.com/drivers/ODBC/v2.0.6.0/Windows/AmazonAthenaODBC-2.0.6.0.msi" -`$athenaDriverInstaller = "C:\scripts\SimbaAthenaODBC.msi" +`$athenaDriverUrl = "${athena_driver_url}" +`$athenaDriverInstaller = "C:\scripts\AmazonAthenaODBC.msi" Invoke-WebRequest -Uri `$athenaDriverUrl -OutFile `$athenaDriverInstaller # Silent installation of Amazon Athena ODBC driver @@ -85,23 +78,6 @@ if (Test-Path `$pwshPath) { Write-Output "AWS CLI installation failed or not found in PATH." exit 1 } - - # Get the client (application) and tenant id's - `$clientId = aws ssm get-parameter --name ${client_id} --with-decryption --query Parameter.Value --output text - `$tenantId = aws ssm get-parameter --name ${tenant_id} --with-decryption --query Parameter.Value --output text - - # Get and convert the client secret and recovery key to SecureStrings - `$clientSecretPlainText = aws ssm get-parameter --name ${client_secret} --with-decryption --query Parameter.Value --output text - `$clientSecret = `$clientSecretPlainText | ConvertTo-SecureString -AsPlainText -Force - `$recoveryKeyPlainText = aws ssm get-parameter --name ${recovery_key} --with-decryption --query Parameter.Value --output text - `$recoveryKey = `$recoveryKeyPlainText | ConvertTo-SecureString -AsPlainText -Force - - # Login to the PowerBI Service using the Service Principal (proposed, untested) https://learn.microsoft.com/en-us/powershell/module/datagateway.profile/connect-datagatewayserviceaccount?view=datagateway-ps - # Connect-DataGatewayServiceAccount -ApplicationId `$clientId -ClientSecret `$clientSecret -Tenant `$tenantId - # Install the gateway - # Install-DataGateway -AcceptConditions - # Register the Gateway (proposed, untested) https://learn.microsoft.com/en-us/powershell/module/datagateway/add-datagatewaycluster?view=datagateway-ps - # Add-DataGatewayCluster -RecoveryKey `$recoveryKey -Name ${gateway_name} -RegionKey uksouth } } else { diff --git a/infrastructure/terraform/components/reporting/variables.tf b/infrastructure/terraform/components/reporting/variables.tf index 3a1c983a..2d8130c3 100644 --- a/infrastructure/terraform/components/reporting/variables.tf +++ b/infrastructure/terraform/components/reporting/variables.tf @@ -99,10 +99,16 @@ variable "log_retention_days" { variable "enable_powerbi_gateway" { type = bool - description = "Deploy EC2 instance for PowerBI On-Premises Gateway" + description = "Deploy supporting resources for the legacy PowerBI On-Premises Gateway" default = true } +variable "athena_driver_url" { + type = string + description = "Amazon Athena ODBC MSI download URL for PowerBI gateway bootstrap" + default = "https://downloads.athena.us-east-1.amazonaws.com/drivers/ODBC/v2.1.0.0/Windows/AmazonAthenaODBC-2.1.0.0.msi" +} + variable "powerbi_gateway_instance_count" { description = "Number of standalone Power BI On-Premises Gateway instances created directly from the launch template." type = number @@ -127,24 +133,6 @@ variable "instance_type" { default = "t3.medium" } -variable "desired_capacity" { - description = "The desired number of instances in the Power BI On-Premises Gateway Auto Scaling group." - type = number - default = 1 -} - -variable "min_size" { - description = "The minimum number of instances in the Power BI On-Premises Gateway Auto Scaling group." - type = number - default = 1 -} - -variable "max_size" { - description = "The maximum number of instances in the Power BI On-Premises Gateway Auto Scaling group." - type = number - default = 1 -} - variable "enable_spot" { type = bool description = "run Power BI On-Premises Gateway as spot instances" @@ -163,18 +151,6 @@ variable "root_volume_size" { default = 80 } -variable "scale_out_recurrence_schedule" { - description = "The cron expression for the scale out schedule. Set to null if no recurrence is needed." - type = string - default = null -} - -variable "scale_in_recurrence_schedule" { - description = "The cron expression for the scale in schedule. Set to null if no recurrence is needed." - type = string - default = null -} - variable "core_account_ids" { description = "List of all corresponding core account id's that exist in the Non-Prod domain" type = list(string) diff --git a/infrastructure/terraform/deploy.sh b/infrastructure/terraform/deploy.sh index dcf64c44..66354c92 100755 --- a/infrastructure/terraform/deploy.sh +++ b/infrastructure/terraform/deploy.sh @@ -31,11 +31,17 @@ cd "${basedir}" || exit 1; terraform_environment="${DEPLOY_ENVIRONMENT}"; # determine the order of components to process +if [[ -n "${TERRAFORM_COMPONENTS:-}" ]]; then + component_list="${TERRAFORM_COMPONENTS}"; +elif [[ "${TERRAFORM_ACTION}" == "destroy" ]]; then + component_list="powerbi-gateway reporting"; +else + component_list="reporting powerbi-gateway"; +fi; + if [[ "${TERRAFORM_ACTION}" == "destroy" ]]; then - component_list="reporting"; plan_action="plan-destroy"; else - component_list="reporting"; plan_action="plan"; fi; diff --git a/infrastructure/terraform/etc/env_eu-west-2_main.tfvars b/infrastructure/terraform/etc/env_eu-west-2_main.tfvars index d3662737..150c6057 100644 --- a/infrastructure/terraform/etc/env_eu-west-2_main.tfvars +++ b/infrastructure/terraform/etc/env_eu-west-2_main.tfvars @@ -15,6 +15,8 @@ core_account_ids = [ # PowerBI On-Premises Gateway variables: enable_powerbi_gateway = true +instance_type = "t3.xlarge" +root_volume_size = 200 public_subnet_cidrs = [ "10.0.1.0/24", diff --git a/scripts/config/gitleaks.toml b/scripts/config/gitleaks.toml index 3f748294..106544af 100644 --- a/scripts/config/gitleaks.toml +++ b/scripts/config/gitleaks.toml @@ -22,7 +22,9 @@ paths = [ '''poetry.lock''', '''yarn.lock''', '''Gemfile.lock''', - '''infrastructure/terraform/components/reporting/templates/cloudinit_config.tmpl''' + '''infrastructure/terraform/components/reporting/templates/cloudinit_config.tmpl''', + '''infrastructure/terraform/components/powerbi-gateway/variables.tf''', + '''infrastructure/terraform/components/powerbi-gateway/README.md''' ] # Exclude Chrome version in user agent
"NULL"
]