tfctl 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +1 -0
- data/.travis.yml +17 -0
- data/CHANGELOG.adoc +9 -0
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/Makefile +26 -0
- data/README.adoc +145 -0
- data/bin/tfctl +198 -0
- data/docs/configuration.adoc +53 -0
- data/docs/control_tower.adoc +191 -0
- data/docs/creating_a_profile.adoc +169 -0
- data/docs/iam_permissions.adoc +18 -0
- data/docs/project_layout.adoc +43 -0
- data/examples/bootstrap/terraform-exec-role.template +24 -0
- data/examples/bootstrap/terraform-state.template +81 -0
- data/examples/bootstrap/tfctl-org-access.template +42 -0
- data/examples/control_tower/conf/example.yaml +71 -0
- data/examples/control_tower/modules/s3-bucket/main.tf +4 -0
- data/examples/control_tower/modules/s3-bucket/variables.tf +4 -0
- data/examples/control_tower/profiles/example-profile/data.tf +1 -0
- data/examples/control_tower/profiles/example-profile/main.tf +4 -0
- data/examples/control_tower/profiles/example-profile/variables.tf +12 -0
- data/lib/hash.rb +27 -0
- data/lib/tfctl/aws_org.rb +121 -0
- data/lib/tfctl/config.rb +177 -0
- data/lib/tfctl/error.rb +6 -0
- data/lib/tfctl/executor.rb +104 -0
- data/lib/tfctl/generator.rb +96 -0
- data/lib/tfctl/logger.rb +38 -0
- data/lib/tfctl/version.rb +5 -0
- data/lib/tfctl.rb +9 -0
- data/tfctl.gemspec +29 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0e1075714676ca848752725f2d98c38240e5a9ee2bc901b582ffba6edd46fc0f
|
4
|
+
data.tar.gz: b48bc64d5a9aaae352538ed88b648b20e2b2c68c59bd0c1b3b5c8757e3c421b9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b87ecf1c66f1528bf98134d631aef3c8264326bbd0e399eb769860ad853d24ebc59339ba6df16be41b6d728c4fd63df035a210a1ca9d09fd17960de2b4d66fa1
|
7
|
+
data.tar.gz: 657f44ce06c1ac667525a80009f73e88fbf9754f32f5703faad8fbfe1b6355fd6796690c8b0583af549fef8d75de318d1dc98f56aa80644b78ff2a068e30bfa7
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
rvm:
|
2
|
+
- 2.3
|
3
|
+
- 2.6
|
4
|
+
sudo: false
|
5
|
+
script: make test
|
6
|
+
deploy:
|
7
|
+
provider: rubygems
|
8
|
+
api_key:
|
9
|
+
secure: FKAONS7x6koN7oiEULr4ViwjlDBzbE0bCgqhXRP4DfTtlRyEymeqgrfSIqkVH7unjd0muIGrMBnFuaQVSx7648RS1Ss0QAJo32SVnzoYl1P03cijqNmbbaf1jRdA3IGmh0gV5vsXrmlHiP8gfAuC9PqQ550OzxzWUvEI8vXgSTibmKd/PoQinv5g/dq0gBFjlhSMt/k3Z9WlMmkEsAro/r/Ie2M7mItHPT65f0ga5q5SeujPQQ3Sd/l3mznh37bmnw5RZpFDYdA7jL2p0Y58XJPBU8soa3ZC5GeHyxCYVoGh6EDGAFb83ERRT6rQ7ywkOufTv1o497P7a/prSbvT6fzc+DcugXPEaglT+dUXMe36OoF907Xva4vq3xIHV2N/yrxbDM85hmMk22wEU+9wpDDzFNQnfsXNbaHG9F7gLgy0eoTRrSuJf6cPDlE8pwvn7b8cjieeqWc//ZNhSYnHYZGER4LFINWVxs68Eofmmqp2IESTcUpJ8oB4bV+bzzyobJMRobOXu2hvgCrTdr6r/PnckpAfZE/l4nVQa14f1FU//8bU3DwvNun6TX1Ujp+XNiRDUlvP2KnkBU4s5rsIkL3lCHW7r6GipSk6SOvGMTz5eySMsoWvZQBdAzk/OxcIteeWH9pdo1Hbu5x2/bwyuTRCQ9E79CKWDKlIQwCgUY0=
|
10
|
+
gem: tfctl
|
11
|
+
on:
|
12
|
+
tags: true
|
13
|
+
repo: scalefactory/tfctl
|
14
|
+
notifications:
|
15
|
+
slack:
|
16
|
+
rooms:
|
17
|
+
secure: HrdsUaTCvwtq5rLyAf7Uwo76p9JhxnoFvoZU/AtxTCL6lwMm5pqpfDAdbBH8C2BYMWRyiS8IfSrvuLbGUr317qJgVCmnhD7JIgdA0Aih4GkR0yK+EVao7qdOClO2CGJFJGqHveBBpXy94FEwgFwQSvnuSZdY8gNdXafvpgrPvcbWHIF9Bq7StfYelWZX6+FL7xczqVhdetXcHJEj6Q/thkbvyn0hJAVegjDq4Pw3MyR66Mv5HTj+jYbGrJOR1poNW8vv4iQjK0TIMvCCeTv4Ddrjitn1kOK1BPYqoHEkaRwXdS7+hevv+UfV074awTp3UOIS5VP3ve/KX21xcl9DWMG7Gde9tuyap9WKhN3XE5686qrfn4M/L7PmVnzeYimUKLQfdqjDXa7UI3orGIOq8nKCsCUFvFUQZ/DxeEssJzBNNRbeGywwVtXk7/8L2HHiQJ3t/29vMLEq6EwskYK3Uao2aIVWpLSkPwOuwVxrNWXrwojrrKo+oo8QvW6xkov5UW3VkapyzRyDN2oIpNkFeLYfuyTO4MBsnLzCBqJYEeku2l0Hp9WBq2Q1a+FRgqQlcRbLPsYwK5AdkRKcWuXP8FMZoqXX6F9B73q5Dbub2et4YN4zhvroLZN6MmlneM6faJh9aS4yV2ZlU1XbOtbqP0nkHrJ+o8DeM/wgTB1GB90=
|
data/CHANGELOG.adoc
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2019 Essentia Analytics Ltd
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/Makefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
.PHONY: clean install test
|
2
|
+
|
3
|
+
vendor:
|
4
|
+
$(info => Installing Ruby dependencies)
|
5
|
+
@bundle install --path vendor --with developement --binstubs=vendor/bin
|
6
|
+
|
7
|
+
test: vendor
|
8
|
+
$(info => Running spec tests)
|
9
|
+
@vendor/bin/rspec
|
10
|
+
|
11
|
+
pkg:
|
12
|
+
$(info => Building gem package in pkg/)
|
13
|
+
@mkdir pkg/
|
14
|
+
@gem build tfctl.gemspec
|
15
|
+
@mv *.gem pkg/
|
16
|
+
|
17
|
+
install: pkg
|
18
|
+
gem install pkg/*.gem
|
19
|
+
|
20
|
+
clean:
|
21
|
+
$(info => Cleaning)
|
22
|
+
@rm -rf pkg/
|
23
|
+
@rm -rf vendor/
|
24
|
+
@rm -rf .bundle
|
25
|
+
@rm -f Gemfile.lock
|
26
|
+
@rm -rf spec/reports/
|
data/README.adoc
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
:toc:
|
2
|
+
|
3
|
+
= tfctl
|
4
|
+
|
5
|
+
== Overview
|
6
|
+
|
7
|
+
Tfctl is a small Terraform wrapper for working with multi-account AWS
|
8
|
+
infrastructures where new accounts may be created dynamically and on-demand.
|
9
|
+
|
10
|
+
Discovers accounts by reading the AWS Organizations API and can assign
|
11
|
+
Terraform resources to accounts based on the organization hierarchy. Resources
|
12
|
+
can be assigned globally, based on organization unit or individual accounts.
|
13
|
+
It supports nested OU hierarchies.
|
14
|
+
|
15
|
+
Tfctl was originally developed to integrate Terraform with
|
16
|
+
https://aws.amazon.com/solutions/aws-landing-zone/[AWS Landing Zone] and
|
17
|
+
https://aws.amazon.com/controltower/[Control Tower] but should work with most
|
18
|
+
other ways of managing accounts in AWS Organizations.
|
19
|
+
|
20
|
+
== Features
|
21
|
+
|
22
|
+
* Discovers AWS accounts automatically.
|
23
|
+
* Automatically generates Terraform account configuration.
|
24
|
+
* Parallel execution across multiple accounts.
|
25
|
+
* Hierarchical configuration based on AWS Organization units structure.
|
26
|
+
* Supports per account configuration overrides for handling exceptions.
|
27
|
+
* Supports nested organization units.
|
28
|
+
* Terraform state tracking in S3 and locking in DynamoDB.
|
29
|
+
* Account targeting by OU path regular expressions.
|
30
|
+
* Automatic role assumption in target accounts.
|
31
|
+
* Works with CI/CD pipelines.
|
32
|
+
|
33
|
+
== Requirements
|
34
|
+
|
35
|
+
* Terraform >= 0.12
|
36
|
+
* Ruby >= 2.3
|
37
|
+
* Accounts managed in AWS Organizations (by Landing Zone, Control Tower, some
|
38
|
+
other means)
|
39
|
+
|
40
|
+
== Installation
|
41
|
+
|
42
|
+
Clone this repository and run:
|
43
|
+
|
44
|
+
----
|
45
|
+
make install
|
46
|
+
----
|
47
|
+
|
48
|
+
This will build a ruby gem and install it.
|
49
|
+
|
50
|
+
When using bundler, add this to your `Gemfile`:
|
51
|
+
|
52
|
+
----
|
53
|
+
gem 'tfctl', git: 'https://github.com/scalefactory/tfctl'
|
54
|
+
----
|
55
|
+
|
56
|
+
== Docs
|
57
|
+
|
58
|
+
* https://github.com/scalefactory/tfctl/tree/master/docs/control_tower.adoc[Control Tower quick start guide]
|
59
|
+
* https://github.com/scalefactory/tfctl/tree/master/docs/project_layout.adoc[Project layout]
|
60
|
+
* https://github.com/scalefactory/tfctl/tree/master/docs/configuration.adoc[Configuration]
|
61
|
+
* https://github.com/scalefactory/tfctl/tree/master/docs/iam_permissions.adoc[IAM permissions]
|
62
|
+
* https://github.com/scalefactory/tfctl/tree/master/docs/creating_a_profile.adoc[Creating a profile]
|
63
|
+
|
64
|
+
== Running tfctl
|
65
|
+
|
66
|
+
tfctl should be run from the root of the project directory. It will generate
|
67
|
+
Terraform configuration in `.tfctl/`.
|
68
|
+
|
69
|
+
Anatomy of a tfctl command:
|
70
|
+
|
71
|
+
----
|
72
|
+
tfctl -c CONFIG_FILE TARGET_OPTIONS -- TERRAFORM_COMMAND
|
73
|
+
----
|
74
|
+
|
75
|
+
* `-c` specifies which tfctl config file to use (usually in `conf/`)
|
76
|
+
* `TARGET_OPTIONS` specifies which accounts to target. This could be an individual
|
77
|
+
account, a group of accounts in an organizational unit or all accounts.
|
78
|
+
* `TERRAFORM_COMMAND` will be passed to `terraform` along with any
|
79
|
+
options. See https://www.terraform.io/docs/commands/index.html[Terraform
|
80
|
+
commands] for details.
|
81
|
+
|
82
|
+
NOTE: You must have your AWS credentials configured before running tfctl or run
|
83
|
+
it using an AWS credentials helper such as
|
84
|
+
https://github.com/99designs/aws-vault[aws-vault].
|
85
|
+
|
86
|
+
=== Example commands
|
87
|
+
|
88
|
+
Show help:
|
89
|
+
|
90
|
+
----
|
91
|
+
tfctl -h
|
92
|
+
----
|
93
|
+
|
94
|
+
Show merged configuration:
|
95
|
+
|
96
|
+
----
|
97
|
+
tfctl -c conf/example.yaml -s
|
98
|
+
----
|
99
|
+
|
100
|
+
Run Terraform init accross all accounts:
|
101
|
+
|
102
|
+
----
|
103
|
+
tfctl -c conf/example.yaml --all -- init
|
104
|
+
----
|
105
|
+
|
106
|
+
Run plan in `test` OU accounts:
|
107
|
+
|
108
|
+
----
|
109
|
+
tfctl -c conf/example.yaml -o test -- plan
|
110
|
+
----
|
111
|
+
|
112
|
+
Run plan in `live` accounts assuming that `live` is a child OU in multiple
|
113
|
+
organization units:
|
114
|
+
|
115
|
+
----
|
116
|
+
tfctl -c conf/example.yaml -o '.*/live' -- plan
|
117
|
+
----
|
118
|
+
|
119
|
+
Run plan in an individual account:
|
120
|
+
|
121
|
+
----
|
122
|
+
tfctl -c conf/example.yaml -a example-account - plan
|
123
|
+
----
|
124
|
+
|
125
|
+
Run apply in all accounts:
|
126
|
+
|
127
|
+
----
|
128
|
+
tfctl -c conf/example.yaml --all -- apply
|
129
|
+
----
|
130
|
+
|
131
|
+
Run destroy in `test` OU accounts:
|
132
|
+
|
133
|
+
----
|
134
|
+
tfctl -c conf/example.yaml -o test -- destroy -auto-approve
|
135
|
+
----
|
136
|
+
|
137
|
+
Don't buffer the output:
|
138
|
+
|
139
|
+
----
|
140
|
+
tfctl -c conf/example.yaml -a example-account -u -- plan
|
141
|
+
----
|
142
|
+
|
143
|
+
This will show output in real time. Usually output is buffered and displayed
|
144
|
+
after Terraform command finishes to make it more readable when running across
|
145
|
+
multiple accounts in parallel.
|
data/bin/tfctl
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
if File.directory?(File.dirname(__FILE__) + '/../vendor')
|
5
|
+
require 'bundler/setup'
|
6
|
+
end
|
7
|
+
require 'optparse'
|
8
|
+
require 'fileutils'
|
9
|
+
require 'parallel'
|
10
|
+
require_relative '../lib/tfctl'
|
11
|
+
|
12
|
+
PROJECT_ROOT = Dir.pwd
|
13
|
+
|
14
|
+
#
|
15
|
+
# Process CLI arguments
|
16
|
+
#
|
17
|
+
|
18
|
+
options={
|
19
|
+
:account => nil,
|
20
|
+
:ou => nil,
|
21
|
+
:all => nil,
|
22
|
+
:show_config => false,
|
23
|
+
:config_file => nil,
|
24
|
+
:unbuffered => false,
|
25
|
+
:debug => false,
|
26
|
+
:use_cache => false
|
27
|
+
}
|
28
|
+
|
29
|
+
optparse = OptionParser.new do |opts|
|
30
|
+
opts.on('-a', '--account=name', "Target a specific AWS account") do |o|
|
31
|
+
options[:account] = o
|
32
|
+
end
|
33
|
+
opts.on('-o', '--ou=organization_unit', "Target accounts in an Organization Unit (uses regex matching)") do |o|
|
34
|
+
options[:ou] = o
|
35
|
+
end
|
36
|
+
opts.on('--all', "Target all accounts") do
|
37
|
+
options[:all] = true
|
38
|
+
end
|
39
|
+
opts.on('-c', '--config-file=config', "Path to config file") do |o|
|
40
|
+
options[:config_file] = o
|
41
|
+
end
|
42
|
+
opts.on('-s', '--show-config', "Display configuration") do
|
43
|
+
options[:show_config] = true
|
44
|
+
end
|
45
|
+
opts.on('-x', '--use-cache', "Use cached AWS organization data") do
|
46
|
+
options[:use_cache] = true
|
47
|
+
end
|
48
|
+
opts.on('-u', '--unbuffered', "Disable buffering of Terraform output") do
|
49
|
+
options[:unbuffered] = true
|
50
|
+
end
|
51
|
+
opts.on('-d', '--debug', "Turn on debug messages") do
|
52
|
+
options[:debug] = true
|
53
|
+
end
|
54
|
+
opts.on('-v', '--version', "Show version") do
|
55
|
+
puts Tfctl::VERSION
|
56
|
+
exit
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
begin
|
62
|
+
optparse.parse!
|
63
|
+
|
64
|
+
# Validate CLI arguments
|
65
|
+
|
66
|
+
if options[:config_file].nil?
|
67
|
+
raise OptionParser::MissingArgument, '--config-file'
|
68
|
+
end
|
69
|
+
|
70
|
+
unless File.exists? options[:config_file]
|
71
|
+
raise OptionParser::InvalidOption,
|
72
|
+
"Config file not found in: #{options[:config_file]}"
|
73
|
+
end
|
74
|
+
|
75
|
+
unless File.exists? options[:config_file]
|
76
|
+
raise OptionParser::InvalidOption, "Config file #{options[:config_file]} not found."
|
77
|
+
end
|
78
|
+
|
79
|
+
# Validate targets
|
80
|
+
targetting_opts = [:account, :ou, :all]
|
81
|
+
targets_set = []
|
82
|
+
options.each do |k,v|
|
83
|
+
if targetting_opts.include?(k)
|
84
|
+
targets_set << k.to_s unless v == nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
if targets_set.length > 1
|
88
|
+
raise OptionParser::InvalidOption,
|
89
|
+
"Too many target options set: #{targets_set.join(', ')}. Only one can be specified."
|
90
|
+
end
|
91
|
+
if targets_set.empty? and options[:show_config] == false
|
92
|
+
raise OptionParser::InvalidOption, "Please specify target"
|
93
|
+
end
|
94
|
+
|
95
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
96
|
+
$stderr.puts $!.to_s
|
97
|
+
$stderr.puts optparse
|
98
|
+
exit 2
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
# Generates configuration and runs Terraform commands for a target account.
|
104
|
+
def run_account(config, account, options, tf_argv, log)
|
105
|
+
|
106
|
+
# Skip excluded accounts
|
107
|
+
if account[:excluded] == true
|
108
|
+
log.info "#{account[:name]}: excluded, skipping"
|
109
|
+
return
|
110
|
+
end
|
111
|
+
|
112
|
+
# Generate Terraform run directory with configured providers, backend and
|
113
|
+
# profiles for the target account. This is where Terraform will be
|
114
|
+
# executed from.
|
115
|
+
log.info "#{account[:name]}: Generating Terraform run directory"
|
116
|
+
Tfctl::Generator.make(
|
117
|
+
config: config,
|
118
|
+
terraform_io_org: config[:terraform_io_org],
|
119
|
+
account_id: account[:id],
|
120
|
+
account_name: account[:name],
|
121
|
+
profiles: account[:profiles],
|
122
|
+
execution_role: account[:tf_execution_role]
|
123
|
+
)
|
124
|
+
|
125
|
+
log.info "#{account[:name]}: Executing Terraform #{tf_argv[0]}"
|
126
|
+
Tfctl::Executor.run(
|
127
|
+
account_name: account[:name],
|
128
|
+
config_name: config[:config_name],
|
129
|
+
unbuffered: options[:unbuffered],
|
130
|
+
log: log,
|
131
|
+
argv: tf_argv
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
#
|
137
|
+
# Main
|
138
|
+
#
|
139
|
+
|
140
|
+
begin
|
141
|
+
|
142
|
+
# Set up logging
|
143
|
+
options[:debug] ? log_level = Logger::DEBUG : log_level = Logger::INFO
|
144
|
+
log = Tfctl::Logger.new(log_level)
|
145
|
+
|
146
|
+
log.info 'tfctl running'
|
147
|
+
|
148
|
+
config_name = File.basename(options[:config_file]).chomp('.yaml')
|
149
|
+
log.info "Using config: #{config_name}"
|
150
|
+
|
151
|
+
log.info 'Working out AWS account topology'
|
152
|
+
|
153
|
+
yaml_config = YAML.load(File.read(options[:config_file]))
|
154
|
+
yaml_config.symbolize_names!
|
155
|
+
|
156
|
+
org_units = yaml_config[:organization_units].keys
|
157
|
+
aws_org_accounts = Tfctl::AwsOrg.new(yaml_config[:tfctl_role_arn]).accounts(org_units)
|
158
|
+
|
159
|
+
log.info 'Merging configuration'
|
160
|
+
|
161
|
+
config = Tfctl::Config.new(
|
162
|
+
config_name: config_name,
|
163
|
+
yaml_config: yaml_config,
|
164
|
+
aws_org_config: aws_org_accounts,
|
165
|
+
use_cache: options[:use_cache])
|
166
|
+
|
167
|
+
|
168
|
+
if options[:show_config]
|
169
|
+
puts config.to_yaml
|
170
|
+
exit 0
|
171
|
+
end
|
172
|
+
|
173
|
+
# Run targets
|
174
|
+
|
175
|
+
if options[:account]
|
176
|
+
account = config.find_accounts(:name, options[:account]).first
|
177
|
+
run_account(config, account, options, ARGV, log)
|
178
|
+
|
179
|
+
elsif options[:ou]
|
180
|
+
accounts = config.find_accounts_regex(:ou_path, options[:ou])
|
181
|
+
Parallel.each(accounts, in_processes: 8) do |ac|
|
182
|
+
run_account(config, ac, options, ARGV, log)
|
183
|
+
end
|
184
|
+
|
185
|
+
elsif options[:all]
|
186
|
+
accounts = config[:accounts]
|
187
|
+
Parallel.each(accounts, in_processes: 8) do |ac|
|
188
|
+
run_account(config, ac, options, ARGV, log)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
log.info 'Done'
|
194
|
+
|
195
|
+
rescue Tfctl::Error => e
|
196
|
+
log.error(e)
|
197
|
+
exit 1
|
198
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
== Configuration
|
2
|
+
|
3
|
+
Tfctl retrieves initial account configuration from AWS Organizations and merges
|
4
|
+
it with organization config specified in the yaml file.
|
5
|
+
|
6
|
+
The configuration is merged in the following order:
|
7
|
+
|
8
|
+
* AWS Organizations data is fetched and stored in an `accounts` array.
|
9
|
+
* `organization_root` settings are merged with all accounts.
|
10
|
+
* `organization_units` settings are merged with accounts matching the OU.
|
11
|
+
* `account_overrides` are merged with individual accounts matching the account name.
|
12
|
+
|
13
|
+
Parameters further down the hierarchy take precedence. For example:
|
14
|
+
|
15
|
+
[source, yaml]
|
16
|
+
----
|
17
|
+
organization_root:
|
18
|
+
example_param: 'will be overriden further down'
|
19
|
+
|
20
|
+
organization_units:
|
21
|
+
team:
|
22
|
+
example_param: 'will win in team ou'
|
23
|
+
team/live:
|
24
|
+
example_param: 'will win in team/live ou'
|
25
|
+
----
|
26
|
+
|
27
|
+
One exception to this rule is the `profiles` parameter. Profiles are additive:
|
28
|
+
|
29
|
+
[source, yaml]
|
30
|
+
----
|
31
|
+
organization_root:
|
32
|
+
profiles:
|
33
|
+
- profile-one
|
34
|
+
- profile-two
|
35
|
+
|
36
|
+
organization_units:
|
37
|
+
team:
|
38
|
+
profiles:
|
39
|
+
- profile-three
|
40
|
+
----
|
41
|
+
|
42
|
+
This will result in all three profiles deployed to accounts in `team` OU.
|
43
|
+
|
44
|
+
TIP: You can display the fully merged configuration by running `tfctl -c
|
45
|
+
conf/CONFIG_FILE.yaml -s`. It's safe to run as it doesn't make any changes to
|
46
|
+
AWS resources. It's a good way to test your configuration.
|
47
|
+
|
48
|
+
=== Handling secrets
|
49
|
+
|
50
|
+
No secrets should be committed into Terraform or tfctl configuration. Use AWS
|
51
|
+
Secrets Manager instead and retrieve in Terraform profiles using
|
52
|
+
https://www.terraform.io/docs/providers/aws/d/secretsmanager_secret.html[secrets
|
53
|
+
manager data source]
|
@@ -0,0 +1,191 @@
|
|
1
|
+
:toc:
|
2
|
+
|
3
|
+
== Control Tower integration guide
|
4
|
+
|
5
|
+
This guide will help you integrate Terraform with AWS Control Tower using the
|
6
|
+
tfctl wrapper. This involves setting up resources for remote state tracking,
|
7
|
+
necessary IAM roles and a tfctl project.
|
8
|
+
|
9
|
+
=== Overview
|
10
|
+
|
11
|
+
For state tracking we're going to create a dedicated `shared-services` account
|
12
|
+
under a `mgmt` organization unit. We'll use S3 for state storage and DynamoDB
|
13
|
+
for locking. `TerraformState` IAM role will be created for cross account
|
14
|
+
access to state resources from the primary account.
|
15
|
+
|
16
|
+
In the primary account we'll create a `TfctlOrgAccess` role. It gives tfctl
|
17
|
+
read only access to AWS Organizations which is used to discover accounts and
|
18
|
+
the organization unit structure.
|
19
|
+
|
20
|
+
We'll use CloudFormation stacks and stack-sets to bootstrap these resources.
|
21
|
+
|
22
|
+
For executing Terraform in spoke accounts we'll use the
|
23
|
+
`AWSControlTowerExecution` role which is automatically created by Control Tower
|
24
|
+
account factory and can be assumed from the primary account.
|
25
|
+
|
26
|
+
We're going to create a `live` and `test` organization units in Control Tower
|
27
|
+
and provision a couple of accounts for testing.
|
28
|
+
|
29
|
+
=== Prerequisites
|
30
|
+
|
31
|
+
Before starting you'll need:
|
32
|
+
|
33
|
+
* Control Tower set up in your primary account.
|
34
|
+
* A user with `AdministratorAccess` privileges in primary account.
|
35
|
+
* AWS CLI tools installed on your machine.
|
36
|
+
* Terraform 0.12 or higher.
|
37
|
+
|
38
|
+
=== Configure Control Tower
|
39
|
+
|
40
|
+
Create the following organization units in Control Tower:
|
41
|
+
|
42
|
+
* `mgmt`
|
43
|
+
* `live`
|
44
|
+
* `test`
|
45
|
+
|
46
|
+
Then provision accounts:
|
47
|
+
|
48
|
+
* In `mgmt` OU create an account called `mgmt-shared-services`
|
49
|
+
* In `live` create `live-example1`
|
50
|
+
* In `test` create `test-example1`
|
51
|
+
|
52
|
+
NOTE: Control Tower accounts need to be provisioned one at a time. It takes
|
53
|
+
approximately 20 mins to provision one.
|
54
|
+
|
55
|
+
=== Install tfctl
|
56
|
+
|
57
|
+
----
|
58
|
+
git clone git@github.com:scalefactory/tfctl.git
|
59
|
+
cd tfctl/ && sudo make install
|
60
|
+
----
|
61
|
+
|
62
|
+
=== Set up AWS resources
|
63
|
+
|
64
|
+
It's assumed you have configured AWS CLI access to the primary account.
|
65
|
+
|
66
|
+
We'll use CloudFormation templates in `examples/bootstrap/`.
|
67
|
+
|
68
|
+
First export configuration using environment variables making sure to change to
|
69
|
+
values to suit your set up:
|
70
|
+
|
71
|
+
----
|
72
|
+
export PRIMARY_ACCOUNT_ID=11111111
|
73
|
+
export SHARED_SERVICES_ACCOUNT_ID=22222222
|
74
|
+
export STATE_BUCKET_NAME='example-terraform-state'
|
75
|
+
----
|
76
|
+
|
77
|
+
Create the remote state resources stack set:
|
78
|
+
|
79
|
+
----
|
80
|
+
cd examples/bootstrap/
|
81
|
+
|
82
|
+
aws cloudformation create-stack-set \
|
83
|
+
--stack-set-name TerraformState \
|
84
|
+
--template-body file://terraform-state.template \
|
85
|
+
--description "Resources for managing Terraform state" \
|
86
|
+
--capabilities CAPABILITY_NAMED_IAM CAPABILITY_IAM \
|
87
|
+
--execution-role-name AWSControlTowerExecution \
|
88
|
+
--administration-role-arn arn:aws:iam::${PRIMARY_ACCOUNT_ID}:role/service-role/AWSControlTowerStackSetRole \
|
89
|
+
--parameters ParameterKey=PrimaryAccountId,ParameterValue=${PRIMARY_ACCOUNT_ID} \
|
90
|
+
ParameterKey=TerraformStateBucket,ParameterValue=${STATE_BUCKET_NAME}
|
91
|
+
----
|
92
|
+
|
93
|
+
Create a stack set instance in you shared services account:
|
94
|
+
|
95
|
+
----
|
96
|
+
aws cloudformation create-stack-instances \
|
97
|
+
--stack-set-name TerraformState \
|
98
|
+
--accounts ${SHARED_SERVICES_ACCOUNT_ID} \
|
99
|
+
--regions eu-west-1
|
100
|
+
----
|
101
|
+
|
102
|
+
Check status:
|
103
|
+
|
104
|
+
----
|
105
|
+
aws cloudformation describe-stack-instance \
|
106
|
+
--stack-set-name TerraformState \
|
107
|
+
--stack-instance-account ${SHARED_SERVICES_ACCOUNT_ID} \
|
108
|
+
--stack-instance-region eu-west-1
|
109
|
+
----
|
110
|
+
|
111
|
+
NOTE: Initial status will be `OUTDATED`, it should change to `CURRENT` once deployed.
|
112
|
+
|
113
|
+
Deploy `TfctlOrgAccess` IAM role stack:
|
114
|
+
|
115
|
+
----
|
116
|
+
aws cloudformation create-stack \
|
117
|
+
--stack-name TfctlOrgAccess \
|
118
|
+
--template-body file://tfctl-org-access.template \
|
119
|
+
--capabilities CAPABILITY_NAMED_IAM CAPABILITY_IAM \
|
120
|
+
--parameters ParameterKey=PrimaryAccountId,ParameterValue=${PRIMARY_ACCOUNT_ID}
|
121
|
+
----
|
122
|
+
|
123
|
+
Check status:
|
124
|
+
|
125
|
+
----
|
126
|
+
aws cloudformation describe-stacks --stack-name TfctlOrgAccess
|
127
|
+
----
|
128
|
+
|
129
|
+
NOTE: Successful status should read: `CREATE_COMPLETE`.
|
130
|
+
|
131
|
+
=== Configure tfctl
|
132
|
+
|
133
|
+
Copy the example project directory `examples/control_tower` somewhere convenient
|
134
|
+
and edit `conf/example.yaml`.
|
135
|
+
|
136
|
+
You need to modify the following parameters:
|
137
|
+
|
138
|
+
* `tf_state_bucket` - set to `$STATE_BUCKET_NAME`
|
139
|
+
* `tf_state_role_arn` - set shared services account ID
|
140
|
+
* `tfctl_role_arn` - set primary account ID
|
141
|
+
* `primary_account` - set the primary account name. You can find it in AWS Organizations.
|
142
|
+
|
143
|
+
TIP: You should keep your project directory under version control.
|
144
|
+
|
145
|
+
=== Deploy example tfctl profile
|
146
|
+
|
147
|
+
The example profile will create an S3 bucket in accounts under `test`, `live`
|
148
|
+
and `mgmt` OUs.
|
149
|
+
|
150
|
+
NOTE: Run tfctl commands from the root of you project directory.
|
151
|
+
|
152
|
+
First dump the configuration to verify everything works:
|
153
|
+
|
154
|
+
----
|
155
|
+
tfctl -c conf/example.yaml -s
|
156
|
+
----
|
157
|
+
|
158
|
+
This will not make any changes but will print out a yaml containing the final,
|
159
|
+
merged configuration data. It should contain a list of discovered accounts and
|
160
|
+
their configuration.
|
161
|
+
|
162
|
+
Initialise terraform for all discovered accounts:
|
163
|
+
|
164
|
+
----
|
165
|
+
tfctl -c conf/example.yaml --all -- init
|
166
|
+
----
|
167
|
+
|
168
|
+
Tfctl will run Terraform against all accounts in parallel.
|
169
|
+
|
170
|
+
Run plan:
|
171
|
+
|
172
|
+
----
|
173
|
+
tfctl -c conf/example.yaml --all -- plan
|
174
|
+
----
|
175
|
+
|
176
|
+
and apply:
|
177
|
+
|
178
|
+
----
|
179
|
+
tfctl -c conf/example.yaml --all -- apply
|
180
|
+
----
|
181
|
+
|
182
|
+
To destroy created resources run:
|
183
|
+
|
184
|
+
----
|
185
|
+
tfctl -c conf/example.yaml --all -- destroy -auto-approve
|
186
|
+
----
|
187
|
+
|
188
|
+
That's it! You can now execute terraform across your Control Tower estate.
|
189
|
+
|
190
|
+
TIP: Your project directory should be under version control excluding the
|
191
|
+
`.tfctl` directory which is automatically generated.
|