8000 Cross account support by ejholmes · Pull Request #553 · cloudtools/stacker · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Cross account support #553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ jobs:
export TERM=xterm
export AWS_DEFAULT_REGION=us-east-1
export STACKER_NAMESPACE=stacker-functional-tests-$CIRCLE_BUILD_NUM
export STACKER_ROLE=arn:aws:iam::487039194316:role/stacker-functional-tests-stacke-FunctionalTestRole-JKTNS8IDNZZ4
sudo -E make test-functional
50 changes: 49 additions & 1 deletion docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,12 @@ A stack has the following keys:
(optional): If provided, specifies the name of the region that the
CloudFormation stack should reside in. If not provided, the default region
will be used (``AWS_DEFAULT_REGION``, ``~/.aws/config`` or the ``--region``
flag).
flag). If both ``region`` and ``profile`` are specified, the value here takes
precedence over the value in the profile.
**profile**:
(optional): If provided, specifies the name of a AWS profile to use when
performing AWS API calls for this stack. This can be used to provision stacks
in multiple accounts or regions.

Here's an example from stacker_blueprints_, used to create a VPC::

Expand Down Expand Up @@ -482,6 +487,48 @@ Note: Doing this creates an implicit dependency from the *webservers* stack
to the *vpc* stack, which will cause stacker to submit the *vpc* stack, and
then wait until it is complete until it submits the *webservers* stack.

Multi Account/Region Provisioning
---------------------------------

You can use stacker to manage CloudFormation stacks in multiple accounts and
regions, and reference outputs across them.

As an example, let's say you had 3 accounts you wanted to manage:

#) OpsAccount: An AWS account that has IAM users for employees.
#) ProdAccount: An AWS account for a "production" environment.
#) StageAccount: An AWS account for a "staging" environment.

You want employees with IAM user accounts in OpsAccount to be able to assume
roles in both the ProdAccount and StageAccount. You can use stacker to easily
manage this::


stacks:
# Create some stacks in both the "prod" and "stage" accounts with IAM roles
# that employees can use.
- name: prod/roles
profile: prod
class_path: blueprints.Roles
- name: stage/roles
profile: stage
class_path: blueprints.Roles

# Create a stack in the "ops" account and grant each employee access to
# assume the roles we created above.
- name: users
profile: ops
class_path: blueprints.IAMUsers
variables:
Users:
john-smith:
Roles:
- ${output prod/roles::EmployeeRoleARN}
- ${output stage/roles::EmployeeRoleARN}


Note how I was able to reference outputs from stacks in multiple accounts using the `output` plugin!

Environments
============

Expand Down Expand Up @@ -511,3 +558,4 @@ submitting it to CloudFormation. For more information, see the
.. _Mappings: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html
.. _Outputs: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html
.. _stacker_blueprints: https://github.com/remind101/stacker_blueprints
.. _`AWS profiles`: https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html
3 changes: 2 additions & 1 deletion stacker/actions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ def post_run(self, *args, **kwargs):
def build_provider(self, stack):
"""Builds a :class:`stacker.providers.base.Provider` suitable for
operating on the given :class:`stacker.Stack`."""
return self.provider_builder.build(region=stack.region)
return self.provider_builder.build(region=stack.region,
profile=stack.profile)

@property
def provider(self):
Expand Down
2 changes: 2 additions & 0 deletions stacker/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ class Stack(Model):

region = StringType(serialize_when_none=False)

profile = StringType(serialize_when_none=False)

class_path = StringType(serialize_when_none=False)

template_path = StringType(serialize_when_none=False)
Expand Down
4 changes: 2 additions & 2 deletions stacker/providers/aws/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,13 +424,13 @@ def __init__(self, region=None, **kwargs):
self.region = region
self.kwargs = kwargs

def build(self, region=None):
def build(self, region=None, profile=None):
# TODO(ejholmes): This class _could_ cache built providers, however,
# the building of boto3 clients is _not_ threadsafe. See
# https://github.com/boto/boto3/issues/801#issuecomment-245455979
if not region:
region = self.region
session = get_session(region=region)
session = get_session(region=region, profile=profile)
return Provider(session, region=region, **self.kwargs)


Expand Down
14 changes: 7 additions & 7 deletions stacker/session_cache.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import json
import os
import botocore
import boto3
import logging
from .ui import ui


def get_session(region):
def get_session(region, profile=None):
"""Creates a boto3 session with a cache

Args:
region (str): The region for the session
profile (str): The profile for the session

Returns:
:class:`boto3.session.Session`: A boto3 session with
credential caching
"""
session = botocore.session.get_session()
if region is not None:
session.set_config_vari 9E81 able('region', region)
c = session.get_component('credential_provider')
session = boto3.Session(region_name=region, profile_name=profile)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the reason we had to use botocore originally was because boto3 didn't have this ability at the time. Do we need to update our dependency for boto3 to a specific minimum version for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We lock on "boto3>=1.3.1", which has support for both of these params, so we should be ok.

c = session._session.get_component('credential_provider')
provider = c.get_provider('assume-role')
provider.cache = CredentialCache()
return boto3.session.Session(botocore_session=session)
provider._prompter = ui.getpass
return session


logger = logging.getLogger(__name__)
Expand Down
1 change: 1 addition & 0 deletions stacker/stack.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(self, definition, context, variables=None, mappings=None,
self.name = definition.name
self.fqn = context.get_fqn(definition.stack_name or self.name)
self.region = definition.region
self.profile = definition.profile
self.definition = definition
self.variables = _gather_variables(definition)
self.mappings = mappings
Expand Down
1 change: 1 addition & 0 deletions stacker/tests/actions/test_destroy.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def __init__(self, name, tags=None, **kwargs):
self.name = name
self.fqn = name
self.region = None
self.profile = None
self.requires = []


Expand Down
2 changes: 1 addition & 1 deletion stacker/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def __init__(self, provider, region=None):
self.provider = provider
self.region = region

def build(self, region):
def build(self, region, profile):
return self.provider


Expand Down
34 changes: 32 additions & 2 deletions stacker/tests/fixtures/mock_blueprints.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from troposphere import GetAtt, Output, Sub, Ref
from troposphere import iam

from awacs.aws import Policy, Statement
from awacs.aws import Policy, Statement, AWSPrincipal
import awacs
import awacs.s3
import awacs.cloudformation
import awacs.iam
import awacs.sts

from troposphere.cloudformation import WaitCondition, WaitConditionHandle

Expand Down Expand Up @@ -94,11 +95,36 @@ def create_template(self):
awacs.cloudformation.DescribeStacks,
awacs.cloudformation.DescribeStackEvents])]))

principal = AWSPrincipal(Ref("AWS::AccountId"))
role = t.add_resource(
iam.Role(
"FunctionalTestRole",
AssumeRolePolicyDocument=Policy(
Statement=[
Statement(
Effect="Allow",
Action=[
awacs.sts.AssumeRole],
Principal=principal)]),
Policies=[
stacker_policy]))

assumerole_policy = iam.Policy(
PolicyName="AssumeRole",
PolicyDocument=Policy(
Statement=[
Statement(
Effect="Allow",
Resource=[GetAtt(role, "Arn")],
Action=[
awacs.sts.AssumeRole])]))

user = t.add_resource(
iam.User(
"FunctionalTestUser",
Policies=[
stacker_policy]))
stacker_policy,
assumerole_policy]))

key = t.add_resource(
iam.AccessKey(
Expand All @@ -112,6 +138,10 @@ def create_template(self):
Output(
"SecretAccessKey",
Value=GetAtt("FunctionalTestKey", "SecretAccessKey")))
t.add_output(
Output(
"FunctionalTestRole",
Value=GetAtt(role, "Arn")))


class Dummy(Blueprint):
Expand Down
9 changes: 9 additions & 0 deletions stacker/ui.py
CEB7
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import threading
import logging
from getpass import getpass


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -48,6 +49,14 @@ def ask(self, message):
finally:
self.unlock()

def getpass(self, *args):
"""Wraps getpass to lock the UI."""
try:
self.lock()
return getpass(*args)
finally:
self.unlock()


# Global UI object for other modules to use.
ui = UI()
2 changes: 1 addition & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ This directory contains the functional testing suite for stacker. It exercises a

```console
$ export STACKER_NAMESPACE=my-stacker-test-namespace
$ export AWS_DEFAULT_REGION=us-east-1
```
3. Generate an IAM user for the test suite to use:

Expand All @@ -24,6 +23,7 @@ This directory contains the functional testing suite for stacker. It exercises a
$ ./stacker.yaml.sh | stacker info -
$ export AWS_ACCESS_KEY_ID=access-key
$ export AWS_SECRET_ACCESS_KEY=secret-access-key
$ export STACKER_ROLE=<FunctionalTestRole>
```
5. Run the test suite:

Expand Down
25 changes: 25 additions & 0 deletions tests/suite.bats
Original file line number Diff line number Diff line change
Expand Up @@ -909,3 +909,28 @@ EOF
assert_has_line "${STACKER_NAMESPACE}-app: submitted (creating new stack)"
assert_has_line "${STACKER_NAMESPACE}-app: complete (creating new stack)"
}

@test "stacker build - profiles" {
needs_aws

config() {
cat <<EOF
namespace: ${STACKER_NAMESPACE}
stacks:
- name: vpc
profile: stacker
class_path: stacker.tests.fixtures.mock_blueprints.Dummy
EOF
}

teardown() {
stacker destroy --force <(config)
}

# Create the new stacks.
stacker build <(config)
assert "$status" -eq 0
assert_has_line "Using default AWS provider mode"
assert_has_line "${STACKER_NAMESPACE}-vpc: submitted (creating new stack)"
assert_has_line "${STACKER_NAMESPACE}-vpc: complete (creating new stack)"
}
20 changes: 20 additions & 0 deletions tests/test_helper.bash
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ if [ -z "$STACKER_NAMESPACE" ]; then
exit 1
fi

if [ -z "$STACKER_ROLE" ]; then
>&2 echo "To run these tests, you must set a STACKER_ROLE environment variable"
exit 1
fi

# Setup a base .aws/config that can be use to test stack configurations that
# require stacker to assume a role.
export AWS_CONFIG_DIR=$(mktemp -d)
export AWS_CONFIG_FILE="$AWS_CONFIG_DIR/config"

cat <<EOF > "$AWS_CONFIG_FILE"
[default]
region = us-east-1

[profile stacker]
region = us-east-1
role_arn = ${STACKER_ROLE}
credential_source = Environment
EOF

# Simple wrapper around the builtin bash `test` command.
assert() {
builtin test "$@"
Expand Down
0