From 42e63789f1b72b75ae9e21b68602274a633d3739 Mon Sep 17 00:00:00 2001 From: aidenvaines-bjss Date: Tue, 16 Jul 2024 23:12:36 +0100 Subject: [PATCH] CCM-5680 setup terraform components --- infrastructure/images/.gitkeep | 0 infrastructure/modules/.gitkeep | 0 infrastructure/terraform/.gitignore | 60 ++++++++++++ .../components/acct/.terraform-version | 1 + .../components/acct/locals_tfscaffold.tf | 44 +++++++++ .../terraform/components/acct/outputs.tf | 11 +++ .../terraform/components/acct/provider_aws.tf | 24 +++++ .../components/acct/route53_delegation_set.tf | 3 + .../terraform/components/acct/route53_zone.tf | 5 + .../acct/ssm_parameter_github_pat.tf | 16 ++++ .../terraform/components/acct/variables.tf | 66 +++++++++++++ .../terraform/components/acct/versions.tf | 10 ++ .../components/iam/.terraform-version | 1 + .../components/iam/acm_certificate.tf | 14 +++ .../terraform/components/iam/amplify_app.tf | 24 +++++ .../iam/amplify_domain_association.tf | 10 ++ .../iam/cloudwatch_log_group_amplify.tf | 5 + .../components/iam/cognito_user_pool.tf | 9 ++ .../iam/cognito_user_pool_client.tf | 26 ++++++ .../iam/cognito_user_pool_domain.tf | 5 + .../iam/data_ssm_parameter_github_pat.tf | 3 + .../components/iam/iam_role_amplify.tf | 51 ++++++++++ .../components/iam/locals_remote_state.tf | 40 ++++++++ .../components/iam/locals_tfscaffold.tf | 44 +++++++++ .../components/iam/module_amplify_branch.tf | 18 ++++ .../terraform/components/iam/module_kms.tf | 54 +++++++++++ .../terraform/components/iam/provider_aws.tf | 24 +++++ .../iam/route53_record_validation.tf | 16 ++++ .../terraform/components/iam/variables.tf | 88 ++++++++++++++++++ .../terraform/components/iam/versions.tf | 10 ++ infrastructure/terraform/etc/README.md | 26 ++++++ .../modules}/.gitkeep | 0 .../modules/amp_branch/amplify_branch.tf | 11 +++ .../terraform/modules/amp_branch/locals.tf | 33 +++++++ .../terraform/modules/amp_branch/outputs.tf | 3 + .../terraform/modules/amp_branch/variables.tf | 92 +++++++++++++++++++ .../terraform/modules/amp_branch/versions.tf | 9 ++ .../terraform/modules/kms/README.md | 42 +++++++++ .../modules/kms/data_iam_kms_admin_policy.tf | 30 ++++++ .../modules/kms/data_iam_kms_user_policy.tf | 42 +++++++++ .../kms/data_iam_policy_document_key.tf | 27 ++++++ .../terraform/modules/kms/iam_policy_admin.tf | 13 +++ .../terraform/modules/kms/iam_policy_user.tf | 13 +++ .../terraform/modules/kms/kms_key.tf | 8 ++ .../terraform/modules/kms/kms_key_alias.tf | 4 + .../terraform/modules/kms/locals.tf | 33 +++++++ .../terraform/modules/kms/outputs.tf | 15 +++ .../terraform/modules/kms/variables.tf | 78 ++++++++++++++++ .../terraform/modules/kms/versions.tf | 9 ++ 49 files changed, 1170 insertions(+) delete mode 100644 infrastructure/images/.gitkeep delete mode 100644 infrastructure/modules/.gitkeep create mode 100644 infrastructure/terraform/.gitignore create mode 100644 infrastructure/terraform/components/acct/.terraform-version create mode 100644 infrastructure/terraform/components/acct/locals_tfscaffold.tf create mode 100644 infrastructure/terraform/components/acct/outputs.tf create mode 100644 infrastructure/terraform/components/acct/provider_aws.tf create mode 100644 infrastructure/terraform/components/acct/route53_delegation_set.tf create mode 100644 infrastructure/terraform/components/acct/route53_zone.tf create mode 100644 infrastructure/terraform/components/acct/ssm_parameter_github_pat.tf create mode 100644 infrastructure/terraform/components/acct/variables.tf create mode 100644 infrastructure/terraform/components/acct/versions.tf create mode 100644 infrastructure/terraform/components/iam/.terraform-version create mode 100644 infrastructure/terraform/components/iam/acm_certificate.tf create mode 100644 infrastructure/terraform/components/iam/amplify_app.tf create mode 100644 infrastructure/terraform/components/iam/amplify_domain_association.tf create mode 100644 infrastructure/terraform/components/iam/cloudwatch_log_group_amplify.tf create mode 100644 infrastructure/terraform/components/iam/cognito_user_pool.tf create mode 100644 infrastructure/terraform/components/iam/cognito_user_pool_client.tf create mode 100644 infrastructure/terraform/components/iam/cognito_user_pool_domain.tf create mode 100644 infrastructure/terraform/components/iam/data_ssm_parameter_github_pat.tf create mode 100644 infrastructure/terraform/components/iam/iam_role_amplify.tf create mode 100644 infrastructure/terraform/components/iam/locals_remote_state.tf create mode 100644 infrastructure/terraform/components/iam/locals_tfscaffold.tf create mode 100644 infrastructure/terraform/components/iam/module_amplify_branch.tf create mode 100644 infrastructure/terraform/components/iam/module_kms.tf create mode 100644 infrastructure/terraform/components/iam/provider_aws.tf create mode 100644 infrastructure/terraform/components/iam/route53_record_validation.tf create mode 100644 infrastructure/terraform/components/iam/variables.tf create mode 100644 infrastructure/terraform/components/iam/versions.tf create mode 100644 infrastructure/terraform/etc/README.md rename infrastructure/{environments => terraform/modules}/.gitkeep (100%) create mode 100644 infrastructure/terraform/modules/amp_branch/amplify_branch.tf create mode 100644 infrastructure/terraform/modules/amp_branch/locals.tf create mode 100644 infrastructure/terraform/modules/amp_branch/outputs.tf create mode 100644 infrastructure/terraform/modules/amp_branch/variables.tf create mode 100644 infrastructure/terraform/modules/amp_branch/versions.tf create mode 100644 infrastructure/terraform/modules/kms/README.md create mode 100644 infrastructure/terraform/modules/kms/data_iam_kms_admin_policy.tf create mode 100644 infrastructure/terraform/modules/kms/data_iam_kms_user_policy.tf create mode 100644 infrastructure/terraform/modules/kms/data_iam_policy_document_key.tf create mode 100644 infrastructure/terraform/modules/kms/iam_policy_admin.tf create mode 100644 infrastructure/terraform/modules/kms/iam_policy_user.tf create mode 100644 infrastructure/terraform/modules/kms/kms_key.tf create mode 100644 infrastructure/terraform/modules/kms/kms_key_alias.tf create mode 100644 infrastructure/terraform/modules/kms/locals.tf create mode 100644 infrastructure/terraform/modules/kms/outputs.tf create mode 100644 infrastructure/terraform/modules/kms/variables.tf create mode 100644 infrastructure/terraform/modules/kms/versions.tf diff --git a/infrastructure/images/.gitkeep b/infrastructure/images/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/infrastructure/modules/.gitkeep b/infrastructure/modules/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/infrastructure/terraform/.gitignore b/infrastructure/terraform/.gitignore new file mode 100644 index 0000000..f0d9138 --- /dev/null +++ b/infrastructure/terraform/.gitignore @@ -0,0 +1,60 @@ +### Terraform ### + +# Transient backends +components/**/backend_tfscaffold.tf + +# Compiled files +**/*.tfstate +**/*.tfplan +**/*.tfstate.backup +**/.terraform +**/.terraform.lock.hcl +**/.terraform/* +**/build/* +**/work/* +**/*tfstate.lock.info + +# Scaffold Plugin Cache +plugin-cache/* + +# PyCache +**/__pycache__ + +### OSX ### +**/.DS_Store +**/.AppleDouble +**/.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +*.swp +.nyc_output + +# VS Code +.vscode + +# IntelliJ Idea +.idea +**/*.iml + +# js +node_modules diff --git a/infrastructure/terraform/components/acct/.terraform-version b/infrastructure/terraform/components/acct/.terraform-version new file mode 100644 index 0000000..bb9ffd3 --- /dev/null +++ b/infrastructure/terraform/components/acct/.terraform-version @@ -0,0 +1 @@ +latest:^1.8 diff --git a/infrastructure/terraform/components/acct/locals_tfscaffold.tf b/infrastructure/terraform/components/acct/locals_tfscaffold.tf new file mode 100644 index 0000000..b7cf321 --- /dev/null +++ b/infrastructure/terraform/components/acct/locals_tfscaffold.tf @@ -0,0 +1,44 @@ +locals { + terraform_state_bucket = format( + "%s-tfscaffold-%s-%s", + var.project, + var.aws_account_id, + var.region, + ) + + csi = replace( + format( + "%s-%s-%s", + var.project, + var.environment, + var.component, + ), + "_", + "", + ) + + # CSI for use in resources with a global namespace, i.e. S3 Buckets + csi_global = replace( + format( + "%s-%s-%s-%s-%s", + var.project, + var.aws_account_id, + var.region, + var.environment, + var.component, + ), + "_", + "", + ) + + 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/acct/outputs.tf b/infrastructure/terraform/components/acct/outputs.tf new file mode 100644 index 0000000..d406aef --- /dev/null +++ b/infrastructure/terraform/components/acct/outputs.tf @@ -0,0 +1,11 @@ +output "dns_zone" { + value = { + id = aws_route53_zone.main.id + name = aws_route53_zone.main.name + nameservers = aws_route53_zone.main.name_servers + } +} + +output "github_pat_ssm_param_name" { + value = aws_ssm_parameter.github_pat.name +} diff --git a/infrastructure/terraform/components/acct/provider_aws.tf b/infrastructure/terraform/components/acct/provider_aws.tf new file mode 100644 index 0000000..d694811 --- /dev/null +++ b/infrastructure/terraform/components/acct/provider_aws.tf @@ -0,0 +1,24 @@ +provider "aws" { + region = var.region + + allowed_account_ids = [ + var.aws_account_id, + ] + + default_tags { + tags = local.default_tags + } +} + +provider "aws" { + alias = "us-east-1" + region = "us-east-1" + + default_tags { + tags = local.default_tags + } + + allowed_account_ids = [ + var.aws_account_id, + ] +} diff --git a/infrastructure/terraform/components/acct/route53_delegation_set.tf b/infrastructure/terraform/components/acct/route53_delegation_set.tf new file mode 100644 index 0000000..b1a35d7 --- /dev/null +++ b/infrastructure/terraform/components/acct/route53_delegation_set.tf @@ -0,0 +1,3 @@ +resource "aws_route53_delegation_set" "main" { + reference_name = "iam.${var.root_domain_name}" +} diff --git a/infrastructure/terraform/components/acct/route53_zone.tf b/infrastructure/terraform/components/acct/route53_zone.tf new file mode 100644 index 0000000..1ffaf68 --- /dev/null +++ b/infrastructure/terraform/components/acct/route53_zone.tf @@ -0,0 +1,5 @@ +resource "aws_route53_zone" "main" { + name = "iam.${var.root_domain_name}" + + delegation_set_id = aws_route53_delegation_set.main.id +} diff --git a/infrastructure/terraform/components/acct/ssm_parameter_github_pat.tf b/infrastructure/terraform/components/acct/ssm_parameter_github_pat.tf new file mode 100644 index 0000000..059ab78 --- /dev/null +++ b/infrastructure/terraform/components/acct/ssm_parameter_github_pat.tf @@ -0,0 +1,16 @@ +resource "aws_ssm_parameter" "github_pat" { + name = "/${local.csi}/github_pat" + description = "A GitHub PAT token for settings up AWS Amplify. This is only used at initial setup of the service" + type = "SecureString" + value = try(var.initial_cli_secrets_provision_override.github_pat, "UNSET") + + lifecycle { + ignore_changes = [value] + } +} + +# This can be set at provision time like: +# PARAM_OBJECT=$(jq -n \ +# --arg github_pat "github_pat_123abc" \ +# '{github_pat:$github_pat}' | jq -R) +# .bin/terraform .. -a apply -- -var="initial_cli_secrets_provision_override=${PARAM_OBJECT}" diff --git a/infrastructure/terraform/components/acct/variables.tf b/infrastructure/terraform/components/acct/variables.tf new file mode 100644 index 0000000..bf74331 --- /dev/null +++ b/infrastructure/terraform/components/acct/variables.tf @@ -0,0 +1,66 @@ +## +# Basic Required Variables for tfscaffold Components +## + +variable "project" { + type = string + description = "The name of the tfscaffold project" +} + +variable "environment" { + type = string + description = "The name of the tfscaffold environment" +} + +variable "aws_account_id" { + type = string + description = "The AWS Account ID (numeric)" +} + +variable "region" { + type = string + description = "The AWS Region" +} + +variable "group" { + type = string + description = "The group variables are being inherited from (often synonmous with account short-name)" +} + +## +# tfscaffold variables specific to this component +## + +# This is the only primary variable to have its value defined as +# a default within its declaration in this file, because the variables +# purpose is as an identifier unique to this component, rather +# then to the environment from where all other variables come. +variable "component" { + type = string + description = "The variable encapsulating the name of this component" + default = "acct" +} + +variable "default_tags" { + type = map(string) + description = "A map of default tags to apply to all taggable resources within the component" + default = {} +} + +## +# Variables specific to the "dnsroot"component +## + +variable "root_domain_name" { + type = string + description = "The service's root DNS root nameespace, like nonprod.nhsnotify.national.nhs.uk" + default = "nonprod.nhsnotify.national.nhs.uk" +} + +variable "initial_cli_secrets_provision_override" { + type = map(string) + description = "A map of default value to intialise SSM secret values with. Only useful for initial setup of the account due to lifecycle rules." + default = {} + # Usage like: + # ... -a apply -- -var initial_cli_secrets_provision_override={\"github_pat\":\"l0ngstr1ng"} +} diff --git a/infrastructure/terraform/components/acct/versions.tf b/infrastructure/terraform/components/acct/versions.tf new file mode 100644 index 0000000..4d2b3c3 --- /dev/null +++ b/infrastructure/terraform/components/acct/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.50" + } + } + + required_version = ">= 1.8.1, < 1.9.0" +} diff --git a/infrastructure/terraform/components/iam/.terraform-version b/infrastructure/terraform/components/iam/.terraform-version new file mode 100644 index 0000000..bb9ffd3 --- /dev/null +++ b/infrastructure/terraform/components/iam/.terraform-version @@ -0,0 +1 @@ +latest:^1.8 diff --git a/infrastructure/terraform/components/iam/acm_certificate.tf b/infrastructure/terraform/components/iam/acm_certificate.tf new file mode 100644 index 0000000..478f774 --- /dev/null +++ b/infrastructure/terraform/components/iam/acm_certificate.tf @@ -0,0 +1,14 @@ +# resource "aws_acm_certificate" "main" { +# # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-and-https-requirements.html#https-requirements-certificate-issuer +# domain_name = "${local.csi}.${aws_route53_zone.main.name}" +# validation_method = "DNS" + +# lifecycle { +# create_before_destroy = true +# } +# } + +# resource "aws_acm_certificate_validation" "main" { +# certificate_arn = aws_acm_certificate.main.arn +# validation_record_fqdns = [for record in aws_route53_record.validation : record.fqdn] +# } diff --git a/infrastructure/terraform/components/iam/amplify_app.tf b/infrastructure/terraform/components/iam/amplify_app.tf new file mode 100644 index 0000000..58f0d4f --- /dev/null +++ b/infrastructure/terraform/components/iam/amplify_app.tf @@ -0,0 +1,24 @@ +resource "aws_amplify_app" "main" { + name = local.csi + repository = "https://github.com/NHSDigital/nhs-notify-iam-webauth" + access_token = data.aws_ssm_parameter.github_pat_ssm_param_name.value + + iam_service_role_arn = aws_iam_role.amplify.arn + + enable_auto_branch_creation = false + enable_branch_auto_build = var.enable_amplify_branch_auto_build + platform = "WEB_COMPUTE" + + auto_branch_creation_patterns = [ + "*", + "*/**" + ] + + environment_variables = { + USER_POOL_ID = aws_cognito_user_pool.main.id + HOSTED_LOGIN_DOMAIN = aws_cognito_user_pool.main.domain + NOTIFY_GROUP = var.group + NOTIFY_ENVIRONMENT = var.environment + NOTIFY_DOMAIN_NAME = local.acct.dns_zone["name"] + } +} diff --git a/infrastructure/terraform/components/iam/amplify_domain_association.tf b/infrastructure/terraform/components/iam/amplify_domain_association.tf new file mode 100644 index 0000000..cc40b92 --- /dev/null +++ b/infrastructure/terraform/components/iam/amplify_domain_association.tf @@ -0,0 +1,10 @@ +resource "aws_amplify_domain_association" "domain" { + app_id = aws_amplify_app.main.id + domain_name = local.acct.dns_zone["name"] + enable_auto_sub_domain = false + + sub_domain { + branch_name = module.amplify_branch.name + prefix = "" + } +} diff --git a/infrastructure/terraform/components/iam/cloudwatch_log_group_amplify.tf b/infrastructure/terraform/components/iam/cloudwatch_log_group_amplify.tf new file mode 100644 index 0000000..6841884 --- /dev/null +++ b/infrastructure/terraform/components/iam/cloudwatch_log_group_amplify.tf @@ -0,0 +1,5 @@ +resource "aws_cloudwatch_log_group" "amplify" { + name = "/aws/amplify/${local.csi}" + retention_in_days = var.log_retention_in_days + kms_key_id = module.kms.key_arn +} diff --git a/infrastructure/terraform/components/iam/cognito_user_pool.tf b/infrastructure/terraform/components/iam/cognito_user_pool.tf new file mode 100644 index 0000000..8c0ade6 --- /dev/null +++ b/infrastructure/terraform/components/iam/cognito_user_pool.tf @@ -0,0 +1,9 @@ +resource "aws_cognito_user_pool" "main" { + name = local.csi + + username_attributes = ["email"] + + admin_create_user_config { + allow_admin_create_user_only = true + } +} diff --git a/infrastructure/terraform/components/iam/cognito_user_pool_client.tf b/infrastructure/terraform/components/iam/cognito_user_pool_client.tf new file mode 100644 index 0000000..754c727 --- /dev/null +++ b/infrastructure/terraform/components/iam/cognito_user_pool_client.tf @@ -0,0 +1,26 @@ +resource "aws_cognito_user_pool_client" "main" { + name = local.csi + user_pool_id = aws_cognito_user_pool.main.id + + callback_urls = flatten([ + var.cognito_user_pool_additional_callback_urls, + [ + "https://${local.csi}.${var.root_domain_name}/auth/", + "https://${local.csi}.${aws_amplify_app.main.id}.amplifyapp.com/auth/" + ] + ]) + + supported_identity_providers = flatten([ + var.enable_cognito_built_in_idp ? ["COGNITO"] : [], + # identity_provider_names.provider.provider_name #e.g. auth0 + ]) + + allowed_oauth_flows = ["code"] + allowed_oauth_scopes = [ + "openid", + "email", + "phone", + "profile", + "aws.cognito.signin.user.admin" + ] +} diff --git a/infrastructure/terraform/components/iam/cognito_user_pool_domain.tf b/infrastructure/terraform/components/iam/cognito_user_pool_domain.tf new file mode 100644 index 0000000..84a99c1 --- /dev/null +++ b/infrastructure/terraform/components/iam/cognito_user_pool_domain.tf @@ -0,0 +1,5 @@ +# resource "aws_cognito_user_pool_domain" "domain" { +# user_pool_id = aws_cognito_user_pool.main.id +# domain = local.acct.dns_zone["name"] +# certificate_arn = aws_acm_certificate.main.arn +# } diff --git a/infrastructure/terraform/components/iam/data_ssm_parameter_github_pat.tf b/infrastructure/terraform/components/iam/data_ssm_parameter_github_pat.tf new file mode 100644 index 0000000..cc52a39 --- /dev/null +++ b/infrastructure/terraform/components/iam/data_ssm_parameter_github_pat.tf @@ -0,0 +1,3 @@ +data "aws_ssm_parameter" "github_pat_ssm_param_name" { + name = local.acct.github_pat_ssm_param_name +} diff --git a/infrastructure/terraform/components/iam/iam_role_amplify.tf b/infrastructure/terraform/components/iam/iam_role_amplify.tf new file mode 100644 index 0000000..bd13c70 --- /dev/null +++ b/infrastructure/terraform/components/iam/iam_role_amplify.tf @@ -0,0 +1,51 @@ +resource "aws_iam_role" "amplify" { + name = "${local.csi}-service-role" + assume_role_policy = data.aws_iam_policy_document.assumerole_amplify.json +} + +data "aws_iam_policy_document" "assumerole_amplify" { + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["amplify.amazonaws.com"] + } + + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role_policy_attachment" "amplify_amplify_backend_built_in" { + role = aws_iam_role.amplify.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmplifyBackendDeployFullAccess" +} + +resource "aws_iam_role_policy_attachment" "amplify_amplify" { + role = aws_iam_role.amplify.name + policy_arn = aws_iam_policy.amplify.arn +} + +resource "aws_iam_policy" "amplify" { + name = "${local.csi}-amplify" + description = "Amplify " + policy = data.aws_iam_policy_document.amplify.json +} + +data "aws_iam_policy_document" "amplify" { + statement { + effect = "Allow" + + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogGroups", + ] + + #tfsec:ignore:aws-iam-no-policy-wildcards + resources = [ + "${aws_cloudwatch_log_group.amplify.arn}:*", + "${aws_cloudwatch_log_group.amplify.arn}:log-stream:*", + ] + } +} diff --git a/infrastructure/terraform/components/iam/locals_remote_state.tf b/infrastructure/terraform/components/iam/locals_remote_state.tf new file mode 100644 index 0000000..50c57af --- /dev/null +++ b/infrastructure/terraform/components/iam/locals_remote_state.tf @@ -0,0 +1,40 @@ +locals { + bootstrap = data.terraform_remote_state.bootstrap.outputs + acct = data.terraform_remote_state.acct.outputs +} + +data "terraform_remote_state" "bootstrap" { + backend = "s3" + + config = { + bucket = local.terraform_state_bucket + + key = format( + "%s/%s/%s/%s/bootstrap.tfstate", + var.project, + var.aws_account_id, + "eu-west-2", + "bootstrap" + ) + + region = "eu-west-2" + } +} + +data "terraform_remote_state" "acct" { + backend = "s3" + + config = { + bucket = local.terraform_state_bucket + + key = format( + "%s/%s/%s/%s/acct.tfstate", + var.project, + var.aws_account_id, + "eu-west-2", + "main" + ) + + region = "eu-west-2" + } +} diff --git a/infrastructure/terraform/components/iam/locals_tfscaffold.tf b/infrastructure/terraform/components/iam/locals_tfscaffold.tf new file mode 100644 index 0000000..b7cf321 --- /dev/null +++ b/infrastructure/terraform/components/iam/locals_tfscaffold.tf @@ -0,0 +1,44 @@ +locals { + terraform_state_bucket = format( + "%s-tfscaffold-%s-%s", + var.project, + var.aws_account_id, + var.region, + ) + + csi = replace( + format( + "%s-%s-%s", + var.project, + var.environment, + var.component, + ), + "_", + "", + ) + + # CSI for use in resources with a global namespace, i.e. S3 Buckets + csi_global = replace( + format( + "%s-%s-%s-%s-%s", + var.project, + var.aws_account_id, + var.region, + var.environment, + var.component, + ), + "_", + "", + ) + + 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/iam/module_amplify_branch.tf b/infrastructure/terraform/components/iam/module_amplify_branch.tf new file mode 100644 index 0000000..db6d8be --- /dev/null +++ b/infrastructure/terraform/components/iam/module_amplify_branch.tf @@ -0,0 +1,18 @@ +module "amplify_branch" { + source = "../../modules/amp_branch" + + name = var.environment + aws_account_id = var.aws_account_id + component = var.component + environment = var.environment + project = var.project + region = var.region + group = var.group + + cognito_user_pool_client_id = aws_cognito_user_pool_client.main.user_pool_id + cognito_user_pool_identity_provider_names = aws_cognito_user_pool_client.main.supported_identity_providers + amplify_app_id = aws_amplify_app.main.id + branch = var.environment + domain_name = local.acct.dns_zone["name"] + subdomain = var.environment +} diff --git a/infrastructure/terraform/components/iam/module_kms.tf b/infrastructure/terraform/components/iam/module_kms.tf new file mode 100644 index 0000000..0a09f3d --- /dev/null +++ b/infrastructure/terraform/components/iam/module_kms.tf @@ -0,0 +1,54 @@ +module "kms" { + source = "../../modules/kms" + + aws_account_id = var.aws_account_id + component = var.component + environment = var.environment + project = var.project + region = var.region + + name = "main" + deletion_window = var.kms_deletion_window + alias = "alias/${local.csi}" + key_policy_documents = [data.aws_iam_policy_document.kms.json] + iam_delegation = true +} + +data "aws_iam_policy_document" "kms" { + # '*' resource scope is permitted in access policies as as the resource is itself + # https://docs.aws.amazon.com/kms/latest/developerguide/key-policy-services.html + + statement { + sid = "AllowCloudWatchEncrypt" + effect = "Allow" + + principals { + type = "Service" + + identifiers = [ + "logs.${var.region}.amazonaws.com", + ] + } + + actions = [ + "kms:Encrypt*", + "kms:Decrypt*", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:Describe*" + ] + + resources = [ + "*", + ] + + condition { + test = "ArnLike" + variable = "kms:EncryptionContext:aws:logs:arn" + + values = [ + "arn:aws:logs:${var.region}:${var.aws_account_id}:log-group:*", + ] + } + } +} diff --git a/infrastructure/terraform/components/iam/provider_aws.tf b/infrastructure/terraform/components/iam/provider_aws.tf new file mode 100644 index 0000000..d694811 --- /dev/null +++ b/infrastructure/terraform/components/iam/provider_aws.tf @@ -0,0 +1,24 @@ +provider "aws" { + region = var.region + + allowed_account_ids = [ + var.aws_account_id, + ] + + default_tags { + tags = local.default_tags + } +} + +provider "aws" { + alias = "us-east-1" + region = "us-east-1" + + default_tags { + tags = local.default_tags + } + + allowed_account_ids = [ + var.aws_account_id, + ] +} diff --git a/infrastructure/terraform/components/iam/route53_record_validation.tf b/infrastructure/terraform/components/iam/route53_record_validation.tf new file mode 100644 index 0000000..e7ad40e --- /dev/null +++ b/infrastructure/terraform/components/iam/route53_record_validation.tf @@ -0,0 +1,16 @@ +# resource "aws_route53_record" "validation" { +# for_each = { +# for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => { +# name = dvo.resource_record_name +# record = dvo.resource_record_value +# type = dvo.resource_record_type +# } +# } + +# allow_overwrite = true +# name = each.value.name +# records = [each.value.record] +# ttl = 60 +# type = each.value.type +# zone_id = local.acct.dns_zone["id"] +# } diff --git a/infrastructure/terraform/components/iam/variables.tf b/infrastructure/terraform/components/iam/variables.tf new file mode 100644 index 0000000..a76b4b5 --- /dev/null +++ b/infrastructure/terraform/components/iam/variables.tf @@ -0,0 +1,88 @@ +## +# Basic Required Variables for tfscaffold Components +## + +variable "project" { + type = string + description = "The name of the tfscaffold project" +} + +variable "environment" { + type = string + description = "The name of the tfscaffold environment" +} + +variable "aws_account_id" { + type = string + description = "The AWS Account ID (numeric)" +} + +variable "region" { + type = string + description = "The AWS Region" +} + +variable "group" { + type = string + description = "The group variables are being inherited from (often synonmous with account short-name)" +} + +## +# tfscaffold variables specific to this component +## + +# This is the only primary variable to have its value defined as +# a default within its declaration in this file, because the variables +# purpose is as an identifier unique to this component, rather +# then to the environment from where all other variables come. +variable "component" { + type = string + description = "The variable encapsulating the name of this component" + default = "iam" +} + +variable "default_tags" { + type = map(string) + description = "A map of default tags to apply to all taggable resources within the component" + default = {} +} + +## +# Variables specific to the "dnsroot"component +## + +variable "log_retention_in_days" { + type = number + description = "The retention period in days for the Cloudwatch Logs events to be retained, default of 0 is indefinite" + default = 0 +} + +variable "kms_deletion_window" { + type = string + description = "When a kms key is deleted, how long should it wait in the pending deletion state?" + default = "30" +} + +variable "root_domain_name" { + type = string + description = "The service's root DNS root nameespace, like nonprod.nhsnotify.national.nhs.uk" + default = "nonprod.nhsnotify.national.nhs.uk" +} + +variable "enable_amplify_branch_auto_build" { + type = bool + description = "Enable automatic building of branches" + default = false +} + +variable "cognito_user_pool_additional_callback_urls" { + type = list(string) + description = "A list of additional callback_urls for the cognito user pool" + default = [] +} + +variable "enable_cognito_built_in_idp" { + type = bool + description = "Enable the use of Cognito as an IDP; CIS2 is prefered" + default = false +} diff --git a/infrastructure/terraform/components/iam/versions.tf b/infrastructure/terraform/components/iam/versions.tf new file mode 100644 index 0000000..4d2b3c3 --- /dev/null +++ b/infrastructure/terraform/components/iam/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.50" + } + } + + required_version = ">= 1.8.1, < 1.9.0" +} diff --git a/infrastructure/terraform/etc/README.md b/infrastructure/terraform/etc/README.md new file mode 100644 index 0000000..23f35cc --- /dev/null +++ b/infrastructure/terraform/etc/README.md @@ -0,0 +1,26 @@ +# THIS DIRECTORY SHOULD BE IGNORED IN THE CONTEXT OF THE NHS NOTIFY PROGRAMME AND IS INTENDED FOR INDEPENDENT DEVELOPMENT USE ONLY + +## Example configuration files may look like this with the global.tfvars forming the base, any configuration will override with a most specific config taking presidence. env > region > group > Global + +**env_eu-west-2_example.tfvars** +``` +environment = "example" +``` + +**eu-west-2.tfvars** +``` +region = "eu-west-2" +``` + +**group_example.tfvars** +``` +group = "example" +aws_account_id = "1234567890" +``` + +**global.tfvars** +``` +tfscaffold_bucket_prefix = "nhs-notify-tfscaffold" +project = "myproject" +aws_account_id = "0987654321" +``` diff --git a/infrastructure/environments/.gitkeep b/infrastructure/terraform/modules/.gitkeep similarity index 100% rename from infrastructure/environments/.gitkeep rename to infrastructure/terraform/modules/.gitkeep diff --git a/infrastructure/terraform/modules/amp_branch/amplify_branch.tf b/infrastructure/terraform/modules/amp_branch/amplify_branch.tf new file mode 100644 index 0000000..8aefbc7 --- /dev/null +++ b/infrastructure/terraform/modules/amp_branch/amplify_branch.tf @@ -0,0 +1,11 @@ +resource "aws_amplify_branch" "main" { + app_id = var.amplify_app_id + branch_name = var.branch + display_name = var.name + enable_pull_request_preview = false # PR previews are not supported for public repos + + environment_variables = { + USER_POOL_CLIENT_ID = var.cognito_user_pool_client_id + NOTIFY_SUBDOMAIN = var.subdomain + } +} diff --git a/infrastructure/terraform/modules/amp_branch/locals.tf b/infrastructure/terraform/modules/amp_branch/locals.tf new file mode 100644 index 0000000..8d1bc72 --- /dev/null +++ b/infrastructure/terraform/modules/amp_branch/locals.tf @@ -0,0 +1,33 @@ +locals { + csi = format( + "%s-%s-%s-%s-%s", + var.project, + var.environment, + var.component, + var.module, + var.name, + ) + + # CSI for use in resources with an account namespace, eg IAM roles + csi_account = replace( + format( + "%s-%s-%s-%s-%s-%s", + var.project, + var.region, + var.environment, + var.component, + var.module, + var.name, + ), + "_", + "", + ) + + default_tags = merge( + var.default_tags, + { + Module = var.module + Name = local.csi + }, + ) +} diff --git a/infrastructure/terraform/modules/amp_branch/outputs.tf b/infrastructure/terraform/modules/amp_branch/outputs.tf new file mode 100644 index 0000000..2e300ce --- /dev/null +++ b/infrastructure/terraform/modules/amp_branch/outputs.tf @@ -0,0 +1,3 @@ +output "name" { + value = aws_amplify_branch.main.branch_name +} diff --git a/infrastructure/terraform/modules/amp_branch/variables.tf b/infrastructure/terraform/modules/amp_branch/variables.tf new file mode 100644 index 0000000..5798d9e --- /dev/null +++ b/infrastructure/terraform/modules/amp_branch/variables.tf @@ -0,0 +1,92 @@ +## +# Basic inherited variables for terraformscaffold modules +## + +variable "project" { + type = string + description = "The name of the terraformscaffold project calling the module" +} + +variable "environment" { + type = string + description = "The name of the terraformscaffold environment the module is called for" +} + +variable "component" { + type = string + description = "The name of the terraformscaffold component calling this module" +} + +variable "aws_account_id" { + type = string + description = "The AWS Account ID (numeric)" +} + +variable "group" { + type = string + description = "The group variables are being inherited from (often synonmous with account short-name)" +} + +## +# Module self-identification +## + +variable "module" { + type = string + description = "The name of this module. This is a special variable, it should be set only here and never overridden." + default = "kms" +} + +## +# Variable specific to the module +## + +# We presume this will always be specified. The default of {} will cause an error if a valid map is not specified. +# If we ever want to define this but allow it to not be specified, then we must provide a default tag keypair will be applied +# as the true default. In any other case default_tags should be removed from the module. +variable "default_tags" { + type = map(string) + description = "Default tag map for application to all taggable resources in the module" + default = {} +} + +variable "region" { + type = string + description = "The AWS Region" +} + +variable "name" { + type = string + description = "A unique name to distinguish this module invocation from others within the same CSI scope" +} + +variable "cognito_user_pool_client_id" { + description = "Cognito User Pool client ID" + type = string +} + +variable "cognito_user_pool_identity_provider_names" { + description = "A list of Cognito IDP names" + type = list(string) +} + +variable "amplify_app_id" { + description = "Amplify application ID" + type = string +} + +variable "branch" { + description = "The name of the branch being deployed" + type = string +} + +variable "domain_name" { + type = string + description = "Root domain name for this Amplify app" +} + +variable "subdomain" { + type = string + default = "main" + description = "Subdomain used as the branch alias" +} diff --git a/infrastructure/terraform/modules/amp_branch/versions.tf b/infrastructure/terraform/modules/amp_branch/versions.tf new file mode 100644 index 0000000..241ac05 --- /dev/null +++ b/infrastructure/terraform/modules/amp_branch/versions.tf @@ -0,0 +1,9 @@ + +terraform { + required_version = ">= 0.13" + required_providers { + aws = { + source = "hashicorp/aws" + } + } +} diff --git a/infrastructure/terraform/modules/kms/README.md b/infrastructure/terraform/modules/kms/README.md new file mode 100644 index 0000000..31fe336 --- /dev/null +++ b/infrastructure/terraform/modules/kms/README.md @@ -0,0 +1,42 @@ + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [alias](#input\_alias) | Alias name for the hieradata KMS key | `string` | n/a | yes | +| [aws\_account\_id](#input\_aws\_account\_id) | The AWS Account ID (numeric) | `string` | n/a | yes | +| [component](#input\_component) | The name of the terraformscaffold component calling this module | `string` | n/a | yes | +| [default\_tags](#input\_default\_tags) | Default tag map for application to all taggable resources in the module | `map(string)` | `{}` | no | +| [deletion\_window](#input\_deletion\_window) | KMS key deletion window | `string` | n/a | yes | +| [environment](#input\_environment) | The name of the terraformscaffold environment the module is called for | `string` | n/a | yes | +| [iam\_delegation](#input\_iam\_delegation) | Whether to delegate administration of the key to the local account. Defaults to true | `bool` | `true` | no | +| [key\_policy\_documents](#input\_key\_policy\_documents) | List of KMS key policy JSON documents | `list(string)` | `[]` | no | +| [module](#input\_module) | The name of this module. This is a special variable, it should be set only here and never overridden. | `string` | `"kms"` | no | +| [name](#input\_name) | A unique name to distinguish this module invocation from others within the same CSI scope | `string` | n/a | yes | +| [project](#input\_project) | The name of the terraformscaffold project calling the module | `string` | n/a | yes | +| [region](#input\_region) | The AWS Region | `string` | n/a | yes | +## Outputs + +| Name | Description | +|------|-------------| +| [admin\_policy\_arn](#output\_admin\_policy\_arn) | n/a | +| [key\_arn](#output\_key\_arn) | n/a | +| [key\_id](#output\_key\_id) | n/a | +| [user\_policy\_arn](#output\_user\_policy\_arn) | n/a | +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | n/a | +## Resources + +| Name | Type | +|------|------| +| [aws_iam_policy.admin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.user](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_kms_alias.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource | +| [aws_kms_key.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | +| [aws_iam_policy_document.admin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.user](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + diff --git a/infrastructure/terraform/modules/kms/data_iam_kms_admin_policy.tf b/infrastructure/terraform/modules/kms/data_iam_kms_admin_policy.tf new file mode 100644 index 0000000..25d4fb3 --- /dev/null +++ b/infrastructure/terraform/modules/kms/data_iam_kms_admin_policy.tf @@ -0,0 +1,30 @@ +data "aws_iam_policy_document" "admin" { + policy_id = "${local.csi}-admin" + + statement { + sid = "AllowKeyAdmin" + effect = "Allow" + + actions = [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:TagResource", + "kms:UntagResource", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + ] + + resources = [ + aws_kms_key.main.arn, + aws_kms_alias.main.arn, + ] + } +} diff --git a/infrastructure/terraform/modules/kms/data_iam_kms_user_policy.tf b/infrastructure/terraform/modules/kms/data_iam_kms_user_policy.tf new file mode 100644 index 0000000..99a63f9 --- /dev/null +++ b/infrastructure/terraform/modules/kms/data_iam_kms_user_policy.tf @@ -0,0 +1,42 @@ +data "aws_iam_policy_document" "user" { + policy_id = "${local.csi}-user" + + statement { + sid = "AllowUseOfTheKmskey" + effect = "Allow" + + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ] + + resources = [ + aws_kms_key.main.arn, + ] + } + + statement { + sid = "AllowDelegationToAwsServiceViaGrant" + effect = "Allow" + + actions = [ + "kms:CreateGrant", + ] + + resources = [ + aws_kms_key.main.arn, + ] + + condition { + test = "Bool" + variable = "kms:GrantIsForAWSResource" + + values = [ + "true", + ] + } + } +} diff --git a/infrastructure/terraform/modules/kms/data_iam_policy_document_key.tf b/infrastructure/terraform/modules/kms/data_iam_policy_document_key.tf new file mode 100644 index 0000000..7116cd3 --- /dev/null +++ b/infrastructure/terraform/modules/kms/data_iam_policy_document_key.tf @@ -0,0 +1,27 @@ +data "aws_iam_policy_document" "key" { + source_policy_documents = var.key_policy_documents + + dynamic "statement" { + for_each = var.iam_delegation ? [1] : [] + content { + sid = "AllowFullLocalAdministration" + effect = "Allow" + + principals { + type = "AWS" + + identifiers = [ + "arn:aws:iam::${var.aws_account_id}:root", + ] + } + + actions = [ + "kms:*", + ] + + resources = [ + "*", + ] + } + } +} diff --git a/infrastructure/terraform/modules/kms/iam_policy_admin.tf b/infrastructure/terraform/modules/kms/iam_policy_admin.tf new file mode 100644 index 0000000..bdef4b3 --- /dev/null +++ b/infrastructure/terraform/modules/kms/iam_policy_admin.tf @@ -0,0 +1,13 @@ +# Create the Key Policy for the AWS KMS Key +resource "aws_iam_policy" "admin" { + name = "${local.csi_account}-admin" + path = "/" + policy = data.aws_iam_policy_document.admin.json + + tags = merge( + local.default_tags, + { + Name = "${local.csi_account}-admin", + }, + ) +} diff --git a/infrastructure/terraform/modules/kms/iam_policy_user.tf b/infrastructure/terraform/modules/kms/iam_policy_user.tf new file mode 100644 index 0000000..f7dc182 --- /dev/null +++ b/infrastructure/terraform/modules/kms/iam_policy_user.tf @@ -0,0 +1,13 @@ +# Create the Key Policy for the AWS KMS Key +resource "aws_iam_policy" "user" { + name = "${local.csi_account}-user" + path = "/" + policy = data.aws_iam_policy_document.user.json + + tags = merge( + local.default_tags, + { + Name = "${local.csi_account}-user", + }, + ) +} diff --git a/infrastructure/terraform/modules/kms/kms_key.tf b/infrastructure/terraform/modules/kms/kms_key.tf new file mode 100644 index 0000000..787c01e --- /dev/null +++ b/infrastructure/terraform/modules/kms/kms_key.tf @@ -0,0 +1,8 @@ +resource "aws_kms_key" "main" { + bypass_policy_lockout_safety_check = false + deletion_window_in_days = var.deletion_window + description = local.csi + enable_key_rotation = true + policy = data.aws_iam_policy_document.key.json + tags = local.default_tags +} diff --git a/infrastructure/terraform/modules/kms/kms_key_alias.tf b/infrastructure/terraform/modules/kms/kms_key_alias.tf new file mode 100644 index 0000000..96c986e --- /dev/null +++ b/infrastructure/terraform/modules/kms/kms_key_alias.tf @@ -0,0 +1,4 @@ +resource "aws_kms_alias" "main" { + name = var.alias + target_key_id = aws_kms_key.main.key_id +} diff --git a/infrastructure/terraform/modules/kms/locals.tf b/infrastructure/terraform/modules/kms/locals.tf new file mode 100644 index 0000000..8d1bc72 --- /dev/null +++ b/infrastructure/terraform/modules/kms/locals.tf @@ -0,0 +1,33 @@ +locals { + csi = format( + "%s-%s-%s-%s-%s", + var.project, + var.environment, + var.component, + var.module, + var.name, + ) + + # CSI for use in resources with an account namespace, eg IAM roles + csi_account = replace( + format( + "%s-%s-%s-%s-%s-%s", + var.project, + var.region, + var.environment, + var.component, + var.module, + var.name, + ), + "_", + "", + ) + + default_tags = merge( + var.default_tags, + { + Module = var.module + Name = local.csi + }, + ) +} diff --git a/infrastructure/terraform/modules/kms/outputs.tf b/infrastructure/terraform/modules/kms/outputs.tf new file mode 100644 index 0000000..0f0fd9e --- /dev/null +++ b/infrastructure/terraform/modules/kms/outputs.tf @@ -0,0 +1,15 @@ +output "key_arn" { + value = aws_kms_key.main.arn +} + +output "key_id" { + value = aws_kms_key.main.key_id +} + +output "admin_policy_arn" { + value = aws_iam_policy.admin.arn +} + +output "user_policy_arn" { + value = aws_iam_policy.user.arn +} diff --git a/infrastructure/terraform/modules/kms/variables.tf b/infrastructure/terraform/modules/kms/variables.tf new file mode 100644 index 0000000..a6a7fdb --- /dev/null +++ b/infrastructure/terraform/modules/kms/variables.tf @@ -0,0 +1,78 @@ +## +# Basic inherited variables for terraformscaffold modules +## + +variable "project" { + type = string + description = "The name of the terraformscaffold project calling the module" +} + +variable "environment" { + type = string + description = "The name of the terraformscaffold environment the module is called for" +} + +variable "component" { + type = string + description = "The name of the terraformscaffold component calling this module" +} + +variable "aws_account_id" { + type = string + description = "The AWS Account ID (numeric)" +} + +## +# Module self-identification +## + +variable "module" { + type = string + description = "The name of this module. This is a special variable, it should be set only here and never overridden." + default = "kms" +} + +## +# Variable specific to the module +## + +# We presume this will always be specified. The default of {} will cause an error if a valid map is not specified. +# If we ever want to define this but allow it to not be specified, then we must provide a default tag keypair will be applied +# as the true default. In any other case default_tags should be removed from the module. +variable "default_tags" { + type = map(string) + description = "Default tag map for application to all taggable resources in the module" + default = {} +} + +variable "region" { + type = string + description = "The AWS Region" +} + +variable "name" { + type = string + description = "A unique name to distinguish this module invocation from others within the same CSI scope" +} + +variable "deletion_window" { + type = string + description = "KMS key deletion window" +} + +variable "alias" { + type = string + description = "Alias name for the hieradata KMS key" +} + +variable "key_policy_documents" { + type = list(string) + description = "List of KMS key policy JSON documents" + default = [] +} + +variable "iam_delegation" { + type = bool + description = "Whether to delegate administration of the key to the local account. Defaults to true" + default = true +} diff --git a/infrastructure/terraform/modules/kms/versions.tf b/infrastructure/terraform/modules/kms/versions.tf new file mode 100644 index 0000000..241ac05 --- /dev/null +++ b/infrastructure/terraform/modules/kms/versions.tf @@ -0,0 +1,9 @@ + +terraform { + required_version = ">= 0.13" + required_providers { + aws = { + source = "hashicorp/aws" + } + } +}