Variably create 1 or more AWS Buckets across multiple regions - amazon-web-services

I’m looking for recommendations and help with an issue that I am having with setting up and managing bucket and bucket policy creation for multiple environments and multiple regions within a single environment.
I have 4 AWS accounts (dev, stg, prod1, prod2 which is a copy of prod1). In prod1 we have two kubernetes clusters aws-us-prod1 and aws-eu-prod1. These two clusters are completely independent of one another and they merely serve customers in those regions.
I have an applications running on these two different clusters (aws-us-prod1 and aws-eu-prod1) that need to write content to an S3 bucket. But these two clusters share an AWS account (prod1).
I’m trying to write some terraform resource automation to manage this, and I haven’t been able to variably control what region a bucket gets put in. The latest doc shows that there is a region attribute but it doesn’t work because of how the provider has been implemented with the aws provider region attribute.
What I’d like to do is something like this:
variable "buckets" {
type = map(string) # e.g. buckets='{"a-us-prod1": "us-west-2", "a-eu-prod1":"eu-west-2"}'
}
resource "aws_s3_bucket" "my_buckets" {
for_each = var.buckets
bucket = each.key
region = each.value
}
resource "aws_s3_bucket_policy" "my_buckets_policy" {
for_each = aws_s3_bucket.my_buckets
bucket = each.value.id
policy = ...
}
I’ve tried using multiple providers using aliases, but you can’t programmatically use a provider based on the value of a variable you are iterating over. What’s the proper way to organize this project and resources to accomplish this?
These issues I have come across are related to this:
https://github.com/hashicorp/terraform/issues/3656
https://github.com/terraform-providers/terraform-provider-aws/issues/5999

The region attribute just got removed from the s3_bucket in terraform-provider-aws v3.0.0 from July 31, 2020. Before then you could set the region for a bucket and it would have been respected and the bucket would have been created in that selected region. However that was not how any other resource is managed, it was probably just there because S3 is globally scoped and the bucket has no region in the arn. All other services use the region of the provider itself (as it should be).
I would recommend to create the different providers for all the different regions you may want to support and then splitting the var.buckets according to their region and then create one resource "aws_s3_bucket" "this_region" { } for each region:
variable "buckets" {
type = map(string) # e.g. buckets='{"a-us-prod1": "us-west-2", "a-eu-prod1":"eu-west-2"}'
}
provider "aws" {
region = "eu-west-2"
alias = "eu-west-2"
}
locals {
eu_west_2_buckets = [for name, region in var.buckets: name if region == "eu-west-2"]
}
resource "aws_s3_bucket" "eu_west_2_buckets" {
count = length(local.eu_west_2_buckets)
bucket = eu_west_2_buckets[count.index]
provider = aws.eu-west-2
}
If you want to only rollout the buckets that match the current deployment region you can do that by simply changing the bucket filtering logic:
variable "buckets" {
type = map(string) # e.g. buckets='{"a-us-prod1": "us-west-2", "a-eu-prod1":"eu-west-2"}'
}
locals {
buckets = [for name, region in var.buckets: name if region == data.aws_region.current.name]
}
data "aws_region" "current" { }
resource "aws_s3_bucket" "buckets" {
count = length(local.buckets)
bucket = buckets[count.index]
}

Related

AWS - Creating resources in a multi-account environment

I just created a new AWS account using Terraform aws_organizations_account module. What I am now trying to do is to create ressources into that new account. I guess I would need the account_id of the new AWS account to do that so I stored it into a new output variable but after that I have no idea how can I create a aws_s3_bucket for example
provider.tf
provider "aws" {
region = "us-east-1"
}
main.tf
resource "aws_organizations_account" "account" {
name = "tmp"
email = "first.last+tmp#company.com"
role_name = "myOrganizationRole"
parent_id = "xxxxx"
}
## what I am trying to create inside that tmp account
resource "aws_s3_bucket" "bucket" {}
outputs.tf
output "account_id" {
value = aws_organizations_account.account.id
sensitive = true
}
You can't do this the way you want. You need entire, account creation pipeline for that. Roughly in the pipeline you would have two main stages:
Create your AWS Org and member accounts.
Assume role from the member accounts, and run your TF code for these accounts to create resources.
There are many ways of doing this, and also there are many resources on this topic. Some of them are:
How to Build an AWS Multi-Account Strategy with Centralized Identity Management
Setting up an AWS organization from scratch with Terraform
Terraform on AWS: Multi-Account Setup and Other Advanced Tips
Apart from those, there is also AWS Control Tower, which can be helpful in setting up initial multi-account infrastructure.

Applying Terraform On Two Different AWS Accounts In the Same Plan

We're currently porting some of our CloudFormation templates to Terraform. In one of these templates we use a custom resource with a Lambda function.
The purpose of the function is to assume a role in our main AWS account; where R53 DNS is managed, and add a newly generated CloudFront dns there.
I am wondering if there's a way to do this in terraform, such that:
create the cloudfront resource, alb, etc on the dev/qa/prod accounts
add the r53 recordset to the main account
All within the same terraform plan. Can I choose an IAM role when creating a resource? Or choose the account where the resource should be created?
The only reference I have found is here
You can configure multiple providers ( one per account in your case) and create an alias for each. Then you will need to specify the provider for each ressource. Example:
provider "aws" {
region = "eu-west-1"
profile = "profile1"
alias = "account1"
}
provider "aws" {
region = "eu-west-1"
profile = "profile2"
alias = "account2"
}
resource "aws_lambda_function" "function1" {
provider = "aws.account1" // will be created in account 1
...
}
resource "aws_lambda_function" "function2" {
provider = "aws.account2" // will be created in account 2
...
}

Reference variables from another Terraform plan

I have created a set-up with main and disaster recovery website architecture in AWS using Terraform.
The main website is in region1 and disaster recovery is in region2. This script is created as different plans or different directories.
For region1, I created one directory which contains only the main website Terraform script to launch the main website infrastructure.
For region2, I created another directory which contains only the disaster recovery website Terraform script to launch the disaster recovery website infrastructure.
In my main website script, I need some values of the disaster recovery website such as VPC peering connection ID, DMS endpoint ARNs etc.
How can I reference these variables from the disaster recovery website directory to the main website directory?
One option is to use the terraform_remote_state data source to fetch outputs from the other state file like this:
vpc/main.tf
resource "aws_vpc" "foo" {
cidr_block = "10.0.0.0/16"
}
output "vpc_id" {
value = "${aws_vpc.foo.id}"
}
route/main.tf
data "terraform_remote_state" "vpc" {
backend = "s3"
config {
bucket = "mybucket"
key = "path/to/my/key"
region = "us-east-1"
}
}
resource "aws_route_table" "rt" {
vpc_id = "${data.terraform_remote_state.vpc.vpc_id}"
}
However, it's nearly always better to just use the native data sources of the provider as long as they exist for the resource you need.
So in your case you will need to use data sources such as the aws_vpc_peering_connection data source to be able to establish cross VPC routing with something like this:
data "aws_vpc_peering_connection" "pc" {
vpc_id = "${data.aws_vpc.foo.id}"
peer_cidr_block = "10.0.0.0/16"
}
resource "aws_route_table" "rt" {
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_route" "r" {
route_table_id = "${aws_route_table.rt.id}"
destination_cidr_block = "${data.aws_vpc_peering_connection.pc.peer_cidr_block}"
vpc_peering_connection_id = "${data.aws_vpc_peering_connection.pc.id}"
}
You'll need to do similar things for any other IDs or things you need to reference in your DR region.
It's worth noting that there's not any data sources for the DMS resources so you would either need to use the terraform_remote_state data source to fetch any IDs (such as the source and target endpoint ARNs to setup the aws_dms_replication_task or you could structure things so that all of the DMS stuff happens in the DR region and then you only need to refer to the other region's VPC ID, database names and potentially KMS key IDs which can all be done via data sources.

Sharing resources between Terraform workspaces

I have an infrastructure I'm deploying using Terraform in AWS. This infrastructure can be deployed to different environments, for which I'm using workspaces.
Most of the components in the deployment should be created separately for each workspace, but I have several key components that I wish to be shared between them, primarily:
IAM roles and permissions
They should use the same API Gateway, but each workspace should deploy to different paths and methods
For example:
resource "aws_iam_role" "lambda_iam_role" {
name = "LambdaGeneralRole"
policy = <...>
}
resource "aws_lambda_function" "my_lambda" {
function_name = "lambda-${terraform.workspace}"
role = "${aws_iam_role.lambda_iam_role.arn}"
}
The first resource is a IAM role that should be shared across all instances of that Lambda, and shouldn't be recreated more than once.
The second resource is a Lambda function whose name depends on the current workspace, so each workspace will deploy and keep track of the state of a different Lambda.
How can I share resources, and their state, between different Terraform workspaces?
For the shared resources, I create them in a separate template and then refer to them using terraform_remote_state in the template where I need information about them.
What follows is how I implement this, there are probably other ways to implement it. YMMV
In the shared services template (where you would put your IAM role) I use Terraform backend to store the output data for the shared services template in Consul. You also need to output any information you want to use in other templates.
shared_services template
terraform {
backend "consul" {
address = "consul.aa.example.com:8500"
path = "terraform/shared_services"
}
}
resource "aws_iam_role" "lambda_iam_role" {
name = "LambdaGeneralRole"
policy = <...>
}
output "lambda_iam_role_arn" {
value = "${aws_iam_role.lambda_iam_role.arn}"
}
A "backend" in Terraform determines how state is loaded and how an operation such as apply is executed. This abstraction enables non-local file state storage, remote execution, etc.
In the individual template you invoke the backend as a data source using terraform_remote_state and can use the data in that template.
terraform_remote_state:
Retrieves state meta data from a remote backend
individual template
data "terraform_remote_state" "shared_services" {
backend = "consul"
config {
address = "consul.aa.example.com:8500"
path = "terraform/shared_services"
}
}
# This is where you use the terraform_remote_state data source
resource "aws_lambda_function" "my_lambda" {
function_name = "lambda-${terraform.workspace}"
role = "${data.terraform_remote_state.shared_services.lambda_iam_role_arn}"
}
References:
https://www.terraform.io/docs/state/remote.html
https://www.terraform.io/docs/backends/
https://www.terraform.io/docs/providers/terraform/d/remote_state.html
Resources like aws_iam_role having a name attribute will not create a new instance if the name value matches an already provisioned resource.
So, the following will create a single aws_iam_role named LambdaGeneralRole.
resource "aws_iam_role" "lambda_iam_role" {
name = "LambdaGeneralRole"
policy = <...>
}
...
resource "aws_iam_role" "lambda_iam_role_reuse_existing_if_name_is_LambdaGeneralRole" {
name = "LambdaGeneralRole"
policy = <...>
}
Similarly, the aws provider will effectively creat one S3 bucket name my-store given the following:
resource "aws_s3_bucket" "store-1" {
bucket = "my-store"
acl = "public-read"
force_destroy = true
}
...
resource "aws_s3_bucket" "store-2" {
bucket = "my-store"
acl = "public-read"
force_destroy = true
}
This behaviour holds even if the resources were defined different workspaces with their respective separate Terraform state.
To get the best of this approach, define the shared resources as separate configuration. That way, you don't risk destroying a shared resource after running terraform destroy.

Terraform state locking using DynamoDB

Our Terraform layout is such that we run Terraform for many aws (100+) accounts, and save Terraform state file remotely to a central S3 bucket.
The new locking feature sounds useful and wish to implement it but I am unsure if I can make use of a central DynamoDB table in the same account as that of our S3 bucket or do I need to create a DynamoDB table in each of the AWS accounts?
You can use a single DynamoDB table to control the locking for the state file for all of the accounts. This would work even if you had multiple S3 buckets to store state in.
The DynamoDB table is keyed on LockID which is set as a bucketName/path. So as long as you have a unique combination of those you will be fine (you should or you have bigger problems with your state management).
Obviously you will need to set up cross account IAM policies to allow users creating things in one account to be able to manage items in DynamoDB.
To use terraform DynamoDB locking, follow the steps below
1.Create an AWS DynamoDB with terraform to lock the terraform.tfstate.
provider "aws" {
region = "us-east-2"
}
resource "aws_dynamodb_table" "dynamodb-terraform-lock" {
name = "terraform-lock"
hash_key = "LockID"
read_capacity = 20
write_capacity = 20
attribute {
name = "LockID"
type = "S"
}
tags {
Name = "Terraform Lock Table"
}
}
2.Execute terraform to create the DynamoDB table on AWS
terraform apply
Usage Example
1.Use the DynamoDB table to lock terraform.state creation on AWS. As an EC2 example
terraform {
backend "s3" {
bucket = "terraform-s3-tfstate"
region = "us-east-2"
key = "ec2-example/terraform.tfstate"
dynamodb_table = "terraform-lock"
encrypt = true
}
}
provider "aws" {
region = "us-east-2"
}
resource "aws_instance" "ec2-example" {
ami = "ami-a4c7edb2"
instance_type = "t2.micro"
}
The dynamodb_table value must match the name of the DynamoDB table we created.
2.Initialize the terraform S3 and DynamoDB backend
terraform init
3.Execute terraform to create EC2 server
terraform apply
To see the code, go to the
Github DynamoDB Locking Example