Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions modules/aws/alternate-contacts/backplane/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
name: AWS Alternate Contacts Backplane
supportedPlatforms:
- aws
description: |
Backplane infrastructure for the AWS Alternate Contacts building block.
---

This module sets up the IAM user and StackSet-based role deployment needed to manage alternate contacts on AWS accounts in your organization.

It creates:

1. An **IAM User** in your backplane account with permission to assume a service role in target accounts.
2. A **CloudFormation StackSet** deployed to the specified OUs that creates a service role in each target account with the necessary `account:*AlternateContact` permissions.

## Usage

```hcl
module "alternate_contacts_backplane" {
source = "./modules/aws/alternate-contacts/backplane"

building_block_target_ou_ids = ["ou-xxxx-xxxxxxxx"]

providers = {
aws.management = aws.management
aws.backplane = aws.backplane
}
}
```

<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | ~> 5.0 |

## Modules

No modules.

## Resources

| Name | Type |
|------|------|
| [aws_cloudformation_stack_set.permissions_in_target_accounts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudformation_stack_set) | resource |
| [aws_cloudformation_stack_set_instance.permissions_in_target_accounts](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudformation_stack_set_instance) | resource |
| [aws_iam_access_key.backplane](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource |
| [aws_iam_user.backplane](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource |
| [aws_iam_user_policy.assume_roles](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy) | resource |
| [aws_iam_policy_document.building_block_service](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_backplane_user_name"></a> [backplane\_user\_name](#input\_backplane\_user\_name) | n/a | `string` | `"building-block-alternate-contacts"` | no |
| <a name="input_building_block_target_account_access_role_name"></a> [building\_block\_target\_account\_access\_role\_name](#input\_building\_block\_target\_account\_access\_role\_name) | Name of the role that the backplane user will assume in the target account | `string` | `"building-block-alternate-contacts"` | no |
| <a name="input_building_block_target_ou_ids"></a> [building\_block\_target\_ou\_ids](#input\_building\_block\_target\_ou\_ids) | List of OUs that the building block can be deployed to. Accounts in these OUs will receive the building\_block\_backplane\_account\_access\_role | `set(string)` | n/a | yes |
| <a name="input_stackset_region"></a> [stackset\_region](#input\_stackset\_region) | AWS region to deploy the StackSet instances in | `string` | `"eu-central-1"` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_aws_access_key_id"></a> [aws\_access\_key\_id](#output\_aws\_access\_key\_id) | Access key for the IAM user that can set alternate contacts |
| <a name="output_aws_secret_access_key"></a> [aws\_secret\_access\_key](#output\_aws\_secret\_access\_key) | Secret key for the IAM user that can set alternate contacts |
| <a name="output_role_name"></a> [role\_name](#output\_role\_name) | Name of the IAM role assumed in target accounts to set alternate contacts |
<!-- END_TF_DOCS -->
112 changes: 112 additions & 0 deletions modules/aws/alternate-contacts/backplane/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# AWS Alternate Contacts Backplane
# This module creates necessary IAM Users and role setup so that we have an IAM user that can set alternate contacts
# on any account in the target OU.

# user referenced in building block definition
resource "aws_iam_user" "backplane" {
provider = aws.backplane
name = var.backplane_user_name
}

resource "aws_iam_access_key" "backplane" {
provider = aws.backplane
user = aws_iam_user.backplane.name
}

data "aws_partition" "current" {
provider = aws.backplane
}

# access building block service role in target accounts
data "aws_iam_policy_document" "building_block_service" {
provider = aws.backplane
version = "2012-10-17"
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
resources = ["arn:${data.aws_partition.current.partition}:iam::*:role/${var.building_block_target_account_access_role_name}"]
}
}

resource "aws_iam_user_policy" "assume_roles" {
provider = aws.backplane
name = "assume-roles"
user = aws_iam_user.backplane.name
policy = data.aws_iam_policy_document.building_block_service.json
}


# this stackset automatically deploys the building block backplane role to target accounts
resource "aws_cloudformation_stack_set" "permissions_in_target_accounts" {
provider = aws.management
name = var.building_block_target_account_access_role_name
permission_model = "SERVICE_MANAGED"
auto_deployment {
enabled = true
retain_stacks_on_account_removal = false
}
operation_preferences {
failure_tolerance_count = 50
max_concurrent_count = 50
}

template_body = jsonencode({
AWSTemplateFormatVersion = "2010-09-09",
Description = "Grants the building block backplane ${aws_iam_user.backplane.name} access to manage alternate contacts on a managed account.",
Resources = {
BuildingBlockServiceRolePermissions = {
Type = "AWS::IAM::Role",
Properties = {
RoleName = var.building_block_target_account_access_role_name,
AssumeRolePolicyDocument = {
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
AWS = aws_iam_user.backplane.arn
},
Action = "sts:AssumeRole"
}
]
},
Policies = [
{
PolicyName = var.building_block_target_account_access_role_name
PolicyDocument = {
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"account:PutAlternateContact",
"account:GetAlternateContact",
"account:DeleteAlternateContact",
],
Resource = "*"
}
]
}
}
]
}
}
}
})

capabilities = ["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"]

lifecycle {
ignore_changes = [administration_role_arn]
}
}

resource "aws_cloudformation_stack_set_instance" "permissions_in_target_accounts" {
provider = aws.management
deployment_targets {
organizational_unit_ids = var.building_block_target_ou_ids
}

region = var.stackset_region
stack_set_name = aws_cloudformation_stack_set.permissions_in_target_accounts.name
}
15 changes: 15 additions & 0 deletions modules/aws/alternate-contacts/backplane/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
output "aws_access_key_id" {
description = "Access key for the IAM user that can set alternate contacts"
value = aws_iam_access_key.backplane.id
}

output "aws_secret_access_key" {
description = "Secret key for the IAM user that can set alternate contacts"
sensitive = true
value = aws_iam_access_key.backplane.secret
}

output "role_name" {
description = "Name of the IAM role assumed in target accounts to set alternate contacts"
value = var.building_block_target_account_access_role_name
}
22 changes: 22 additions & 0 deletions modules/aws/alternate-contacts/backplane/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
variable "backplane_user_name" {
type = string
nullable = false
default = "building-block-alternate-contacts"
}

variable "building_block_target_account_access_role_name" {
type = string
description = "Name of the role that the backplane user will assume in the target account"
default = "building-block-alternate-contacts"
}

variable "stackset_region" {
type = string
description = "AWS region to deploy the StackSet instances in"
default = "eu-central-1"
}

variable "building_block_target_ou_ids" {
type = set(string)
description = "List of OUs that the building block can be deployed to. Accounts in these OUs will receive the building_block_backplane_account_access_role"
}
20 changes: 20 additions & 0 deletions modules/aws/alternate-contacts/backplane/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
terraform {
required_version = ">= 1.3.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"

configuration_aliases = [
# This provider needs to point at the account owning your AWS Organization
# or the delegated admin account that can deploy StackSets over your AWS Organization.
aws.management,

# This provider needs to point at the account that will host the IAM User for the building block backplane.
# Typically this is a dedicated account only used for building block automation.
aws.backplane
]
}
}
}

84 changes: 84 additions & 0 deletions modules/aws/alternate-contacts/buildingblock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
name: AWS Alternate Contacts
supportedPlatforms:
- aws
description: |
Sets the alternate contact information (billing, operations, security) for an AWS account.
---

This Terraform module configures the alternate contacts for an AWS account. AWS alternate contacts are used to receive notifications for billing, operations, and security-related communications, ensuring the right people are contacted for each concern.

Each contact type is optional -- set only the ones you need. AWS allows exactly one contact per type (billing, operations, security). Each contact requires a name, title, email, and phone number.

## Usage Examples

Set only a security contact:

```hcl
security_contact = {
name = "Jane Doe"
title = "Security Officer"
email = "security@example.com"
phone = "+1-555-555-0100"
}
```

Set billing and security contacts, skip operations:

```hcl
billing_contact = {
name = "Carlos Salazar"
title = "CFO"
email = "billing@example.com"
phone = "+1-555-555-0199"
}

security_contact = {
name = "Jane Doe"
title = "Security Officer"
email = "security@example.com"
phone = "+1-555-555-0100"
}
```

## Permissions

Please reference the [backplane implementation](../backplane/) for the required permissions to deploy this building block.

<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | ~> 5.0 |

## Modules

No modules.

## Resources

| Name | Type |
|------|------|
| [aws_account_alternate_contact.billing](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/account_alternate_contact) | resource |
| [aws_account_alternate_contact.operations](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/account_alternate_contact) | resource |
| [aws_account_alternate_contact.security](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/account_alternate_contact) | resource |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_account_id"></a> [account\_id](#input\_account\_id) | Target account id where the alternate contacts should be set | `string` | n/a | yes |
| <a name="input_assume_role_name"></a> [assume\_role\_name](#input\_assume\_role\_name) | The name of the role to assume in target account identified by account\_id | `string` | n/a | yes |
| <a name="input_aws_partition"></a> [aws\_partition](#input\_aws\_partition) | The AWS partition to use. e.g. aws, aws-cn, aws-us-gov | `string` | `"aws"` | no |
| <a name="input_billing_contact"></a> [billing\_contact](#input\_billing\_contact) | Billing alternate contact. Set to null to skip. All fields are required when set. | <pre>object({<br/> name = string<br/> title = string<br/> email = string<br/> phone = string<br/> })</pre> | `null` | no |
| <a name="input_operations_contact"></a> [operations\_contact](#input\_operations\_contact) | Operations alternate contact. Set to null to skip. All fields are required when set. | <pre>object({<br/> name = string<br/> title = string<br/> email = string<br/> phone = string<br/> })</pre> | `null` | no |
| <a name="input_security_contact"></a> [security\_contact](#input\_security\_contact) | Security alternate contact. Set to null to skip. All fields are required when set. | <pre>object({<br/> name = string<br/> title = string<br/> email = string<br/> phone = string<br/> })</pre> | `null` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_operations_contacts"></a> [operations\_contacts](#output\_operations\_contacts) | Map of configured alternate contact types to their email addresses |
<!-- END_TF_DOCS -->
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions modules/aws/alternate-contacts/buildingblock/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
resource "aws_account_alternate_contact" "operations" {
alternate_contact_type = "OPERATIONS"

name = var.operations_contact.name
title = var.operations_contact.title
email_address = var.operations_contact.email
phone_number = var.operations_contact.phone
}

resource "aws_account_alternate_contact" "billing" {
alternate_contact_type = "BILLING"

name = var.billing_contact.name
title = var.billing_contact.title
email_address = var.billing_contact.email
phone_number = var.billing_contact.phone
}

resource "aws_account_alternate_contact" "security" {
alternate_contact_type = "SECURITY"

name = var.security_contact.name
title = var.security_contact.title
email_address = var.security_contact.email
phone_number = var.security_contact.phone
}
8 changes: 8 additions & 0 deletions modules/aws/alternate-contacts/buildingblock/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
output "operations_contacts" {
description = "Map of configured alternate contact types to their email addresses"
value = {
"operations" = aws_account_alternate_contact.operations.email_address
"billing" = aws_account_alternate_contact.billing.email_address
"security" = aws_account_alternate_contact.security.email_address
}
}
8 changes: 8 additions & 0 deletions modules/aws/alternate-contacts/buildingblock/provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
provider "aws" {
region = "eu-central-1"

assume_role {
role_arn = "arn:${var.aws_partition}:iam::${var.account_id}:role/${var.assume_role_name}"
session_name = "deploy-alternate-contacts"
}
}
Loading
Loading