terraform-aws-control_tower_account_factory icon indicating copy to clipboard operation
terraform-aws-control_tower_account_factory copied to clipboard

Deploy resources in region specified by custom_fields

Open marcuslindfeldt opened this issue 2 years ago • 9 comments

We have a use case where we want to create some default resources, but only in a region specified in the account request.

I hoped it was possible to use custom_fields to specify a region and then dynamically create a provider for that region in my account customization. This allows me to reuse the same account customization for many different regions.

But I learned that terraform does not support dynamic providers in that way. Is it possible to use the aft-providers.jinja jinja template in some way to generate another region specific provider?

I guess it's possible to create duplicated account-customizations per region but it feels like a workaround.

marcuslindfeldt avatar Apr 07 '22 14:04 marcuslindfeldt

Hey @marcuslindfeldt, I believe we've got an example that modifies the aft-providers.jinja to achieve this, which you should be able to reference in your main.tf, example here

balltrev avatar Apr 07 '22 16:04 balltrev

Hi @balltrev,

Yes, that works to statically create a provider in a different region but as I understand it you cannot access custom fields from within aft-providers.jinja because they are not injected. And so I cannot dynamically select the region based on a custom field from the account request which is ideally what I'm after.

marcuslindfeldt avatar Apr 11 '22 07:04 marcuslindfeldt

Ah, so if I'm understanding, you're looking to have some kind of injection of custom fields into the providers Jinja template?

If that's correct, I can go ahead and create a backlog to capture this enhancement.

balltrev avatar Apr 21 '22 20:04 balltrev

Yes that is correct!

marcuslindfeldt avatar Apr 23 '22 11:04 marcuslindfeldt

Got it! I've gone ahead and created a backlog for this, thanks!

balltrev avatar Apr 26 '22 21:04 balltrev

+1

hsdp-smulford avatar Jul 28 '22 14:07 hsdp-smulford

This would help us a lot too. In our case we want to create an S3 Bucket and a DynamoDB in all regions governed by the Control Tower. Currently it's only possible by hardcoding those regions in the jinja template, like:

{% for region in ['eu-central-1','eu-west-1','eu-west-2','eu-west-3','eu-north-1','us-east-1','us-west-2'] -%}
provider "aws" {
  region = "{{ region }}"
  alias  = "{{ region }}"
  assume_role {
    role_arn = local.role_arn
  }
  default_tags {
    tags = local.tags
  }
}

resource "aws_dynamodb_table" "terraform_lock_{{ region }}" {
  provider      = aws.{{ region }}
  name          = "terraform-lock"
....
{% endfor %}

It would be much more convenient to pass them as an attribute to the pipeline.

Mi3-14159 avatar Aug 11 '22 18:08 Mi3-14159

I managed to pass custom fields to the template using the pre-api-helpers.sh script. It is far from a convinient solution but at the moment is solves my problem. In account request:

module "dev" {
  source = "./modules/aft-account-request"
  ...
    custom_fields = {
       ...
       project_region = "eu-north-1"
    }
  }

Then in the pre-api-helpers.sh of the customization:

echo "Main region $CT_MGMT_REGION"
PROJECT_REGION=$(aws --region=$CT_MGMT_REGION ssm get-parameter --name /aft/account-request/custom-fields/project_region --query Parameter.Value --output text)
echo "Project region $PROJECT_REGION"

cd $DEFAULT_PATH/$CUSTOMIZATION/terraform
for f in *.jinja; do sed -i -e 's/{{ project_region }}/food/g' $f; done
cat $DEFAULT_PATH/$CUSTOMIZATION/terraform/aft-providers.jinja

I am using "sed" since calling "jinja2" without all parameters defined would render the other values empty. Could be solved by defining conditionals in the template itself - but makes it way too complicated. Easier to use sed here. And using the custom variable {{ project_region }} in the aft-providers.jinja

provider "aws" {
  region = "{{ project_region }}"
  assume_role {
    role_arn    = "{{ target_admin_role_arn }}"
  }
  default_tags {
    tags = {
      managed_by                  = "AFT"
    }
  }
}

provider "aws" {
  alias  = "provider_region"
  region = "{{ provider_region }}"
  assume_role {
    role_arn    = "{{ target_admin_role_arn }}"
  }
  default_tags {
    tags = {
      managed_by                  = "AFT"
    }
  }
}

I am changing the default provider and adding an alias for the "original" so I can still fetch other custom_fields defined in SSM using terraform. Not ideal but for the moment this might help someone out who has the same problem.

martivo avatar Oct 06 '22 14:10 martivo

Based on the ideas above from @martivo we went a bit further!

In our pre-api-helpers.sh we call a script inject-custom-fields.sh

#!/bin/bash
echo "Main region $CT_MGMT_REGION"
CUSTOM_FIELDS=$(aws --region=$CT_MGMT_REGION ssm get-parameters-by-path --path /aft/account-request/custom-fields/ --query Parameters --output json)
cd $DEFAULT_PATH/$CUSTOMIZATION/terraform

definitions=$(echo $CUSTOM_FIELDS | jq -r $'map(.Name |= split("/")[-1]) | .[] | "{% set \(.Name) = \'\(.Value)\' %}"')

echo -e "Injecting:\n$definitions"
for f in *.jinja; do
  echo -e "$definitions\n$(cat $f)" >$f
done

This will inject a set of jinja definitions on top of each jinja file so they can be used in jinja templating. So what the AFT pipelines will process could look like this:

{% set project_region = 'eu-north-1' %}
## Auto generated providers.tf ##
## Updated on: {{ timestamp }} ##

provider "aws" {
  region = "{{ project_region }}"
  assume_role {
    role_arn    = "{{ target_admin_role_arn }}"
  }
}

This lets us cover cases where a custom field might for example be renamed if we where to change project_region to account_region we could update our aft-providers.jinja file to have a {{ account_region or project_region }} allowing it to use the new field if available or fallback to the old field if thats defined instead.

If you want to go even further, which might solve @Mi3-14159 problem, this below will try to parse the custom fields to generate the jinja definitions with the correct type.
So if your custom field contains a json encoded array or object you could then use that to iterate over in your jinja templates! Your usability of that might vary tho 😄 we've found its good enough to just get the string values to inject.
#!/bin/bash
echo "Main region $CT_MGMT_REGION"
CUSTOM_FIELDS=$(aws --region=$CT_MGMT_REGION ssm get-parameters-by-path --path /aft/account-request/custom-fields/ --query Parameters --output json)
cd $DEFAULT_PATH/$CUSTOMIZATION/terraform

definitions=$(echo $CUSTOM_FIELDS | jq -r $'map(.Name |= split("/")[-1]) | .[] | "{% set \(.Name) = \(.Value | if type == "string" and (try fromjson catch false) then fromjson else . end | if type == "string" then "\\"" + tostring + "\\"" else . end) %}"')

echo -e "Injecting:\n$definitions"
for f in *.jinja; do
  echo -e "$definitions\n$(cat $f)" >$f
done

resulting in something like this, first two rows injected by pre-api-helper and then AFT will run jinja on it and get two providers

{% set project_region = "eu-west-3" %}
{% set aws_regions = ["eu-west-1", "eu-west-3"] %}
## Auto generated providers.tf ##
## Updated on: {{ timestamp }} ##

{% for region in aws_regions %}
provider "aws" {
  region = "{{ region }}"
  alias = "{{ region }}"
  assume_role {
    role_arn    = "{{ target_admin_role_arn }}"
  }
}
{% endfor %}

end result

## Auto generated providers.tf ##
## Updated on: 2023-03-02 18:54:34 ##

provider "aws" {
  region = "eu-west-1"
  alias = "eu-west-1"
  assume_role {
    role_arn    = "arn:aws:iam::0000000000:role/AWSAFTExecution"
  }
}

provider "aws" {
  region = "eu-west-3"
  alias = "eu-west-3"
  assume_role {
    role_arn    = "arn:aws:iam::0000000000:role/AWSAFTExecution"
  }
}

Edit: To avoid naming collisions of custom fields and potential AFT introduced fields, one could put the whole custom fields under a custom_fields variable instead and reference that in the jinja template.

#!/bin/bash
# injecting custom fields into jinja template
echo "Main region $CT_MGMT_REGION"
CUSTOM_FIELDS=$(aws --region=$CT_MGMT_REGION ssm get-parameters-by-path --path /aft/account-request/custom-fields/ --query Parameters --output json)
cd $DEFAULT_PATH/$CUSTOMIZATION/terraform

definitions=$(echo $CUSTOM_FIELDS | jq -r $'map(.Name |= split("/")[-1]) | map({ (.Name): (.Value | if type == "string" and (try fromjson catch false) then fromjson else . end) }) | add | "{% set custom_fields = \(.) %}"')

echo -e "Injecting:\n$definitions"
for f in *.jinja; do
  echo -e "$definitions\n$(cat $f)" >$f
done

Will at the top of each *.jinja file inject:

{% set custom_fields = {"env":"staging","region":"eu-west-3","sso_groups":{"Your Group":["AWSAdministratorAccess","AWSReadOnlyAccess"]},"sso_users":{}} %}

Then you can reference it like {{ custom_fields.region }} or similar :)

Flydiverny avatar Mar 02 '23 17:03 Flydiverny