hashicorp / terraform-provider-aws

The AWS Provider enables Terraform to manage AWS resources.
https://registry.terraform.io/providers/hashicorp/aws
Mozilla Public License 2.0
9.76k stars 9.11k forks source link

aws_organizations_account support for deletion of default resources #9922

Open borgoat opened 5 years ago

borgoat commented 5 years ago

Community Note

Description

We use aws_organizations_account to create accounts in our organization. For each generated account, a file called account-name.tf.json is created. In this file, the provider uses an assume_role config block to escalate into the newly created account, and a module is applied (what we call an account skeleton) to create a basic set of resources that all accounts will need (a VPC, a SAML IdP and roles that may be assumed, and so on...).

However, a step we are missing is to delete:

This is how we create the account:

# We need this to generate a random email address
resource "random_pet" "managed_account_unique_name" {
  length    = 3
  separator = "-"
}

# Create the actual account within the organization
resource "aws_organizations_account" "managed_account" {
  email = "${var.wildcard_email_user}+${random_pet.managed_account_unique_name.id}@${var.wildcard_email_domain}"
  name  = var.account_name

  parent_id = var.parent_id
  role_name = var.role_name

  # This is because the role won't be retrieved in consequent runs
  lifecycle {
    ignore_changes = ["role_name"]
  }
}

# Here we generate the json files that will be used to keep accounts in sync
resource "null_resource" "generate_json" {

  # These are all the variables used to produce the template,
  # hence whenever any of these change we will recreate it
  triggers = {
    account_name       = aws_organizations_account.managed_account.name,
    account_id         = aws_organizations_account.managed_account.id,
    skeleton_source    = var.skeleton_source,
    skeleton_variables = jsonencode(var.skeleton_variables)
  }

  provisioner "local-exec" {

    command = "echo \"$BODY\" > $TARGET"

    environment = {
      BODY = templatefile("${path.module}/templates/managed-account.tf.json.tmpl", {
        account_name       = aws_organizations_account.managed_account.name,
        account_id         = aws_organizations_account.managed_account.id,
        role_name          = var.role_name,
        skeleton_source    = var.skeleton_source,
        skeleton_variables = var.skeleton_variables
      })
      TARGET = "${var.generated_json_folder}/${var.account_name}.tf.json"
    }
  }
}

and this is the template used to create the account specific config:

{  
    "//": "This file is generated by stis/terraform-aws-managed-account. DO NOT EDIT!",

    "provider": {
        "aws": {
            "alias": "${account_name}",
            "region": "eu-west-1",
            "shared_credentials_file": "../credentials",
            "profile": "MasterAccountProfile",
            "assume_role": {
                "role_arn": "arn:aws:iam::${account_id}:role/${role_name}"
            }
        }
    },
    "module": {
        "skeleton_${account_name}": {
            "source": "${skeleton_source}",
            "account_alias": "${account_name}",
            "assumable_from_account": "${data.aws_caller_identity.current.account_id}",
%{ for skel_var in skeleton_variables ~}
            "${skel_var.key}": ${jsonencode(skel_var.value)},
%{ endfor ~}
            "providers": {
                "aws": "aws.${account_name}"
            }
        }
    }
}

New or Affected Resource(s)

Potential Terraform Configuration

An option could be to add some optional flags to aws_organizations_account

resource "aws_organizations_account" "managed_account" {
  email = "${var.wildcard_email_user}+${random_pet.managed_account_unique_name.id}@${var.wildcard_email_domain}"
  name  = var.account_name

  parent_id = var.parent_id
  role_name = var.role_name

  # This is because the role won't be retrieved in consequent runs
  lifecycle {
    ignore_changes = ["role_name"]
  }

  delete_default_vpc = true
}

or even a provisioner (could be kept separately, however, looking at some discussions in Terraform repo, custom provisioners are not recommended)

resource "aws_organizations_account" "managed_account" {
  email = "${var.wildcard_email_user}+${random_pet.managed_account_unique_name.id}@${var.wildcard_email_domain}"
  name  = var.account_name

  parent_id = var.parent_id
  role_name = var.role_name

  # This is because the role won't be retrieved in consequent runs
  lifecycle {
    ignore_changes = ["role_name"]
  }

  provisioner "aws-account-setup" {
    delete_default_vpc = true
  }
}

Another idea could be (but would have to be carefully validated), to "inject" the state of the automatically created resources into a given state backend, so that they could then very simply be managed by Terraform.

References

This lambda from the account factory of AWS Control Tower gives an idea on how this is handled there.

udayanms commented 5 years ago

Regions keep popping up. May be AWS could have a switch at the time of account creation to not to create default stufff.

onitake commented 3 years ago

I agree that these default resources are a nuisance, but I don't think this is something that the Terraform provider can/should solve.

There is no well-defined set of actions that must be taken to clean up those default resources - it's completely in AWS's own discretion. Unless AWS provides an additional flag during account creation, implementing such complex behaviour in Terraform seems wrong to me.

Also, about the OrganizationAccountAccessRole, there's this document on how it's used: https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_accounts_access.html If such a role isn't created during organization account creation, how will you be able to modify resources in that account from the super account? You won't even be able to create the role itself. Maybe I'm missing something here, and there is another trust policy that solves this issue, but if there is, what's the point of restricting the OrganizationAccountAccessRole?

These are all limitations of the AWS Organizations framework, and IMHO must be solved there. If Terraform tries to patch over them, there is a high risk those patches will stop working or cause problems at some point.

gtmtech commented 2 years ago

If you're happy for the role to stick around, you could just use a datasource to read it, and then a role_policy or role_policy_attachment to deny "" on resource "" - thereby making it totally useless.

You could also add depends_on to spin up the roles you want first before doing so.

sidekick-eimantas commented 2 years ago

Just came accross this. As @gtmtech pointed out, there is an approach to deal with OrganizationAccountAccessRole role.

With regards to the default vpc - if you add this to your service control policy, default vpc wont be provisioned:

  statement {
    sid    = "DenyCreatingDefaultVPC"
    effect = "Deny"
    actions = [
      "ec2:CreateDefaultVpc",
      "ec2:CreateDefaultSubnet"
    ]
    resources = ["*"]
  }
ejade commented 6 months ago

Just came accross this. As @gtmtech pointed out, there is an approach to deal with OrganizationAccountAccessRole role.

With regards to the default vpc - if you add this to your service control policy, default vpc wont be provisioned:

  statement {
    sid    = "DenyCreatingDefaultVPC"
    effect = "Deny"
    actions = [
      "ec2:CreateDefaultVpc",
      "ec2:CreateDefaultSubnet"
    ]
    resources = ["*"]
  }

@sidekick-eimantas Could you elaborate further? I have tried this and organizations still provisions accounts with the default VPC enabled. I have created it as you stated and attached it to Root OU. This seems like a great solution, should it actually work.