AWS Cloud Operations Blog
OpsWorks for Chef Automate – Automatically Bootstrapping Nodes in Different Accounts
Lots of us today are managing multiple AWS accounts. Although having multiple accounts can bring you benefits, such as more granular control of resources and access, decentralized control, and simpler billing. Multiple accounts can also introduce some challenges. A challenge we face in this blog post is having a centralized configuration management server with its nodes spread throughout other AWS accounts.
Goal
Let’s picture working for a company where various teams own Amazon EC2 instances in their own respective AWS accounts. We have a team dedicated to managing the configuration of all EC2 instances (nodes) across all of the company’s AWS accounts. The configuration management is performed by an OpsWorks for Chef Automate server. How do we go about bootstrapping these nodes to the Chef Automate server?
I’ll show you how you can bootstrap nodes in other AWS accounts to a single/centralized OpsWorks for Chef Automate server. To be precise, I’ll bootstrap an Amazon Linux node in AWS account B to an OpsWorks for Chef Automate server in Account A. To bootstrap the instance I’ll use the user data provided in the Chef Automate starter kit we have downloaded during the creation of the server. This user data script uses the built-in AWS API calls to bootstrap your node.
A few notes before we start
In this tutorial I assume that you don’t have IAM roles created for your Chef nodes, so we are creating everything from scratch. If, in fact, you do have roles configured, then just focus on the policy part of this tutorial. Also, we assume that you have the AWS CLI set up on your workstation with adequate privileges. Additionally, make sure all the ARNs mentioned here have the correct account IDs.
Step 1: Set up IAM permissions on account A, our Chef Automate account
First, we’ll create a role in account A that can be assumed by a node in account B. This role gives the node the necessary permissions to bootstrap itself with the Chef Automate server.
A) Create a policy that allows us to associate the nodes with our Chef Automate server. Create the policy using the following document: owca_allow_associate.json
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"opsworks-cm:AssociateNode",
"opsworks-cm:DescribeNodeAssociationStatus"
],
"Effect": "Allow",
"Resource": [
"*"
]
}
]
}
B) Create the policy with that document, using the AWS CLI:
aws iam create-policy
--policy-name OWCA-AllowAssociate
--policy-document file://owca_allow_associate.json
C) Create a role that can be assumed by the other account (Account B – where the nodes are). Create that role using the following document: owca_trust_policy.json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<NODE-ACCOUNT-ID>:root"
},
"Action": "sts:AssumeRole"
}]
}
D) Now, let’s use the AWS CLI to create the role:
aws iam create-role
--role-name CrossAccount-OWCA
--assume-role-policy-document file://./owca_trust_policy.json
E) And finally, attach the policy we created in step B to the role we created in step D:
aws iam attach-role-policy
--role-name CrossAccountPowerUser
--policy-arn arn:aws:iam::<CHEF-ACCOUNT-ID>:policy/OWCA-AllowAssociate
Step 2: Set up IAM permissions on account B, our nodes account
In this step we create an instance role that will be attached to your node on launch. This role allows the instance to assume a role from account A (the Chef Automate account) so it can associate itself to the Chef Automate server.
A) To create a role, we first need to create a document: role_owca_associate.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
B) Create the role with the document, by running AWS CLI:
aws iam create-role
--role-name AssociateCrossAccount-owca
--assume-role-policy-document file://role_owca_associate.json
C) Create a policy document that allows our instance to assume a role: node_assume_role.json
{
"Version": "2012-10-17",
"Statement": {
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::<CHEF-ACCOUNT-ID>:role/CrossAccount-OWCA "
}
}
D) Create the policy:
aws iam create-policy
--policy-name ChefAssociateAccess
--policy-document file://./node_assume_role.json
E) Finally, attach the policy to the role we created earlier:
aws iam attach-role-policy
--role-name CrossAccountPowerUser
--policy-arn arn:aws:iam::<NODE-ACCOUNT-ID>:policy/AssociateCrossAccount-owca
Step 3: Modifying the user data
In this step, we take the generated user data (userdata.sh
) from the downloaded OpsWorks for Chef Automate starter_kit.zip and modify it with a few more functions that allow us to bootstrap your nodes across accounts.
The modifications we are adding here are quite simple. We are just using aws sts assume-role
to assume the role that we have created in account A (the Chef server account), and use the temporary credentials it generates to run the associate-node
commands, which are required for bootstrapping the node to the Chef Automate server.
Additionally, we are installing the jq utility for JSON parsing – but this is removed during the cleanup.
A) Open up the userdata.sh
file in a text editor and add the following lines on line 21 (just after the set -e -o pipefail
line):
set_cli_role() {
yum install -y jq
ASSUMED_ROLE=$(aws sts assume-role --role-arn arn:aws:iam::<CHEF-ACCOUNT-ID>:role/CrossAccount-OWCA --role-session-name owca)
export AWS_ACCESS_KEY_ID=$(echo $ASSUMED_ROLE | jq .Credentials.AccessKeyId | xargs)
export AWS_SECRET_ACCESS_KEY=$(echo $ASSUMED_ROLE | jq .Credentials.SecretAccessKey | xargs)
export AWS_SESSION_TOKEN=$(echo $ASSUMED_ROLE | jq .Credentials.SessionToken | xargs)
}
cleanup_cli_role(){
yum remove -y jq
unset AWS_SESSION_TOKEN
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
}
The first function we create here (set_cli_role
) is used for assuming the role from account A, and exporting the temporary credentials to environment variables that will be used by our API calls. The second function is just for cleanup – removing of the jq
JSON parser and un-setting the environment variables we set in the function before.
B) Now on the line 87 insert the set_cli_role
function, and on line 93 add the cleanup_cli_role
. Your function execution section should go from this:
install_aws_cli
node_association_status_token="$(associate_node)"
install_chef_client
write_chef_config
install_trusted_certs
wait_node_associated "${node_association_status_token}"
C) To this:
install_aws_cli
set_cli_role
node_association_status_token="$(associate_node)"
install_chef_client
write_chef_config
install_trusted_certs
wait_node_associated "${node_association_status_token}"
cleanup_cli_role
Here we simply change the execution part of the user data shell script to include our two new functions.
D) Save the userdata.sh
somewhere safe.
Step 4: Launch the nodes with the user data script
The only thing left now is to run your nodes with the newly edited user data script. We can achieve this either by adding the user data during a manual launch of the EC2 instance, or by setting this on an Auto Scaling group launch configuration.
Conclusion
In this blog post we’ve enabled your nodes from different AWS accounts to bootstrap themselves to a centralized OpsWorks for Chef Automate server that is present in a separate account. We’ve achieved this by creating the required IAM roles and policies, and also by changing the user data that ships with the OpsWorks for Chef Automate starter kit. We’ve done this only for two accounts, but you can modify this to work on any number of accounts.
About the Author
Darko Meszaros is a Cloud Support Engineer who supports customers that use various AWS automation tools, such as AWS OpsWorks, AWS CodeDeploy, and AWS CloudFormation. He is a subject matter expert for OpsWorks and CodeDeploy. Outside of work, he loves collecting video games and old computers.