Hi and welcome!

AWS multi account setup with Terraform 24-10-2015

awsterraformdevops

We are going to create an environment with 2 separate AWS accounts.

First account will be the main one, the gateway to other accounts. This account will contain all our IAM users and roles.

Second account will be the actual workhorse with instances, VPC, etc..

See an official guide in AWS documentation with similar setup.

Why?

This post is greatly inspired by excellent blog from Segment, about how they rethought and rebuild their AWS infrastructure and why. I advise you to read their blog before continue, but in short….

By using several AWS accounts:

  • You can separate and group your resources in natural and simple way;

  • You can truly isolate different projects or environments, but still have possibility to manage access rights in one central location;

  • By setting up consolidated billing you can review and manage costs across projects and environments in clear way;

  • If one of your projects will be bought by Google, then it will be trivial to change ownership of project and pass it to new owner;

  • And a small but pleasant bonus: each new account you will create is eligible for free usage tier. 5 accounts ⇒ 5 free t2.micro instances for a year. Nice!

Before you start

You should have 2 AWS accounts. Let’s name first - "main", and second - "dev". In each account create a user with administrator rights. Write down access credentials. We will use these users to access AWS from Terraform.

Also you need to know AWS accounts ids.

Let’s define a variables:

variables.tf
variable "main_account_id" {}
variable "main_access_key" {}
variable "main_secret_key" {}

variable "dev_account_id" {}
variable "dev_access_key" {}
variable "dev_secret_key" {}

variable "aws_region" {}

Please, refer to Terraform documentation about how to pass values for these variables.

Obviously, you need to install Terraform to proceed.

Setup dev account

dev.tf
# setup aws provider for dev account
provider "aws" {
  alias = "dev"
  region = "${var.aws_region}"
  access_key = "${var.dev_access_key}"
  secret_key = "${var.dev_secret_key}"
}

# in dev account create iam policy, which will grants admin rights
resource "aws_iam_policy" "external_admin_policy" {
    provider = "aws.dev"
    name = "ExternalAdminPolicy"
    path = "/"
    policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        }
    ]
}
EOF
}

# in dev account create a role which can be assumed by main account
resource "aws_iam_role" "external_admin_role" {
    provider = "aws.dev"
    name = "ExternalAdminRole"
    assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::${var.main_account_id}:root"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

# attach policy to role
resource "aws_iam_policy_attachment" "external_admin_policy_attachment_to_external_admin_role" {
    provider = "aws.dev"
    name = "external_admin_policy_attachment"
    roles = ["${aws_iam_role.external_admin_role.name}"]
    policy_arn = "${aws_iam_policy.external_admin_policy.arn}"
}

So, here we register AWS provider for dev environment. We use it explicitly in AWS resources definitions.

We create a special role "ExternalAdminRole", which can be "assumed" from our main account. And we attach an administrator policy to that role. This means, that if someone from main account will "assume" "ExternalAdminRole", then he will be granted with administrator rights in dev account.

Setup main account

main.tf
# setup aws provider for main account
provider "aws" {
    alias = "main"
    region = "${var.aws_region}"
    access_key = "${var.main_access_key}"
    secret_key = "${var.main_secret_key}"
}

# create a group, which will be able to assume "ExternalAdminRole" from dev account
resource "aws_iam_group" "dev_admins" {
    provider = "aws.main"
    name = "DevAdminsGroup"
}

# create a group policy, which allows to assume "ExternalAdminRole"
resource "aws_iam_group_policy" "dev_admins_policy" {
    provider = "aws.main"
    name = "DevAdminsPolicy"
    group = "${aws_iam_group.dev_admins.id}"
    policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Allow",
        "Action": "sts:AssumeRole",
        "Resource": "${aws_iam_role.external_admin_role.arn}"
    }
}
EOF
}

# create a user "bob"
resource "aws_iam_user" "bob" {
    provider = "aws.main"
    name = "bob"
}

# add "bob" to dev_admins group
resource "aws_iam_group_membership" "dev_admins" {
    provider = "aws.main"
    name = "dev_admins_group_membership"
    users = [
        "${aws_iam_user.bob.name}"
    ]
    group = "${aws_iam_group.dev_admins.name}"
}

Here we define a group, members of which can "assume" "ExternalAdminRole". Also we create an IAM user "bob".

So now we have a user bob in main account. You can allow bob to login to main account’s AWS Console. But bob isn’t allowed to do anything inside main account - he can’t launch instances or view billing information, etc… All he can do - he can assume "ExternalAdminRole" and get admin rights for dev account.

Bob can login into dev account thru main account using the following URL:

https://signin.aws.amazon.com/switchrole?account=[dev_account_id]&roleName=ExternalAdminRole

How to work with dev account?

Let’s assume that we have a different project, which wants to use dev account to setup some applications in it. So the project has it’s own Terraform scripts to create VPC, start instances, etc…

Now, how bob can run Terraform against dev account?

As it is stated in AWS documentation, bob should firstly assume the "ExternalAdminRole" by running following AWS CLI command:

aws sts assume-role --role-arn "arn:aws:iam::[dev_account_id]:role/ExternalAdminRole" --role-session-name "bob_dev"

This command will return temporal access keys for dev account, which will look something like:

{
    "Credentials": {
        "SecretAccessKey": "111111111111111111",
        "SessionToken": "222222222222222222222,
        "Expiration": "2014-12-11T23:08:07Z",
        "AccessKeyId": "33333333333333333333"
    }
}

Terraform right now doesn’t support authentication against AWS provider by "assuming" roles. So before running Terraform you need to get these temporal credentials and use them against Terraform.

One of the ways to configure AWS provider in Terraform is by using environment variables. So you just skip credential variables in AWS provider configuration, assuming that they will be passed thru environment variables. For example:

provider "aws" {
    region = "${var.aws_region}"
}

And use some wrapper script for Terraform commands. For example:

assume_dev_admin.sh
#!/bin/bash

CMD="$@"
DEV_ACCOUNT_ID=11111111111

ASSUME_ROLE="arn:aws:iam::${DEV_ACCOUNT_ID}:role/ExternalAdminRole"
ROLE_SESSION_NAME="dev"
TMP_FILE=".temp_credentials"

aws sts assume-role --output json --role-arn ${ASSUME_ROLE} --role-session-name ${ROLE_SESSION_NAME} > ${TMP_FILE}

ACCESS_KEY=$(cat ${TMP_FILE} | jq -r ".Credentials.AccessKeyId")
SECRET_KEY=$(cat ${TMP_FILE} | jq -r ".Credentials.SecretAccessKey")
SESSION_TOKEN=$(cat ${TMP_FILE} | jq -r ".Credentials.SessionToken")
EXPIRATION=$(cat ${TMP_FILE} | jq -r ".Credentials.Expiration")

echo "Retrieved temp access key ${ACCESS_KEY} for role ${ASSUME_ROLE}. Key will expire at ${EXPIRATION}"

AWS_ACCESS_KEY_ID=${ACCESS_KEY} AWS_SECRET_ACCESS_KEY=${SECRET_KEY} AWS_SESSION_TOKEN=${SESSION_TOKEN} ${CMD}

Now to apply Terraform bob should run (assuming that he is configured AWS credentials for main account):

./assume_dev_admin.sh terraform apply

Final words

This is a very basic recipe. But it should give you a good start point if you will decide that multi AWS account setup is right for you. I hope this will help someone and will save some hours.