tfctl 1.0.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6d47682cf9949db840c18d07b6f06907a9d36e2a75d0a5255b4c57f3603c0dbf
4
+ data.tar.gz: d5a81e877943fff53e903a104249e1147a827c593430bc91c0b78d5cc467fc4b
5
+ SHA512:
6
+ metadata.gz: 03b7d69b7a7bbf296b0b1ab3aa8794d8b91e51559a1e2ee1d450396615aeba495b63993f84866c6fe5997d34c760da143cb66ca0cabc2a1a5a1266b701676bac
7
+ data.tar.gz: f9ebdae72fd58473c3a8cc5015fd2d2e32092488218b52efa79e2d625802ccdf4447298e45497d4ec0e03583b0edbd3a31b51e92f76a5981a5f3819f9cee1e4a
@@ -0,0 +1,13 @@
1
+ .DS_Store
2
+ *.swp
3
+ .tfctl
4
+ pkg/
5
+ *.gem
6
+ vendor/
7
+ .bundle
8
+ bin/bundle
9
+ bin/htmldiff
10
+ bin/ldiff
11
+ bin/rspec
12
+ spec/reports
13
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1,79 @@
1
+ ---
2
+ AllCops:
3
+ TargetRubyVersion: 2.3
4
+ DisplayCopNames: true
5
+
6
+ Layout/IndentationWidth:
7
+ Width: 4
8
+
9
+ Layout/IndentHeredoc:
10
+ Enabled: false
11
+
12
+ Layout/EmptyLines:
13
+ Enabled: false
14
+
15
+ Layout/EmptyLinesAroundMethodBody:
16
+ Enabled: false
17
+
18
+ Layout/AlignHash:
19
+ EnforcedHashRocketStyle:
20
+ - table
21
+ EnforcedColonStyle:
22
+ - table
23
+
24
+ Layout/SpaceAroundOperators:
25
+ Enabled: false
26
+
27
+ Layout/ExtraSpacing:
28
+ Enabled: false
29
+
30
+ Layout/EmptyLinesAroundBlockBody:
31
+ Enabled: false
32
+
33
+ Layout/EmptyLinesAroundClassBody:
34
+ Enabled: false
35
+
36
+ Metrics/CyclomaticComplexity:
37
+ Enabled: false
38
+
39
+ Metrics/PerceivedComplexity:
40
+ Enabled: false
41
+
42
+ Metrics/BlockLength:
43
+ Enabled: false
44
+
45
+ Metrics/MethodLength:
46
+ Enabled: false
47
+
48
+ Metrics/LineLength:
49
+ Max: 140
50
+
51
+ Metrics/AbcSize:
52
+ Enabled: false
53
+
54
+ Metrics/ParameterLists:
55
+ Enabled: false
56
+
57
+ Metrics/ClassLength:
58
+ Enabled: false
59
+
60
+ Style/IfUnlessModifier:
61
+ Enabled: false
62
+
63
+ Style/AndOr:
64
+ Enabled: false
65
+
66
+ Style/Documentation:
67
+ Enabled: false
68
+
69
+ Style/TrailingCommaInArguments:
70
+ EnforcedStyleForMultiline: comma
71
+
72
+ Style/TrailingCommaInArrayLiteral:
73
+ EnforcedStyleForMultiline: comma
74
+
75
+ Style/TrailingCommaInHashLiteral:
76
+ EnforcedStyleForMultiline: comma
77
+
78
+ Style/RedundantReturn:
79
+ Enabled: false
@@ -0,0 +1,18 @@
1
+ rvm:
2
+ - 2.3
3
+ - 2.6
4
+ os: linux
5
+ language: ruby
6
+ script: make test
7
+ jobs:
8
+ include:
9
+ - stage: Gem release
10
+ rvm: 2.6
11
+ deploy:
12
+ provider: rubygems
13
+ api_key:
14
+ secure: FKAONS7x6koN7oiEULr4ViwjlDBzbE0bCgqhXRP4DfTtlRyEymeqgrfSIqkVH7unjd0muIGrMBnFuaQVSx7648RS1Ss0QAJo32SVnzoYl1P03cijqNmbbaf1jRdA3IGmh0gV5vsXrmlHiP8gfAuC9PqQ550OzxzWUvEI8vXgSTibmKd/PoQinv5g/dq0gBFjlhSMt/k3Z9WlMmkEsAro/r/Ie2M7mItHPT65f0ga5q5SeujPQQ3Sd/l3mznh37bmnw5RZpFDYdA7jL2p0Y58XJPBU8soa3ZC5GeHyxCYVoGh6EDGAFb83ERRT6rQ7ywkOufTv1o497P7a/prSbvT6fzc+DcugXPEaglT+dUXMe36OoF907Xva4vq3xIHV2N/yrxbDM85hmMk22wEU+9wpDDzFNQnfsXNbaHG9F7gLgy0eoTRrSuJf6cPDlE8pwvn7b8cjieeqWc//ZNhSYnHYZGER4LFINWVxs68Eofmmqp2IESTcUpJ8oB4bV+bzzyobJMRobOXu2hvgCrTdr6r/PnckpAfZE/l4nVQa14f1FU//8bU3DwvNun6TX1Ujp+XNiRDUlvP2KnkBU4s5rsIkL3lCHW7r6GipSk6SOvGMTz5eySMsoWvZQBdAzk/OxcIteeWH9pdo1Hbu5x2/bwyuTRCQ9E79CKWDKlIQwCgUY0=
15
+ gem: tfctl
16
+ on:
17
+ tags: true
18
+ repo: scalefactory/tfctl
@@ -0,0 +1,29 @@
1
+ = Changelog
2
+
3
+ == 1.0.0
4
+
5
+ * feat(config): JSON schema config validation
6
+ * feat(config): added 'data' parameter
7
+
8
+ BREAKING CHANGE: This release moves user defined data under a separate `data`
9
+ parameter so it can be easily distinguished from parameters required by tfctl.
10
+ Configuration file will need to be updated to reflect this to pass validation.
11
+
12
+
13
+ == 0.2.0
14
+
15
+ * feat: configurable Terraform and AWS provider version requirements
16
+ * fix: use provider region from config file
17
+ * fix: fail when terraform command is missing
18
+
19
+ == 0.1.0
20
+
21
+ * feat: Added `-l` switch to list discovered accounts.
22
+
23
+ == 0.0.2
24
+
25
+ * fix: Fixed an exception when `exclude_accounts` is not set.
26
+
27
+ == 0.0.1
28
+
29
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :rspec, cmd: 'bundle exec rspec' do
4
+ watch(%r{^spec/.+_spec\.rb$})
5
+ watch(%r{^lib/tfctl/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
6
+ watch('spec/spec_helper.rb') { 'spec' }
7
+ end
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.
@@ -0,0 +1,36 @@
1
+ .PHONY: clean install test rubocop spec guard
2
+
3
+ vendor:
4
+ $(info => Installing Ruby dependencies)
5
+ @bundle install --path vendor --with developement --binstubs=vendor/bin
6
+
7
+ test: vendor rubocop spec
8
+
9
+ guard: vendor
10
+ $(info => Starting guard)
11
+ @bundle exec guard
12
+
13
+ rubocop:
14
+ $(info => Running rubocop)
15
+ @vendor/bin/rubocop
16
+
17
+ spec:
18
+ $(info => Running spec tests)
19
+ @vendor/bin/rspec
20
+
21
+ pkg:
22
+ $(info => Building gem package in pkg/)
23
+ @mkdir pkg/
24
+ @gem build tfctl.gemspec
25
+ @mv *.gem pkg/
26
+
27
+ install: pkg
28
+ gem install pkg/*.gem
29
+
30
+ clean:
31
+ $(info => Cleaning)
32
+ @rm -rf pkg/
33
+ @rm -rf vendor/
34
+ @rm -rf .bundle
35
+ @rm -f Gemfile.lock
36
+ @rm -rf spec/reports/
@@ -0,0 +1,176 @@
1
+ // Settings:
2
+ :idprefix:
3
+ :idseparator: -
4
+ ifndef::env-github[:icons: font]
5
+ ifdef::env-github,env-browser[]
6
+ :toc: macro
7
+ :toclevels: 1
8
+ endif::[]
9
+ ifdef::env-github[]
10
+ :branch: master
11
+ :status:
12
+ :outfilesuffix: .adoc
13
+ :!toc-title:
14
+ :caution-caption: :fire:
15
+ :important-caption: :exclamation:
16
+ :note-caption: :paperclip:
17
+ :tip-caption: :bulb:
18
+ :warning-caption: :warning:
19
+ endif::[]
20
+
21
+ = tfctl
22
+
23
+ image:https://travis-ci.org/scalefactory/tfctl.svg?branch=master["Build Status", link="https://travis-ci.org/scalefactory/tfctl"]
24
+ image:https://badge.fury.io/rb/tfctl.svg["Gem Version", link="https://badge.fury.io/rb/tfctl"]
25
+ image:https://img.shields.io/badge/terraform-0.12-blue.svg["Terraform 0.12", link="https://img.shields.io/badge/terraform-0.12-blue"]
26
+
27
+ toc::[]
28
+
29
+ == Overview
30
+
31
+ Tfctl is a small Terraform wrapper for working with multi-account AWS
32
+ infrastructures where new accounts may be created dynamically and on-demand.
33
+
34
+ It discovers accounts by reading the AWS Organizations API and can assign
35
+ Terraform resources to multiple accounts based on the organization hierarchy.
36
+ Resources can be assigned globally, based on organization unit or to individual
37
+ accounts. It supports nested OU hierarchies and helps keep your Terraform DRY.
38
+
39
+ Tfctl was originally developed to integrate Terraform with
40
+ https://aws.amazon.com/solutions/aws-landing-zone/[AWS Landing Zone] and
41
+ https://aws.amazon.com/controltower/[Control Tower] but should work with most
42
+ other ways of managing accounts in AWS Organizations.
43
+
44
+ == Features
45
+
46
+ * Discovers AWS accounts automatically.
47
+ * Automatically generates Terraform account configuration.
48
+ * Parallel execution across multiple accounts.
49
+ * Hierarchical configuration based on AWS Organization units structure.
50
+ * Supports per account configuration overrides for handling exceptions.
51
+ * Supports nested organization units.
52
+ * Terraform state tracking in S3 and locking in DynamoDB.
53
+ * Account targeting by OU path regular expressions.
54
+ * Automatic role assumption in target accounts.
55
+ * Works with CI/CD pipelines.
56
+
57
+ == Requirements
58
+
59
+ * Terraform >= 0.12
60
+ * Ruby >= 2.3
61
+ * Accounts managed in AWS Organizations (by Landing Zone, Control Tower, some
62
+ other means)
63
+
64
+ == Installation
65
+
66
+ To install the latest release from RubyGems run:
67
+
68
+ ----
69
+ gem install tfctl
70
+ ----
71
+
72
+ Alternatively you can build and install from this repo with:
73
+
74
+ ----
75
+ make install
76
+ ----
77
+
78
+ == Docs
79
+
80
+ * https://github.com/scalefactory/tfctl/tree/master/docs/control_tower.adoc[Control Tower quick start guide]
81
+ * https://github.com/scalefactory/tfctl/tree/master/docs/project_layout.adoc[Project layout]
82
+ * https://github.com/scalefactory/tfctl/tree/master/docs/configuration.adoc[Configuration]
83
+ * https://github.com/scalefactory/tfctl/tree/master/docs/iam_permissions.adoc[IAM permissions]
84
+ * https://github.com/scalefactory/tfctl/tree/master/docs/creating_a_profile.adoc[Creating a profile]
85
+
86
+ == Running tfctl
87
+
88
+ tfctl should be run from the root of the project directory. It will generate
89
+ Terraform configuration in `.tfctl/`.
90
+
91
+ Anatomy of a tfctl command:
92
+
93
+ ----
94
+ tfctl -c CONFIG_FILE TARGET_OPTIONS -- TERRAFORM_COMMAND
95
+ ----
96
+
97
+ * `-c` specifies which tfctl config file to use (usually in `conf/`)
98
+ * `TARGET_OPTIONS` specifies which accounts to target. This could be an individual
99
+ account, a group of accounts in an organizational unit or all accounts.
100
+ * `TERRAFORM_COMMAND` will be passed to `terraform` along with any
101
+ options. See https://www.terraform.io/docs/commands/index.html[Terraform
102
+ commands] for details.
103
+
104
+ NOTE: You must have your AWS credentials configured before running tfctl or run
105
+ it using an AWS credentials helper such as
106
+ https://github.com/99designs/aws-vault[aws-vault].
107
+
108
+ === Example commands
109
+
110
+ Show help:
111
+
112
+ ----
113
+ tfctl -h
114
+ ----
115
+
116
+ Show merged configuration:
117
+
118
+ ----
119
+ tfctl -c conf/example.yaml -s
120
+ ----
121
+
122
+ List all discovered accounts:
123
+
124
+ ----
125
+ tfctl -c conf/example.yaml --all -l
126
+ ----
127
+
128
+ TIP: This can be narrowed down using targeting options and is a good way to
129
+ test what accounts match.
130
+
131
+ Run Terraform init across all accounts:
132
+
133
+ ----
134
+ tfctl -c conf/example.yaml --all -- init
135
+ ----
136
+
137
+ Run plan in `test` OU accounts:
138
+
139
+ ----
140
+ tfctl -c conf/example.yaml -o test -- plan
141
+ ----
142
+
143
+ Run plan in `live` accounts assuming that `live` is a child OU in multiple
144
+ organization units:
145
+
146
+ ----
147
+ tfctl -c conf/example.yaml -o '.*/live' -- plan
148
+ ----
149
+
150
+ Run plan in an individual account:
151
+
152
+ ----
153
+ tfctl -c conf/example.yaml -a example-account - plan
154
+ ----
155
+
156
+ Run apply in all accounts:
157
+
158
+ ----
159
+ tfctl -c conf/example.yaml --all -- apply
160
+ ----
161
+
162
+ Run destroy in `test` OU accounts:
163
+
164
+ ----
165
+ tfctl -c conf/example.yaml -o test -- destroy -auto-approve
166
+ ----
167
+
168
+ Don't buffer the output:
169
+
170
+ ----
171
+ tfctl -c conf/example.yaml -a example-account -u -- plan
172
+ ----
173
+
174
+ This will show output in real time. Usually output is buffered and displayed
175
+ after Terraform command finishes to make it more readable when running across
176
+ multiple accounts in parallel.
@@ -0,0 +1,223 @@
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 'English'
11
+ require 'terminal-table'
12
+ require_relative '../lib/tfctl'
13
+
14
+ PROJECT_ROOT = Dir.pwd
15
+
16
+ #
17
+ # Process CLI arguments
18
+ #
19
+
20
+ options = {
21
+ account: nil,
22
+ ou: nil,
23
+ all: nil,
24
+ show_config: false,
25
+ config_file: nil,
26
+ unbuffered: false,
27
+ debug: false,
28
+ use_cache: false,
29
+ }
30
+
31
+ optparse = OptionParser.new do |opts|
32
+ opts.on('-a', '--account=name', 'Target a specific AWS account') do |o|
33
+ options[:account] = o
34
+ end
35
+ opts.on('-o', '--ou=organization_unit', 'Target accounts in an Organization Unit (uses regex matching)') do |o|
36
+ options[:ou] = o
37
+ end
38
+ opts.on('--all', 'Target all accounts') do
39
+ options[:all] = true
40
+ end
41
+ opts.on('-c', '--config-file=config', 'Path to config file') do |o|
42
+ options[:config_file] = o
43
+ end
44
+ opts.on('-s', '--show-config', 'Display configuration') do
45
+ options[:show_config] = true
46
+ end
47
+ opts.on('-l', '--list-accounts', 'List discovered accounts') do
48
+ options[:list_accounts] = true
49
+ end
50
+ opts.on('-x', '--use-cache', 'Use cached AWS organization data') do
51
+ options[:use_cache] = true
52
+ end
53
+ opts.on('-u', '--unbuffered', 'Disable buffering of Terraform output') do
54
+ options[:unbuffered] = true
55
+ end
56
+ opts.on('-d', '--debug', 'Turn on debug messages') do
57
+ options[:debug] = true
58
+ end
59
+ opts.on('-v', '--version', 'Show version') do
60
+ puts Tfctl::VERSION
61
+ exit
62
+ end
63
+ end
64
+
65
+
66
+ begin
67
+ optparse.parse!
68
+
69
+ # Validate CLI arguments
70
+
71
+ if options[:config_file].nil?
72
+ raise OptionParser::MissingArgument, '--config-file'
73
+ end
74
+
75
+ unless File.exist? options[:config_file]
76
+ raise OptionParser::InvalidOption,
77
+ "Config file not found in: #{options[:config_file]}"
78
+ end
79
+
80
+ unless File.exist? options[:config_file]
81
+ raise OptionParser::InvalidOption, "Config file #{options[:config_file]} not found."
82
+ end
83
+
84
+ # Validate targets
85
+ targetting_opts = %i[account ou all]
86
+ targets_set = []
87
+ options.each do |k, v|
88
+ if targetting_opts.include?(k)
89
+ targets_set << k.to_s unless v.nil?
90
+ end
91
+ end
92
+ if targets_set.length > 1
93
+ raise OptionParser::InvalidOption,
94
+ "Too many target options set: #{targets_set.join(', ')}. Only one can be specified."
95
+ end
96
+ if targets_set.empty? and options[:show_config] == false
97
+ raise OptionParser::InvalidOption, 'Please specify target'
98
+ end
99
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument
100
+ warn $ERROR_INFO.to_s
101
+ warn optparse
102
+ exit 2
103
+ end
104
+
105
+
106
+
107
+ # Generates configuration and runs Terraform commands for a target account.
108
+ def run_account(config, account, options, tf_argv, log)
109
+
110
+ # Skip excluded accounts
111
+ if account[:excluded] == true
112
+ log.info "#{account[:name]}: excluded, skipping"
113
+ return
114
+ end
115
+
116
+ # Generate Terraform run directory with configured providers, backend and
117
+ # profiles for the target account. This is where Terraform will be
118
+ # executed from.
119
+ log.info "#{account[:name]}: Generating Terraform run directory"
120
+ Tfctl::Generator.make(
121
+ account: account,
122
+ config: config,
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
+ # Set up logging
142
+ log_level = options[:debug] ? Logger::DEBUG : Logger::INFO
143
+ log = Tfctl::Logger.new(log_level)
144
+
145
+ log.info 'tfctl running'
146
+
147
+ config_name = File.basename(options[:config_file]).chomp('.yaml')
148
+ log.info "Using config: #{config_name}"
149
+
150
+ log.info 'Working out AWS account topology'
151
+
152
+ yaml_config = YAML.safe_load(File.read(options[:config_file]))
153
+ Tfctl::Schema.validate(yaml_config)
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
+ # Find target accounts
174
+
175
+ if options[:account]
176
+ accounts = config.find_accounts(:name, options[:account])
177
+ elsif options[:ou]
178
+ accounts = config.find_accounts_regex(:ou_path, options[:ou])
179
+ elsif options[:all]
180
+ accounts = config[:accounts]
181
+ else
182
+ raise Tfctl::Error, 'Missing target'
183
+ end
184
+
185
+ # List target accounts
186
+
187
+ if options[:list_accounts]
188
+ log.info "Listing accounts\n"
189
+ table = Terminal::Table.new do |t|
190
+ t.style = {
191
+ border_x: '',
192
+ border_y: '',
193
+ border_i: '',
194
+ padding_left: 0,
195
+ }
196
+ t << %w[ACCOUNT_ID OU NAME]
197
+ accounts.each do |account|
198
+ t << [account[:id], account[:ou_path], account[:name]]
199
+ end
200
+ end
201
+
202
+ puts table
203
+ exit 0
204
+ end
205
+
206
+ # Execute Terraform in target accounts
207
+
208
+ Parallel.each(accounts, in_processes: 8) do |ac|
209
+ run_account(config, ac, options, ARGV, log)
210
+ end
211
+
212
+ log.info 'Done'
213
+ rescue Tfctl::Error => e
214
+ log.error(e)
215
+ exit 1
216
+ rescue Tfctl::ValidationError => e
217
+ log.error(e)
218
+ e.issues.each do |issue|
219
+ log.error("Parameter: #{issue[:data_pointer]}") unless issue[:data_pointer] == ''
220
+ log.error(issue[:details]) unless issue[:details].nil?
221
+ end
222
+ exit 2
223
+ end