Skip to content

Commit

Permalink
DSOS-2749: windows fsx module (#5865)
Browse files Browse the repository at this point in the history
* add DC secret

* fsx_windows module

* add fsx to baseline

* test fsx in ndh

* fix

* fix

* test for FSX

* add skip_final_backup

* bodge

* fix

* undo bodge

* fsx test

* fix

* fix

* fs test

* fix

* fix

* test

* fix

* README

* fix

* remove test fs

* readme

* remove from ndh

* fix
  • Loading branch information
drobinson-moj authored Apr 26, 2024
1 parent d2e6104 commit cb56db8
Show file tree
Hide file tree
Showing 13 changed files with 423 additions and 6 deletions.
1 change: 1 addition & 0 deletions terraform/environments/hmpps-domain-services/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ locals {
baseline_cloudwatch_metric_alarms = {}
baseline_ec2_autoscaling_groups = {}
baseline_ec2_instances = {}
baseline_fsx_windows = {}
baseline_iam_policies = {
SSMPolicy = {
description = "Policy to allow ssm actions"
Expand Down
40 changes: 34 additions & 6 deletions terraform/environments/hmpps-domain-services/locals_test.tf
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,41 @@ locals {
}
}

baseline_fsx_windows = {
# test-win-fs = {
# subnets = [{
# name = "private"
# availability_zones = ["eu-west-2a", "eu-west-2b"]
# }]
# preferred_subnet_name = "private"
# preferred_availability_zone = "eu-west-2a"
# deployment_type = "MULTI_AZ_1"
# security_groups = ["rds-ec2s"]
# skip_final_backup = true
# storage_capacity = 32
# throughput_capacity = 8
# self_managed_active_directory = {
# dns_ips = [
# module.ip_addresses.mp_ip.ad-azure-dc-a,
# module.ip_addresses.mp_ip.ad-azure-dc-b,
# ]
# domain_name = "azure.noms.root"
# username = "svc_join_domain"
# password_secret_name = "/microsoft/AD/azure.noms.root/shared-passwords"
# }
# }
}

baseline_ec2_autoscaling_groups = {

test-win-2012 = {
# ami has unwanted ephemeral device, don't copy all the ebs_volumess
# clean up test-win-2012 Computer and DNS entry from azure.noms.root domain before using
config = merge(module.baseline_presets.ec2_instance.config.default, {
ami_name = "base_windows_server_2012_r2_release*"
availability_zone = null
ebs_volumes_copy_all_from_ami = false
instance_profile_policies = concat(module.baseline_presets.ec2_instance.config.default.instance_profile_policies, ["SSMPolicy", "PatchBucketAccessPolicy"])
user_data_raw = base64encode(file("./templates/rds-gateway-user-data.yaml"))
user_data_raw = module.baseline_presets.ec2_instance.user_data_raw["user-data-pwsh"]
})
instance = merge(module.baseline_presets.ec2_instance.instance.default, {
vpc_security_group_ids = ["rds-ec2s"]
Expand All @@ -54,22 +80,23 @@ locals {
autoscaling_group = merge(module.baseline_presets.ec2_autoscaling_group.default, {
desired_capacity = 0
})
autoscaling_schedules = module.baseline_presets.ec2_autoscaling_schedules.working_hours
# autoscaling_schedules = module.baseline_presets.ec2_autoscaling_schedules.working_hours
tags = {
description = "Windows Server 2012 for connecting to Azure domain"
os-type = "Windows"
component = "test"
server-type = "HmppsDomainServicesTest"
}
}

test-win-2022 = {
# ami has unwanted ephemeral device, don't copy all the ebs_volumess
# clean up test-win-2022 Computer and DNS entry from azure.noms.root domain before using
config = merge(module.baseline_presets.ec2_instance.config.default, {
ami_name = "hmpps_windows_server_2022_release_2024-*"
availability_zone = null
ebs_volumes_copy_all_from_ami = false
instance_profile_policies = concat(module.baseline_presets.ec2_instance.config.default.instance_profile_policies, ["SSMPolicy", "PatchBucketAccessPolicy"])
user_data_raw = base64encode(file("./templates/rds-gateway-user-data.yaml"))
user_data_raw = module.baseline_presets.ec2_instance.user_data_raw["user-data-pwsh"]
})
instance = merge(module.baseline_presets.ec2_instance.instance.default, {
vpc_security_group_ids = ["rds-ec2s"]
Expand All @@ -80,11 +107,12 @@ locals {
autoscaling_group = merge(module.baseline_presets.ec2_autoscaling_group.default, {
desired_capacity = 0
})
autoscaling_schedules = module.baseline_presets.ec2_autoscaling_schedules.working_hours
# autoscaling_schedules = module.baseline_presets.ec2_autoscaling_schedules.working_hours
tags = {
description = "Windows Server 2022 for connecting to Azure domain"
os-type = "Windows"
component = "test"
server-type = "HmppsDomainServicesTest"
}
}

Expand Down
5 changes: 5 additions & 0 deletions terraform/environments/hmpps-domain-services/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ module "baseline" {
lookup(local.baseline_environment_config, "baseline_ec2_instances", {})
)

fsx_windows = merge(
local.baseline_fsx_windows,
lookup(local.baseline_environment_config, "baseline_fsx_windows", {})
)

iam_policies = merge(
module.baseline_presets.iam_policies,
local.baseline_iam_policies,
Expand Down
10 changes: 10 additions & 0 deletions terraform/environments/hmpps-domain-services/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ output "acm_certificates_validation_records_external" {
value = { for key, value in module.baseline.acm_certificates : key => value.validation_records_external }
}

output "efs_dns_names" {
description = "EFS DNS names"
value = { for key, value in module.baseline.efs : key => value.file_system.dns_name }
}

output "fsx_windows_dns_names" {
description = "FSX Windows DNS Names"
value = { for key, value in module.baseline.fsx_windows : key => value.windows_file_system.dns_name }
}

output "route53_zone_ns_records" {
description = "NS records of created zones"
value = { for key, value in module.baseline.route53_zones : key => value.name_servers }
Expand Down
42 changes: 42 additions & 0 deletions terraform/modules/baseline/fsx.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
locals {
# lookup kms_key_ids, subnet ids and lookup security group ids
fsx_windows = {
for key, value in var.fsx_windows : key => merge(value, {
kms_key_id = try(var.environment.kms_keys[value.kms_key_id].arn, value.kms_key_id)
preferred_subnet_id = value.preferred_availability_zone != null ? var.environment.subnet[value.preferred_subnet_name][value.preferred_availability_zone].id : null
subnet_ids = flatten([
for subnet in value.subnets : [
for az in subnet.availability_zones : [
var.environment.subnet[subnet.name][az].id
]
]
])
security_group_ids = [for sg in value.security_groups : try(aws_security_group.this[sg].id, sg)]
})
}
}

module "fsx_windows" {
for_each = local.fsx_windows

source = "../../modules/fsx_windows"

name = each.key
active_directory_id = each.value.active_directory_id
automatic_backup_retention_days = each.value.automatic_backup_retention_days
backup_id = each.value.backup_id
daily_automatic_backup_start_time = each.value.daily_automatic_backup_start_time
deployment_type = each.value.deployment_type
kms_key_id = each.value.kms_key_id
preferred_subnet_id = each.value.preferred_subnet_id
security_group_ids = each.value.security_group_ids
self_managed_active_directory = each.value.self_managed_active_directory
skip_final_backup = each.value.skip_final_backup
storage_capacity = each.value.storage_capacity
storage_type = each.value.storage_type
subnet_ids = each.value.subnet_ids
throughput_capacity = each.value.throughput_capacity
weekly_maintenance_start_time = each.value.weekly_maintenance_start_time

tags = merge(local.tags, each.value.tags)
}
5 changes: 5 additions & 0 deletions terraform/modules/baseline/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ output "efs" {
value = module.efs
}

output "fsx_windows" {
description = "map of fsx_windows module outputs corresponding to var.fsx_windows"
value = module.fsx_windows
}

output "iam_policies" {
description = "map of aws_iam_policy resources"
value = aws_iam_policy.this
Expand Down
36 changes: 36 additions & 0 deletions terraform/modules/baseline/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,42 @@ variable "environment" {
description = "Standard environmental data resources from the environment module"
}

variable "fsx_windows" {
description = "map of fsx_windows (e.g. windows file system) modules to create where map key is tags.Name"

type = map(object({
active_directory_id = optional(string)
automatic_backup_retention_days = optional(number) # [0,90] (default 7)
backup_id = optional(string)
daily_automatic_backup_start_time = optional(string)
deployment_type = optional(string) # [SINGLE_AZ_1 (default), SINGLE_AZ_2, MULTI_AZ_1]
kms_key_id = optional(string, "general")
preferred_subnet_name = optional(string, "private") # set if MULTI_AZ_1
preferred_availability_zone = optional(string) # set if MULTI_AZ_1
security_group_ids = optional(list(string))
skip_final_backup = optional(bool)
storage_capacity = optional(number) # GiB [32, 65536]
storage_type = optional(string) # SSD (default), HDD allowed for SINGLE_AZ_2, MULTI_AZ_1
subnets = list(object({
name = optional(string, "private")
availability_zones = optional(list(string), ["eu-west-2a", "eu-west-2b", "eu-west-2c"])
}))
security_groups = list(string)
throughput_capacity = number # MBps [8, 2048] in power of 2 increments
weekly_maintenance_start_time = optional(string)
self_managed_active_directory = optional(object({
dns_ips = list(string)
domain_name = string
password_secret_name = optional(string) # secret must be json key/pair with username as key
username = string
file_system_administrators_group = optional(string) # set if not "Domain Admins"
organizational_unit_distinguished_name = optional(string)
}))
tags = optional(map(string), {})
}))
default = {}
}

variable "iam_policies" {
description = "map of iam policies to create, where the key is the name of the policy"
type = map(object({
Expand Down
133 changes: 133 additions & 0 deletions terraform/modules/fsx_windows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# FSX Windows File System Module

Pretty much a straight wrapper for the fsx resource but retrieves credentials for domain join.

## Example Usage

See https://github.com/ministryofjustice/modernisation-platform-configuration-management repo
for ansible code for mounting on linux server (filesystems role).

If joining on Windows server, example powershell:
```
NB: if manually testing, don't run this command under Administrator.
New-PSDrive -Name "D" -PSProvider "FileSystem" -Root "\\amznfsxf09lugmi.azure.noms.root\share" -Persist -Scope Global
```

NOTES:
- Use Single-AZ solution for non-production environments to save cost.
- Multi-AZ can only include 2 availability zones.
- Set `skip_final_backup true` to avoid issues deleting the resource

## Security Groups

The module does not create security groups. Unlike EFS, there is
authentication, but still good practice to limit network access.

### Example 1 - Same security group as EC2

Use the same security group as the EC2 mounting the Windows File System.
Just ensure there is an internal rule allowing internal traffic
like this:

```
resource "aws_security_group_rule" "all_from_self" {
security_group_id = aws_security_group.ec2.id
from_port = 0
to_port = 0
protocol = -1
self = true
}
```

### Example 2 - Separate security group

Create a separate security group and allow inbound traffic
only from the security groups that the EC2s belong to.

```
resource "aws_security_group" "fsx" {
name = "fsx"
vpc_id = data.aws_vpc.shared.id
}
resource "aws_security_group_rule" "fsx_ingress" {
security_group_id = aws_security_group.fsx.id
type = "ingress"
from_port = 445
to_port = 445
protocol = "tcp"
source_security_group_id = aws_security_group.ec2.id
}
```

## Multi-AZ example

This:
- creates multi-AZ solution with mount points in eu-west-2a and eu-west-2b
- joins on-prem AD
- associates the mount point with a `aws_security_group.ec2` resource, see Security Groups - Example 1

```
module "fsx_windows1" {
source = "../../modules/fsx_windows"
preferred_subnet_id = data.aws_subnet.private_subnets_a.id
deployment_type = "MULTI_AZ_1"
name = "fsx_windows1"
security_groups = [aws_security_group.ec2.id]
skip_final_backup = true
storage_capacity = 32
throughput_capacity = 8
subnet_ids = [
data.aws_subnet.private_subnets_a.id,
data.aws_subnet.private_subnets_b.id
]
self_managed_active_directory = {
dns_ips = [
module.ip_addresses.mp_ip.ad-azure-dc-a,
module.ip_addresses.mp_ip.ad-azure-dc-b,
]
domain_name = "azure.noms.root"
username = "svc_join_domain"
password_secret_name = "/microsoft/AD/azure.noms.root/shared-passwords"
}
tags = local.tags
}
output "fsx_windows1_dns_name" {
description = "FSX Windows DNS name"
value = module.fsx_windows1.windows_file_system.dns_name
}
```

## Single-AZ example

This:
- creates single-AZ solution with mount points in zone A only
- joins existing AWS AD (created outside of this module)

```
module "fsx_windows2" {
source = "../../modules/fsx_windows"
active_directory_id = aws_directory_service_directory.this.id
deployment_type = "SINGLE_AZ_1"
name = "fsx_windows2"
security_groups = ["aws_security_group.fsx.id"]
skip_final_backup = true
storage_capacity = 32
subnet_ids = [ data.aws_subnet.private_subnets_a.id]
throughput_capacity = 8
tags = local.tags
}
output "fsx_windows2_dns_name" {
description = "FSX Windows DNS name"
value = module.fsx_windows2.windows_file_system.dns_name
}
```

10 changes: 10 additions & 0 deletions terraform/modules/fsx_windows/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
data "aws_secretsmanager_secret" "this" {
count = try(var.self_managed_active_directory, null) != null ? 1 : 0

name = var.self_managed_active_directory.password_secret_name
}

data "aws_secretsmanager_secret_version" "this" {
count = length(data.aws_secretsmanager_secret.this) != 0 ? 1 : 0
secret_id = data.aws_secretsmanager_secret.this[0].id
}
6 changes: 6 additions & 0 deletions terraform/modules/fsx_windows/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
locals {
# expecting the secret to be json key/pair with username as key, e.g. `{"svc_join_domain":"mypassword"}`
domain_join_secret_string = var.self_managed_active_directory != null ? data.aws_secretsmanager_secret_version.this[0].secret_string : null
domain_join_secret_json = var.self_managed_active_directory != null ? jsondecode(local.domain_join_secret_string) : null
domain_join_password = var.self_managed_active_directory != null ? local.domain_join_secret_json[var.self_managed_active_directory.username] : null
}
Loading

0 comments on commit cb56db8

Please sign in to comment.