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.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +1 -0
- data/.rubocop.yml +79 -0
- data/.travis.yml +18 -0
- data/CHANGELOG.adoc +29 -0
- data/Gemfile +5 -0
- data/Guardfile +7 -0
- data/LICENSE +19 -0
- data/Makefile +36 -0
- data/README.adoc +176 -0
- data/bin/tfctl +223 -0
- data/docs/configuration.adoc +89 -0
- data/docs/control_tower.adoc +211 -0
- data/docs/creating_a_profile.adoc +191 -0
- data/docs/iam_permissions.adoc +38 -0
- data/docs/project_layout.adoc +65 -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 +80 -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 +33 -0
- data/lib/tfctl.rb +10 -0
- data/lib/tfctl/aws_org.rb +112 -0
- data/lib/tfctl/config.rb +182 -0
- data/lib/tfctl/error.rb +15 -0
- data/lib/tfctl/executor.rb +103 -0
- data/lib/tfctl/generator.rb +88 -0
- data/lib/tfctl/logger.rb +52 -0
- data/lib/tfctl/schema.rb +80 -0
- data/lib/tfctl/version.rb +5 -0
- data/tfctl.gemspec +36 -0
- metadata +179 -0
@@ -0,0 +1,89 @@
|
|
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
|
+
= Configuration
|
22
|
+
|
23
|
+
toc::[]
|
24
|
+
|
25
|
+
== Overview
|
26
|
+
|
27
|
+
Tfctl retrieves initial account configuration from AWS Organizations and merges
|
28
|
+
it with configuration specified in a yaml file.
|
29
|
+
|
30
|
+
The configuration is merged in the following order:
|
31
|
+
|
32
|
+
* AWS Organizations data is fetched and stored in an `accounts` array.
|
33
|
+
* `organization_root` settings are merged with all accounts.
|
34
|
+
* `organization_units` settings are merged with accounts matching the OU.
|
35
|
+
* `account_overrides` are merged with individual accounts matching the account name.
|
36
|
+
|
37
|
+
Parameters further down the hierarchy take precedence. For example:
|
38
|
+
|
39
|
+
[source, yaml]
|
40
|
+
----
|
41
|
+
organization_root:
|
42
|
+
data:
|
43
|
+
example_param: 'will be overriden further down'
|
44
|
+
|
45
|
+
organization_units:
|
46
|
+
team:
|
47
|
+
data:
|
48
|
+
example_param: 'will win in team ou'
|
49
|
+
team/live:
|
50
|
+
data:
|
51
|
+
example_param: 'will win in team/live ou'
|
52
|
+
----
|
53
|
+
|
54
|
+
One exception to this rule is the `profiles` parameter. Profiles are additive:
|
55
|
+
|
56
|
+
[source, yaml]
|
57
|
+
----
|
58
|
+
organization_root:
|
59
|
+
profiles:
|
60
|
+
- profile-one
|
61
|
+
- profile-two
|
62
|
+
|
63
|
+
organization_units:
|
64
|
+
team:
|
65
|
+
profiles:
|
66
|
+
- profile-three
|
67
|
+
----
|
68
|
+
|
69
|
+
This will result in all three profiles deployed to accounts in `team` OU.
|
70
|
+
|
71
|
+
TIP: You can display the fully merged configuration by running `tfctl -c
|
72
|
+
conf/CONFIG_FILE.yaml -s`. It's safe to run as it doesn't make any changes to
|
73
|
+
AWS resources. It's a good way to test your configuration.
|
74
|
+
|
75
|
+
== Defining arbitrary data
|
76
|
+
|
77
|
+
You can define arbitrary data under the `data:` parameter, both in the root of
|
78
|
+
the config and in the organization sections. It will be available in Terraform
|
79
|
+
profiles to use by your modules. You can use this to define things like VPC
|
80
|
+
subnet ranges, s3 bucket names and so on. `data:` in organization sections
|
81
|
+
will be merged with accounts following the usual merge order as described
|
82
|
+
above.
|
83
|
+
|
84
|
+
== Handling secrets
|
85
|
+
|
86
|
+
No secrets should be committed into Terraform or tfctl configuration. Use AWS
|
87
|
+
Secrets Manager instead and retrieve in Terraform profiles using
|
88
|
+
https://www.terraform.io/docs/providers/aws/d/secretsmanager_secret.html[secrets
|
89
|
+
manager data source]
|
@@ -0,0 +1,211 @@
|
|
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
|
+
= Control Tower integration guide
|
22
|
+
|
23
|
+
This guide will help you integrate Terraform with AWS Control Tower using the
|
24
|
+
tfctl wrapper. This involves setting up resources for remote state tracking,
|
25
|
+
necessary IAM roles and a tfctl project.
|
26
|
+
|
27
|
+
toc::[]
|
28
|
+
|
29
|
+
== Overview
|
30
|
+
|
31
|
+
For state tracking we're going to create a dedicated `shared-services` account
|
32
|
+
under a `mgmt` organization unit. We'll use S3 for state storage and DynamoDB
|
33
|
+
for locking. `TerraformState` IAM role will be created for cross account
|
34
|
+
access to state resources from the primary account.
|
35
|
+
|
36
|
+
In the primary account we'll create a `TfctlOrgAccess` role. It gives tfctl
|
37
|
+
read only access to AWS Organizations which is used to discover accounts and
|
38
|
+
the organization unit structure.
|
39
|
+
|
40
|
+
We'll use CloudFormation stacks and stack-sets to bootstrap these resources.
|
41
|
+
|
42
|
+
For executing Terraform in spoke accounts we'll use the
|
43
|
+
`AWSControlTowerExecution` role which is automatically created by Control Tower
|
44
|
+
account factory and can be assumed from the primary account.
|
45
|
+
|
46
|
+
We're going to create a `live` and `test` organization units in Control Tower
|
47
|
+
and provision a couple of accounts for testing.
|
48
|
+
|
49
|
+
== Prerequisites
|
50
|
+
|
51
|
+
Before starting you'll need:
|
52
|
+
|
53
|
+
* Control Tower set up in your primary account.
|
54
|
+
* A user with `AdministratorAccess` privileges in primary account.
|
55
|
+
* AWS CLI tools installed on your machine.
|
56
|
+
* Terraform 0.12 or higher.
|
57
|
+
|
58
|
+
== Configure Control Tower
|
59
|
+
|
60
|
+
Create the following organization units in Control Tower:
|
61
|
+
|
62
|
+
* `mgmt`
|
63
|
+
* `live`
|
64
|
+
* `test`
|
65
|
+
|
66
|
+
Then provision accounts:
|
67
|
+
|
68
|
+
* In `mgmt` OU create an account called `mgmt-shared-services`
|
69
|
+
* In `live` create `live-example1`
|
70
|
+
* In `test` create `test-example1`
|
71
|
+
|
72
|
+
NOTE: Control Tower accounts need to be provisioned one at a time. It takes
|
73
|
+
approximately 20 mins to provision one.
|
74
|
+
|
75
|
+
== Install tfctl
|
76
|
+
|
77
|
+
----
|
78
|
+
git clone git@github.com:scalefactory/tfctl.git
|
79
|
+
cd tfctl/ && sudo make install
|
80
|
+
----
|
81
|
+
|
82
|
+
== Set up AWS resources
|
83
|
+
|
84
|
+
It's assumed you have configured AWS CLI access to the primary account.
|
85
|
+
|
86
|
+
We'll use CloudFormation templates in `examples/bootstrap/`.
|
87
|
+
|
88
|
+
First export configuration using environment variables making sure to change to
|
89
|
+
values to suit your set up:
|
90
|
+
|
91
|
+
----
|
92
|
+
export PRIMARY_ACCOUNT_ID=11111111
|
93
|
+
export SHARED_SERVICES_ACCOUNT_ID=22222222
|
94
|
+
export STATE_BUCKET_NAME='example-terraform-state'
|
95
|
+
----
|
96
|
+
|
97
|
+
Create the remote state resources stack set:
|
98
|
+
|
99
|
+
----
|
100
|
+
cd examples/bootstrap/
|
101
|
+
|
102
|
+
aws cloudformation create-stack-set \
|
103
|
+
--stack-set-name TerraformState \
|
104
|
+
--template-body file://terraform-state.template \
|
105
|
+
--description "Resources for managing Terraform state" \
|
106
|
+
--capabilities CAPABILITY_NAMED_IAM CAPABILITY_IAM \
|
107
|
+
--execution-role-name AWSControlTowerExecution \
|
108
|
+
--administration-role-arn arn:aws:iam::${PRIMARY_ACCOUNT_ID}:role/service-role/AWSControlTowerStackSetRole \
|
109
|
+
--parameters ParameterKey=PrimaryAccountId,ParameterValue=${PRIMARY_ACCOUNT_ID} \
|
110
|
+
ParameterKey=TerraformStateBucket,ParameterValue=${STATE_BUCKET_NAME}
|
111
|
+
----
|
112
|
+
|
113
|
+
Create a stack set instance in you shared services account:
|
114
|
+
|
115
|
+
----
|
116
|
+
aws cloudformation create-stack-instances \
|
117
|
+
--stack-set-name TerraformState \
|
118
|
+
--accounts ${SHARED_SERVICES_ACCOUNT_ID} \
|
119
|
+
--regions eu-west-1
|
120
|
+
----
|
121
|
+
|
122
|
+
Check status:
|
123
|
+
|
124
|
+
----
|
125
|
+
aws cloudformation describe-stack-instance \
|
126
|
+
--stack-set-name TerraformState \
|
127
|
+
--stack-instance-account ${SHARED_SERVICES_ACCOUNT_ID} \
|
128
|
+
--stack-instance-region eu-west-1
|
129
|
+
----
|
130
|
+
|
131
|
+
NOTE: Initial status will be `OUTDATED`, it should change to `CURRENT` once deployed.
|
132
|
+
|
133
|
+
Deploy `TfctlOrgAccess` IAM role stack:
|
134
|
+
|
135
|
+
----
|
136
|
+
aws cloudformation create-stack \
|
137
|
+
--stack-name TfctlOrgAccess \
|
138
|
+
--template-body file://tfctl-org-access.template \
|
139
|
+
--capabilities CAPABILITY_NAMED_IAM CAPABILITY_IAM \
|
140
|
+
--parameters ParameterKey=PrimaryAccountId,ParameterValue=${PRIMARY_ACCOUNT_ID}
|
141
|
+
----
|
142
|
+
|
143
|
+
Check status:
|
144
|
+
|
145
|
+
----
|
146
|
+
aws cloudformation describe-stacks --stack-name TfctlOrgAccess
|
147
|
+
----
|
148
|
+
|
149
|
+
NOTE: Successful status should read: `CREATE_COMPLETE`.
|
150
|
+
|
151
|
+
== Configure tfctl
|
152
|
+
|
153
|
+
Copy the example project directory `examples/control_tower` somewhere convenient
|
154
|
+
and edit `conf/example.yaml`.
|
155
|
+
|
156
|
+
You need to modify the following parameters:
|
157
|
+
|
158
|
+
* `tf_state_bucket` - set to `$STATE_BUCKET_NAME`
|
159
|
+
* `tf_state_role_arn` - set shared services account ID
|
160
|
+
* `tfctl_role_arn` - set primary account ID
|
161
|
+
* `primary_account` - set the primary account name. You can find it in AWS Organizations.
|
162
|
+
|
163
|
+
TIP: You should keep your project directory under version control.
|
164
|
+
|
165
|
+
== Deploy example tfctl profile
|
166
|
+
|
167
|
+
The example profile will create an S3 bucket in accounts under `test`, `live`
|
168
|
+
and `mgmt` OUs.
|
169
|
+
|
170
|
+
NOTE: Run tfctl commands from the root of you project directory.
|
171
|
+
|
172
|
+
First dump the configuration to verify everything works:
|
173
|
+
|
174
|
+
----
|
175
|
+
tfctl -c conf/example.yaml -s
|
176
|
+
----
|
177
|
+
|
178
|
+
This will not make any changes but will print out a yaml containing the final,
|
179
|
+
merged configuration data. It should contain a list of discovered accounts and
|
180
|
+
their configuration.
|
181
|
+
|
182
|
+
Initialise terraform for all discovered accounts:
|
183
|
+
|
184
|
+
----
|
185
|
+
tfctl -c conf/example.yaml --all -- init
|
186
|
+
----
|
187
|
+
|
188
|
+
Tfctl will run Terraform against all accounts in parallel.
|
189
|
+
|
190
|
+
Run plan:
|
191
|
+
|
192
|
+
----
|
193
|
+
tfctl -c conf/example.yaml --all -- plan
|
194
|
+
----
|
195
|
+
|
196
|
+
and apply:
|
197
|
+
|
198
|
+
----
|
199
|
+
tfctl -c conf/example.yaml --all -- apply
|
200
|
+
----
|
201
|
+
|
202
|
+
To destroy created resources run:
|
203
|
+
|
204
|
+
----
|
205
|
+
tfctl -c conf/example.yaml --all -- destroy -auto-approve
|
206
|
+
----
|
207
|
+
|
208
|
+
That's it! You can now execute terraform across your Control Tower estate.
|
209
|
+
|
210
|
+
TIP: Your project directory should be under version control excluding the
|
211
|
+
`.tfctl` directory which is automatically generated.
|
@@ -0,0 +1,191 @@
|
|
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
|
+
= Creating and deploying a tfctl profile
|
22
|
+
|
23
|
+
This guide will show you how to create a tfctl profile, declare some resources
|
24
|
+
in it and deploy it to to a group of accounts in an organization unit.
|
25
|
+
|
26
|
+
toc::[]
|
27
|
+
|
28
|
+
== Create a new profile
|
29
|
+
|
30
|
+
In your tfctl project directory create a new profile:
|
31
|
+
|
32
|
+
----
|
33
|
+
mkdir profiles/example-profile
|
34
|
+
----
|
35
|
+
|
36
|
+
Withing the profile create `data.tf`:
|
37
|
+
|
38
|
+
.data.tf
|
39
|
+
[source, tf]
|
40
|
+
----
|
41
|
+
data "aws_caller_identity" "current" {}
|
42
|
+
----
|
43
|
+
|
44
|
+
This file contains Terraform
|
45
|
+
https://www.terraform.io/docs/configuration/data-sources.html[data source]
|
46
|
+
declarations. Data sources are a way of getting data not directly managed in
|
47
|
+
Terraform into Terraform. In this case we're using the
|
48
|
+
https://www.terraform.io/docs/providers/aws/d/caller_identity.html[aws_caller_identity]
|
49
|
+
. One of the outputs of this source is `account_id` which will
|
50
|
+
return the id of the account Terraform is currently running in.
|
51
|
+
|
52
|
+
Now create `variables.tf`:
|
53
|
+
|
54
|
+
.variables.tf
|
55
|
+
[source, tf]
|
56
|
+
----
|
57
|
+
variable "config" {
|
58
|
+
description = "Configuration generated by tfctl in string encoded JSON"
|
59
|
+
type = string
|
60
|
+
}
|
61
|
+
|
62
|
+
# local variables
|
63
|
+
locals {
|
64
|
+
# Decode config JSON into a Terraform data structure
|
65
|
+
config = jsondecode(var.config)
|
66
|
+
|
67
|
+
# Get current account id from aws_caller_identity data source
|
68
|
+
current_account_id = "${data.aws_caller_identity.current.account_id}"
|
69
|
+
|
70
|
+
# Get tfctl configuration for the current account
|
71
|
+
current_account_conf = [ for account in local.config["accounts"]: account if account["id"] == local.current_account_id ][0]
|
72
|
+
}
|
73
|
+
----
|
74
|
+
|
75
|
+
This file contains
|
76
|
+
https://www.terraform.io/docs/configuration/variables.html[input variables] for
|
77
|
+
the profile.
|
78
|
+
|
79
|
+
The `config` variable is special and must always be declared in a tfctl
|
80
|
+
profile. Tfctl configuration can be accessed using this variable. This It
|
81
|
+
includes an array of all discovered accounts as well their parameters from
|
82
|
+
tfctl config file.
|
83
|
+
|
84
|
+
TIP: You can run `tfctl -c conf/CONFIG_FILE.yaml -s` to show the config data in
|
85
|
+
yaml format. This exact data is available in the `config` variable in your
|
86
|
+
profile.
|
87
|
+
|
88
|
+
We also have few https://www.terraform.io/docs/configuration/locals.html[local
|
89
|
+
variables] in the `locals` block. We assign the current account id from the
|
90
|
+
data source we defined previously to `current_account_id`. This is mainly for
|
91
|
+
convenience to make the next statement easier to read. `current_account` loops
|
92
|
+
over the `config` data and returns configuration for an account which matches
|
93
|
+
the current account id (i.e. the current account configuration).
|
94
|
+
|
95
|
+
Now that we have our data inputs sorted we can start declaring actual AWS
|
96
|
+
resources to manage.
|
97
|
+
|
98
|
+
Create `main.tf`:
|
99
|
+
|
100
|
+
.main.tf
|
101
|
+
[source, tf]
|
102
|
+
----
|
103
|
+
resource "aws_s3_bucket" "example" {
|
104
|
+
bucket = "tfctl-${local.current_account_conf["name"]}"
|
105
|
+
acl = "private"
|
106
|
+
}
|
107
|
+
----
|
108
|
+
|
109
|
+
This will create an S3 bucket with a name containing the current account name
|
110
|
+
(which will vary depending on which account it's deployed to).
|
111
|
+
|
112
|
+
== Assign profile to accounts
|
113
|
+
|
114
|
+
Before you can deploy the new profile you need to tell `tfctl` which accounts
|
115
|
+
to deploy it to.
|
116
|
+
|
117
|
+
You have few options here:
|
118
|
+
|
119
|
+
* deploy to all accounts
|
120
|
+
* deploy to specific organization unit (OU)
|
121
|
+
* deploy to individual account
|
122
|
+
|
123
|
+
|
124
|
+
For the sake of this example we're going to deploy our bucket to all accounts
|
125
|
+
in `test` OU.
|
126
|
+
|
127
|
+
In tfctl config file add the profile to the `test` OU:
|
128
|
+
|
129
|
+
[source, yaml]
|
130
|
+
----
|
131
|
+
organization_units:
|
132
|
+
test:
|
133
|
+
profiles:
|
134
|
+
- example-profile
|
135
|
+
----
|
136
|
+
|
137
|
+
|
138
|
+
== Plan
|
139
|
+
|
140
|
+
To see what would happen when the change is applied run:
|
141
|
+
|
142
|
+
----
|
143
|
+
tfctl -c conf/example.yaml -o test -- init
|
144
|
+
tfctl -c conf/example.yaml -o test -- plan
|
145
|
+
----
|
146
|
+
|
147
|
+
This will run `terraform init` to initialise terraform and then `terraform
|
148
|
+
plan` across all accounts in the `test` OU in parallel. It will display a diff
|
149
|
+
of changes for each account.
|
150
|
+
|
151
|
+
.example terraform plan
|
152
|
+
----
|
153
|
+
info: test-example: Terraform will perform the following actions:
|
154
|
+
info: test-example:
|
155
|
+
info: test-example: # module.example-profile.aws_s3_bucket.example will be created
|
156
|
+
info: test-example: + resource "aws_s3_bucket" "example" {
|
157
|
+
info: test-example: + acceleration_status = (known after apply)
|
158
|
+
info: test-example: + acl = "private"
|
159
|
+
info: test-example: + arn = (known after apply)
|
160
|
+
info: test-example: + bucket = "tfctl-test-example"
|
161
|
+
info: test-example: + bucket_domain_name = (known after apply)
|
162
|
+
info: test-example: + bucket_regional_domain_name = (known after apply)
|
163
|
+
info: test-example: + force_destroy = false
|
164
|
+
info: test-example: + hosted_zone_id = (known after apply)
|
165
|
+
info: test-example: + id = (known after apply)
|
166
|
+
info: test-example: + region = (known after apply)
|
167
|
+
info: test-example: + request_payer = (known after apply)
|
168
|
+
info: test-example: + website_domain = (known after apply)
|
169
|
+
info: test-example: + website_endpoint = (known after apply)
|
170
|
+
info: test-example:
|
171
|
+
info: test-example: + versioning {
|
172
|
+
info: test-example: + enabled = (known after apply)
|
173
|
+
info: test-example: + mfa_delete = (known after apply)
|
174
|
+
info: test-example: }
|
175
|
+
info: test-example: }
|
176
|
+
info: test-example:
|
177
|
+
info: test-example: Plan: 1 to add, 0 to change, 0 to destroy.
|
178
|
+
----
|
179
|
+
|
180
|
+
If there are errors in your profile, terraform will fail and usually indicate
|
181
|
+
what went wrong.
|
182
|
+
|
183
|
+
tfctl will generate a plan file automatically and use it with `apply` in the
|
184
|
+
next step.
|
185
|
+
|
186
|
+
== Apply
|
187
|
+
|
188
|
+
Once you're happy with the plan, apply it.
|
189
|
+
----
|
190
|
+
tfctl -c conf/example.yaml -o test -- apply
|
191
|
+
----
|