terraform-aws-control_tower_account_factory
terraform-aws-control_tower_account_factory copied to clipboard
Deploy resources in region specified by custom_fields
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.
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
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.
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.
Yes that is correct!
Got it! I've gone ahead and created a backlog for this, thanks!
+1
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.
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.
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.
#!/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 :)