Skip to content
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

Cross-account parameter resolving #292

Merged
merged 21 commits into from
Dec 23, 2019
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d72a685
Add RoleAssumer class
petervandoros Nov 12, 2019
98c3b03
Update ParameterResolver to support assuming roles
petervandoros Nov 12, 2019
f14b567
Use existing cloudformation driver's class to create new instance
petervandoros Nov 12, 2019
7015ef3
Add integration test for assuming a role for parameter resolving
petervandoros Nov 12, 2019
c96653a
Add parameter resolver assume role docs to readme
petervandoros Nov 12, 2019
d8a7bd6
Minor typo fix in readme
petervandoros Nov 12, 2019
31bbaa3
Be explicit about all parameters can assume a role
petervandoros Nov 13, 2019
83934f3
Remove unused code in SnsTopicName
petervandoros Nov 14, 2019
fbc6d65
Don't cache AmiFinder instance in latest_ami resolver
petervandoros Dec 11, 2019
8a9f15d
Don't cache SSM client in parameter_store resolver
petervandoros Dec 11, 2019
fd78c9d
Fix stack_output tests verifying stack caching
petervandoros Dec 11, 2019
bb447b6
Update stack_output resolver to take credentials into account
petervandoros Dec 11, 2019
530ed3d
Update ejson parameter resolve to take credentials into account
petervandoros Dec 11, 2019
95a8825
Refactor RoleAssumer for clarity
petervandoros Dec 16, 2019
7ff4180
Merge branch 'master' into assume-role-parameter-resolving
petervandoros Dec 16, 2019
41592b3
Update changelog to include cross-account parameter resolving
petervandoros Dec 16, 2019
56cc3a6
Whitespace between rspec contexts
petervandoros Dec 16, 2019
4ad4ade
Minor grammatical tweak to the README
petervandoros Dec 16, 2019
2905ad2
Don't install bundler in Travis CI
petervandoros Dec 16, 2019
9da0c49
Revert "Don't install bundler in Travis CI"
petervandoros Dec 17, 2019
b21ea00
Merge branch 'master' into assume-role-parameter-resolving
petervandoros Dec 17, 2019
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ The format is based on [Keep a Changelog], and this project adheres to

- Project metadata to the gemspec ([#293]).

- Enable cross-account parameter resolving ([#292])

[Unreleased]: https://github.com/envato/stack_master/compare/v1.17.1...HEAD
[#293]: https://github.com/envato/stack_master/pull/293
[#292]: https://github.com/envato/stack_master/pull/292

## [1.17.1] - 2019-10-3

Expand Down
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,47 @@ One benefit of using parameter resolvers instead of hard coding values like VPC
IDs and resource ARNs is that the same configuration works cross
region/account, even though the resolved values will be different.

### Cross-account parameter resolving

One way to resolve parameter values from different accounts to the one StackMaster runs in, is to
assume a role in another account with the relevant IAM permissions to execute successfully.

This is supported in StackMaster by specifying the `role` and `account` properties for the
parameter resolver in the stack's parameters file.

All parameter resolvers are supported.

```yaml
vpc_peering_id:
role: cross-account-parameter-resolver
account: 1234567890
stack_output: vpc-peering-stack-in-other-account/peering_name
petervandoros marked this conversation as resolved.
Show resolved Hide resolved

an_array_param:
role: cross-account-parameter-resolver
account: 1234567890
stack_outputs:
- stack-in-account1/output
- stack-in-account1/another_output

another_array_param:
- role: cross-account-parameter-resolver
account: 1234567890
stack_output: stack-in-account1/output
- role: cross-account-parameter-resolver
account: 0987654321
stack_output: stack-in-account2/output

my_secret:
role: cross-account-parameter-resolver
account: 1234567890
parameter_store: ssm_parameter_name
```

An example of use case where cross-account parameter resolving is particularly useful is when
setting up VPC peering where you need the VPC ID of the peer. Without the ability to assume
a role in another account, the only option was to hard code the peer's VPC ID.

### Stack Output

The stack output parameter resolver looks up outputs from other stacks in the
Expand Down Expand Up @@ -270,7 +311,7 @@ db_password:
```

### 1Password Lookup
An Alternative to the alternative secret store is accessing 1password secrets using the 1password cli (`op`).
An alternative to the alternative secret store is accessing 1password secrets using the 1password cli (`op`).
petervandoros marked this conversation as resolved.
Show resolved Hide resolved
You declare a 1password lookup with the following parameters in your parameters file:

```
Expand Down
1 change: 1 addition & 0 deletions features/apply.feature
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ Feature: Apply command
Then the exit status should be 0

Scenario: Create a stack using a stack output resolver
Given I stub the CloudFormation driver
Given a file named "parameters/myapp_web.yml" with:
"""
VpcId:
Expand Down
57 changes: 57 additions & 0 deletions features/apply_with_assume_role_parameter_resolvers.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
Feature: Apply command with assume role parameter resolvers
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️


Background:
Given a file named "stack_master.yml" with:
"""
stacks:
us-east-2:
vpc:
template: vpc.rb
myapp_web:
template: myapp_web.rb
"""
And a directory named "parameters"
And a file named "parameters/myapp_web.yml" with:
"""
vpc_id:
role: my-role
account: 1234567890
stack_output: vpc/vpc_id
"""
And a directory named "templates"
And a file named "templates/myapp_web.rb" with:
"""
SparkleFormation.new(:myapp_web) do
description "Test template"
set!('AWSTemplateFormatVersion', '2010-09-09')

parameters.vpc_id do
description 'VPC ID'
type 'AWS::EC2::VPC::Id'
end

resources.test_sg do
type 'AWS::EC2::SecurityGroup'
properties do
group_description 'Test SG'
vpc_id ref!(:vpc_id)
end
end
end
"""

Scenario: Run apply and create a new stack
Given I stub the CloudFormation driver
Given I stub the following stack events:
| stack_id | event_id | stack_name | logical_resource_id | resource_status | resource_type | timestamp |
| 1 | 1 | myapp-web | myapp-web | CREATE_COMPLETE | AWS::CloudFormation::Stack | 2020-10-29 00:00:00 |
And I stub the following stacks:
| stack_id | stack_name | parameters | outputs | region |
| 1 | vpc | VpcCidr=10.0.0.16/22 | VpcId=vpc-id | us-east-2 |
| 2 | myapp_web | | | us-east-2 |
Then I expect the role "my-role" is assumed in account "1234567890"
When I run `stack_master apply us-east-2 myapp_web --trace`
And the output should contain all of these lines:
| +--- |
| +VpcId: vpc-id |
Then the exit status should be 0
6 changes: 6 additions & 0 deletions features/step_definitions/asume_role_steps.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Then(/^I expect the role "([^"]*)" is assumed in account "([^"]*)"$/) do |role, account|
expect(Aws::AssumeRoleCredentials).to receive(:new).with(
role_arn: "arn:aws:iam::#{account}:role/#{role}",
role_session_name: instance_of(String)
)
end
4 changes: 4 additions & 0 deletions features/step_definitions/stack_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ def extract_hash_from_kv_string(string)
allow(StackMaster.cloud_formation_driver).to receive(:validate_template).and_raise(Aws::CloudFormation::Errors::ValidationError.new('', message))
end

Given(/^I stub the CloudFormation driver$/) do
allow(StackMaster.cloud_formation_driver.class).to receive(:new).and_return(StackMaster.cloud_formation_driver)
end

When(/^an S3 file in bucket "([^"]*)" with key "([^"]*)" exists with content:$/) do |bucket, key, body|
file = StackMaster.s3_driver.find_file(bucket: bucket, object_key: key)
expect(file).to eq body
Expand Down
1 change: 1 addition & 0 deletions lib/stack_master.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module StackMaster
autoload :SecurityGroupFinder, 'stack_master/security_group_finder'
autoload :ParameterLoader, 'stack_master/parameter_loader'
autoload :ParameterResolver, 'stack_master/parameter_resolver'
autoload :RoleAssumer, 'stack_master/role_assumer'
autoload :ResolverArray, 'stack_master/resolver_array'
autoload :Resolver, 'stack_master/resolver_array'
autoload :Utils, 'stack_master/utils'
Expand Down
37 changes: 31 additions & 6 deletions lib/stack_master/parameter_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,37 @@ def resolve_parameter_value(key, parameter_value)
return parameter_value.to_s if Numeric === parameter_value || parameter_value == true || parameter_value == false
return resolve_array_parameter_values(key, parameter_value).join(',') if Array === parameter_value
return parameter_value unless Hash === parameter_value
validate_parameter_value!(key, parameter_value)
resolve_parameter_resolver_hash(key, parameter_value)
rescue Aws::CloudFormation::Errors::ValidationError
raise InvalidParameter, $!.message
end

def resolve_parameter_resolver_hash(key, parameter_value)
# strip out account and role
resolver_hash = parameter_value.except('account', 'role')
account, role = parameter_value.values_at('account', 'role')

resolver_name = parameter_value.keys.first.to_s
validate_parameter_value!(key, resolver_hash)

resolver_name = resolver_hash.keys.first.to_s
load_parameter_resolver(resolver_name)

value = parameter_value.values.first
value = resolver_hash.values.first
resolver_class_name = resolver_name.camelize
call_resolver(resolver_class_name, value)
rescue Aws::CloudFormation::Errors::ValidationError
raise InvalidParameter, $!.message

assume_role_if_present(account, role, key) do
call_resolver(resolver_class_name, value)
end
end

def assume_role_if_present(account, role, key)
return yield if account.nil? && role.nil?
if account.nil? || role.nil?
raise InvalidParameter, "Both 'account' and 'role' are required to assume role for parameter '#{key}'"
end
role_assumer.assume_role(account, role) do
yield
end
end

def resolve_array_parameter_values(key, parameter_values)
Expand Down Expand Up @@ -94,5 +115,9 @@ def validate_parameter_value!(key, parameter_value)
raise InvalidParameter, "#{key} hash contained more than one key: #{parameter_value.inspect}"
end
end

def role_assumer
@role_assumer ||= RoleAssumer.new
end
end
end
14 changes: 11 additions & 3 deletions lib/stack_master/parameter_resolvers/ejson.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class Ejson < Resolver
def initialize(config, stack_definition)
@config = config
@stack_definition = stack_definition
@decrypted_ejson_files = {}
end

def resolve(secret_key)
Expand All @@ -27,9 +28,12 @@ def validate_ejson_file_specified
end

def decrypt_ejson_file
@decrypt_ejson_file ||= EJSONWrapper.decrypt(ejson_file_path,
use_kms: @stack_definition.ejson_file_kms,
region: ejson_file_region)
ejson_file_key = credentials_key
petervandoros marked this conversation as resolved.
Show resolved Hide resolved
@decrypted_ejson_files.fetch(ejson_file_key) do
@decrypted_ejson_files[ejson_file_key] = EJSONWrapper.decrypt(ejson_file_path,
use_kms: @stack_definition.ejson_file_kms,
region: ejson_file_region)
end
end

def ejson_file_region
Expand All @@ -43,6 +47,10 @@ def ejson_file_path
def secret_path_relative_to_base
@secret_path_relative_to_base ||= File.join('secrets', @stack_definition.ejson_file)
end

def credentials_key
Aws.config[:credentials]&.object_id
end
end
end
end
6 changes: 3 additions & 3 deletions lib/stack_master/parameter_resolvers/latest_ami.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ class LatestAmi < Resolver
def initialize(config, stack_definition)
@config = config
@stack_definition = stack_definition
@ami_finder = AmiFinder.new(@stack_definition.region)
end

def resolve(value)
owners = Array(value.fetch('owners', 'self').to_s)
filters = @ami_finder.build_filters_from_hash(value.fetch('filters'))
@ami_finder.find_latest_ami(filters, owners).try(:image_id)
ami_finder = AmiFinder.new(@stack_definition.region)
filters = ami_finder.build_filters_from_hash(value.fetch('filters'))
ami_finder.find_latest_ami(filters, owners).try(:image_id)
end
end
end
Expand Down
7 changes: 1 addition & 6 deletions lib/stack_master/parameter_resolvers/parameter_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def initialize(config, stack_definition)

def resolve(value)
begin
ssm = Aws::SSM::Client.new(region: @stack_definition.region)
resp = ssm.get_parameter(
name: value,
with_decryption: true
Expand All @@ -20,12 +21,6 @@ def resolve(value)
end
resp.parameter.value
end

private

def ssm
@ssm ||= Aws::SSM::Client.new(region: @stack_definition.region)
end
end
end
end
5 changes: 0 additions & 5 deletions lib/stack_master/parameter_resolvers/sns_topic_name.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ class SnsTopicName < Resolver
def initialize(config, stack_definition)
@config = config
@stack_definition = stack_definition
@stacks = {}
end

def resolve(value)
Expand All @@ -19,10 +18,6 @@ def resolve(value)

private

def cf
@cf ||= StackMaster.cloud_formation_driver
end

def sns_topic_finder
StackMaster::SnsTopicFinder.new(@stack_definition.region)
end
Expand Down
18 changes: 9 additions & 9 deletions lib/stack_master/parameter_resolvers/stack_output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def resolve(value)
private

def cf
@cf ||= StackMaster.cloud_formation_driver
StackMaster.cloud_formation_driver
end

def parse!(value)
Expand All @@ -49,7 +49,7 @@ def parse!(value)

def find_stack(stack_name, region)
unaliased_region = @config.unalias_region(region)
stack_key = stack_key(stack_name, unaliased_region)
stack_key = "#{unaliased_region}:#{stack_name}:#{credentials_key}"

@stacks.fetch(stack_key) do
regional_cf = cf_for_region(unaliased_region)
Expand All @@ -58,19 +58,19 @@ def find_stack(stack_name, region)
end
end

def stack_key(stack_name, region)
"#{region}:#{stack_name}"
end

def cf_for_region(region)
return cf if cf.region == region
driver_key = "#{region}:#{credentials_key}"

@cf_drivers.fetch(region) do
@cf_drivers.fetch(driver_key) do
cloud_formation_driver = cf.class.new
cloud_formation_driver.set_region(region)
@cf_drivers[region] = cloud_formation_driver
@cf_drivers[driver_key] = cloud_formation_driver
end
end

def credentials_key
Aws.config[:credentials]&.object_id
end
end
end
end
Loading