-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfeed.xml
More file actions
2120 lines (1641 loc) · 238 KB
/
Copy pathfeed.xml
File metadata and controls
2120 lines (1641 loc) · 238 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.0">Jekyll</generator><link href="https://thirstydeveloper.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://thirstydeveloper.io/" rel="alternate" type="text/html" /><updated>2022-01-02T12:12:45+00:00</updated><id>https://thirstydeveloper.io/feed.xml</id><title type="html">thirstydeveloper</title><subtitle>Infrastructure-as-Code-as-a-Team</subtitle><author><name>Chris Kent</name></author><entry><title type="html">Terraform Skeleton Part 6: Protecting State</title><link href="https://thirstydeveloper.io/tf-skeleton/2021/02/25/part-6-protecting-state.html" rel="alternate" type="text/html" title="Terraform Skeleton Part 6: Protecting State" /><published>2021-02-25T19:00:00+00:00</published><updated>2021-02-25T19:00:00+00:00</updated><id>https://thirstydeveloper.io/tf-skeleton/2021/02/25/part-6-protecting-state</id><content type="html" xml:base="https://thirstydeveloper.io/tf-skeleton/2021/02/25/part-6-protecting-state.html"><p><a href="/tf-skeleton/2021/02/17/part-5-cfn-terraform-state.html">Part 5</a> moved Terraform’s operational infrastructure <a href="https://github.com/thirstydeveloper/terraform-terragrunt-skeleton/compare/release/1.3...release/1.4#diff-cc93827adede14cb1e69dde0ed0769a47014bbb00f7ec444c61b927f3901c35a">into a CloudFormation stack</a> so that Terragrunt no longer manages the state bucket, lock table, and log bucket. Doing so offers us the opportunity to protect these resources in ways not supported by Terragrunt. We will capitalize on that opportunity today.</p>
<p>We’ll cover adding a bucket policy to our CloudFormation template to restrict access to Terraform’s state, leveraging the backend role and <code class="language-plaintext highlighter-rouge">Terraformer</code> principal tag introduced in <a href="/tf-skeleton/2021/02/10/part-4-backend-role.html">Part 4</a>. I hope my example will ease your navigation of IAM policy syntax and save you the pain of decrypting unhelpful IAM error messages.</p>
<p>We’ll also add some basic protections to the log bucket that Terragrunt fails to implement out of the box.</p>
<h1 id="goals">Goals</h1>
<ul>
<li>Grant only authorized principals access to Terraform state</li>
<li>Protect the log bucket in a similar fashion to the state bucket</li>
<li>Remove unnecessary permissions from the backend role</li>
</ul>
<p>If you prefer to jump to the end, the code implementing this post’s final result is available on branch <a href="https://github.com/thirstydeveloper/terraform-terragrunt-skeleton/tree/release/1.5">release/1.5</a> on GitHub. Additionally, you can <a href="https://github.com/thirstydeveloper/terraform-terragrunt-skeleton/compare/release/1.4...release/1.5">view the diffs from part 5</a>, if that’s more your speed.</p>
<h1 id="access-roles">Access Roles</h1>
<p>There are three types of authorized access to Terraform state we will cover today.</p>
<p>First is our backend role, which the Terragrunt configures Terraform to use for all state operations via the <code class="language-plaintext highlighter-rouge">root.hcl</code> from <a href="/tf-skeleton/2021/02/10/part-4-backend-role.html">Part 4</a>, reprinted here:</p>
<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">remote_state</span> <span class="p">{</span>
<span class="nx">backend</span> <span class="p">=</span> <span class="s2">"s3"</span>
<span class="nx">generate</span> <span class="p">=</span> <span class="p">{</span>
<span class="nx">path</span> <span class="p">=</span> <span class="s2">"backend.tf"</span>
<span class="nx">if_exists</span> <span class="p">=</span> <span class="s2">"overwrite"</span>
<span class="p">}</span>
<span class="nx">config</span> <span class="p">=</span> <span class="p">{</span>
<span class="nx">bucket</span> <span class="p">=</span> <span class="s2">"terraform-skeleton-state"</span>
<span class="nx">region</span> <span class="p">=</span> <span class="s2">"us-east-1"</span>
<span class="nx">encrypt</span> <span class="p">=</span> <span class="kc">true</span>
<span class="nx">role_arn</span> <span class="p">=</span> <span class="s2">"arn:aws:iam::${get_aws_account_id()}:role/terraform/TerraformBackend"</span>
<span class="nx">key</span> <span class="p">=</span> <span class="s2">"${dirname(local.relative_deployment_path)}/${local.stack}.tfstate"</span>
<span class="nx">dynamodb_table</span> <span class="p">=</span> <span class="s2">"terraform-skeleton-state-locks"</span>
<span class="nx">accesslogging_bucket_name</span> <span class="p">=</span> <span class="s2">"terraform-skeleton-state-logs"</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Second are humans, who may wish to interact with the state for any number of reasons. One example is changing the name of an existing stack (which in our setup will change the name of the state file).</p>
<p>I like to differentiate between developer-level and administrative-level access to Terraform state, the primary difference being that only administrators should be allowed to delete state files <em>permanently</em>. Deleting a state file is not something to take lightly. If you delete one you still need by accident, and you have no backups, your only choice will be to reconstruct it by hand.</p>
<p>The policy we implement today will grant:</p>
<ul>
<li>Full access to administrative IAM users, including the AWS account’s root user</li>
<li>Limited access to developer IAM users and the backend role</li>
<li>Deny access to anyone else</li>
</ul>
<h1 id="state-bucket-policy">State Bucket Policy</h1>
<p>We will use an S3 bucket policy to implement the above restrictions. An S3 bucket policy is an ideal choice for two reasons. First, it directly applies permissions to the resource we want to protect (the state bucket). Second, developers can likely create it themselves, unlike adding permissions to IAM users, something typically controlled by an enterprise team.</p>
<p>We’ll add the bucket policy as a resource to our <code class="language-plaintext highlighter-rouge">init-admin-account.cf.yml</code> CloudFormation template. We’ll break the policy down statement by statement.</p>
<p>The first statement requires TLS encryption for any requests accessing Terraform state. This policy Terragrunt creates for you; we preserve it here.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">TerraformStateBucketPolicy</span><span class="pi">:</span>
<span class="na">Type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">AWS::S3::BucketPolicy'</span>
<span class="na">DeletionPolicy</span><span class="pi">:</span> <span class="s">Retain</span>
<span class="na">UpdateReplacePolicy</span><span class="pi">:</span> <span class="s">Retain</span>
<span class="na">Properties</span><span class="pi">:</span>
<span class="na">Bucket</span><span class="pi">:</span> <span class="kt">!Ref</span> <span class="s">TerraformStateBucket</span>
<span class="na">PolicyDocument</span><span class="pi">:</span>
<span class="na">Version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">2012-10-17'</span>
<span class="na">Statement</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">Sid</span><span class="pi">:</span> <span class="s1">'</span><span class="s">AllowTLSRequestsOnly'</span>
<span class="na">Principal</span><span class="pi">:</span> <span class="s1">'</span><span class="s">*'</span>
<span class="na">Condition</span><span class="pi">:</span>
<span class="na">Bool</span><span class="pi">:</span>
<span class="s1">'</span><span class="s">aws:SecureTransport'</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">Effect</span><span class="pi">:</span> <span class="s">Deny</span>
<span class="na">Action</span><span class="pi">:</span> <span class="s1">'</span><span class="s">*'</span>
<span class="na">Resource</span><span class="pi">:</span>
<span class="pi">-</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
<span class="pi">-</span> <span class="kt">!Sub</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">${Bucket}/*"</span>
<span class="pi">-</span> <span class="na">Bucket</span><span class="pi">:</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
</code></pre></div></div>
<p>The second statement uses the <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_variables.html#principaltable">aws:PrincipalType</a> and <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-principaltag">aws:PrincipalTag</a> condition keys to deny access to any IAM users lacking the <code class="language-plaintext highlighter-rouge">Terraformer</code> principal tag we introduced in <a href="/tf-skeleton/2021/02/10/part-4-backend-role.html">Part 4</a>:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="na">Sid</span><span class="pi">:</span> <span class="s">DenyNonTerraformerUsers</span>
<span class="na">Principal</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*"</span>
<span class="na">Condition</span><span class="pi">:</span>
<span class="na">StringEquals</span><span class="pi">:</span>
<span class="s">aws:PrincipalType: User</span>
<span class="na">StringNotLike</span><span class="pi">:</span>
<span class="s1">'</span><span class="s">aws:PrincipalTag/Terraformer'</span><span class="pi">:</span> <span class="s1">'</span><span class="s">*'</span>
<span class="na">Effect</span><span class="pi">:</span> <span class="s">Deny</span>
<span class="na">Action</span><span class="pi">:</span> <span class="s1">'</span><span class="s">*'</span>
<span class="na">Resource</span><span class="pi">:</span>
<span class="pi">-</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
<span class="pi">-</span> <span class="kt">!Sub</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">${Bucket}/*"</span>
<span class="pi">-</span> <span class="na">Bucket</span><span class="pi">:</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
</code></pre></div></div>
<p>The third statement begins differentiating between administrative and non-administrative access to the state. We do so again using the <code class="language-plaintext highlighter-rouge">Terraformer</code> tag, this time inspecting its value.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="na">Sid</span><span class="pi">:</span> <span class="s">RestrictTerraformNonAdmins</span>
<span class="na">Principal</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*"</span>
<span class="na">Condition</span><span class="pi">:</span>
<span class="na">StringEquals</span><span class="pi">:</span>
<span class="s">aws:PrincipalType: User</span>
<span class="na">StringLike</span><span class="pi">:</span>
<span class="s1">'</span><span class="s">aws:PrincipalTag/Terraformer'</span><span class="pi">:</span> <span class="s1">'</span><span class="s">*'</span>
<span class="na">StringNotEquals</span><span class="pi">:</span>
<span class="s1">'</span><span class="s">aws:PrincipalTag/Terraformer'</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Admin'</span>
<span class="na">Effect</span><span class="pi">:</span> <span class="s">Deny</span>
<span class="na">NotAction</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">s3:List*'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">s3:Get*'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">s3:Describe*'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">s3:PutObject'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">s3:DeleteObject'</span>
<span class="na">Resource</span><span class="pi">:</span>
<span class="pi">-</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
<span class="pi">-</span> <span class="kt">!Sub</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">${Bucket}/*"</span>
<span class="pi">-</span> <span class="na">Bucket</span><span class="pi">:</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
</code></pre></div></div>
<p>If the IAM user has the <code class="language-plaintext highlighter-rouge">Terraformer</code> tag, but its value is not <code class="language-plaintext highlighter-rouge">Admin</code>, we grant non-administrative access to that user. We use IAM’s <code class="language-plaintext highlighter-rouge">NotAction</code> to whitelist the permitted actions.</p>
<p>Notably, non-administrative access permits <code class="language-plaintext highlighter-rouge">s3:DeleteObject</code> but <em>not</em> <code class="language-plaintext highlighter-rouge">s3:DeleteObjectVersion</code>. Since our state bucket is versioned (see <a href="/tf-skeleton/2021/02/17/part-5-cfn-terraform-state.html#define-operational-infrastructure-with-cloudformation">Part 5</a>), granting <code class="language-plaintext highlighter-rouge">s3:DeleteObject</code> is not inherently dangerous because all it does is add a delete marker to the object; you can always restore the version before the delete marker. Granting developers the ability to add delete markers aids state migration, so we do so here.</p>
<p>The fourth statement denies access to all IAM roles other than our backend role:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="na">Sid</span><span class="pi">:</span> <span class="s">DenyNonBackendRoles</span>
<span class="na">Principal</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*"</span>
<span class="na">Condition</span><span class="pi">:</span>
<span class="na">StringEquals</span><span class="pi">:</span>
<span class="s">aws:PrincipalType: AssumedRole</span>
<span class="na">StringNotLike</span><span class="pi">:</span>
<span class="s">aws:userId:</span>
<span class="s">- !Sub</span>
<span class="s">- "${TerraformBackendRoleId}:*"</span>
<span class="s">- TerraformBackendRoleId</span><span class="pi">:</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformBackendRole.RoleId"</span>
<span class="na">Effect</span><span class="pi">:</span> <span class="s">Deny</span>
<span class="na">Action</span><span class="pi">:</span> <span class="s1">'</span><span class="s">*'</span>
<span class="na">Resource</span><span class="pi">:</span>
<span class="pi">-</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
<span class="pi">-</span> <span class="kt">!Sub</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">${Bucket}/*"</span>
<span class="pi">-</span> <span class="na">Bucket</span><span class="pi">:</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
</code></pre></div></div>
<p>I did not find the syntax intuitive for restricting access to a specific IAM role. Specifying the role ARN in the <code class="language-plaintext highlighter-rouge">Principal</code> property does not work. <a href="https://aws.amazon.com/blogs/security/how-to-restrict-amazon-s3-bucket-access-to-a-specific-iam-role/">This AWS post explains why</a> and demonstrates using the <code class="language-plaintext highlighter-rouge">StringNotLike</code> and <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_variables.html#principaltable">aws:userId</a> combination I use here.</p>
<p>The fifth statement grants the backend role access:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="na">Sid</span><span class="pi">:</span> <span class="s">ResrictBackendRoleToReadWrite</span>
<span class="na">Principal</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*"</span>
<span class="na">Condition</span><span class="pi">:</span>
<span class="na">StringEquals</span><span class="pi">:</span>
<span class="s">aws:PrincipalType: AssumedRole</span>
<span class="na">StringLike</span><span class="pi">:</span>
<span class="s">aws:userId:</span>
<span class="s">- !Sub</span>
<span class="s">- "${TerraformBackendRoleId}:*"</span>
<span class="s">- TerraformBackendRoleId</span><span class="pi">:</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformBackendRole.RoleId"</span>
<span class="na">Effect</span><span class="pi">:</span> <span class="s">Deny</span>
<span class="na">NotAction</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">s3:ListBucket'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">s3:GetBucketVersioning'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">s3:GetObject'</span>
<span class="pi">-</span> <span class="s1">'</span><span class="s">s3:PutObject'</span>
<span class="na">Resource</span><span class="pi">:</span>
<span class="pi">-</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
<span class="pi">-</span> <span class="kt">!Sub</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">${Bucket}/*"</span>
<span class="pi">-</span> <span class="na">Bucket</span><span class="pi">:</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
</code></pre></div></div>
<p>And our final statement denies access to any other principal types (e.g., FederatedUsers), as we’re not considering those as part of this skeleton.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="na">Sid</span><span class="pi">:</span> <span class="s">DenyAllOtherPrincipals</span>
<span class="na">Principal</span><span class="pi">:</span> <span class="s2">"</span><span class="s">*"</span>
<span class="na">Condition</span><span class="pi">:</span>
<span class="na">StringNotEquals</span><span class="pi">:</span>
<span class="s">aws:PrincipalType:</span>
<span class="s">- AssumedRole</span>
<span class="s">- Account</span>
<span class="s">- User</span>
<span class="na">Effect</span><span class="pi">:</span> <span class="s">Deny</span>
<span class="na">Action</span><span class="pi">:</span> <span class="s1">'</span><span class="s">*'</span>
<span class="na">Resource</span><span class="pi">:</span>
<span class="pi">-</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
<span class="pi">-</span> <span class="kt">!Sub</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">${Bucket}/*"</span>
<span class="pi">-</span> <span class="na">Bucket</span><span class="pi">:</span> <span class="kt">!GetAtt</span> <span class="s2">"</span><span class="s">TerraformStateBucket.Arn"</span>
</code></pre></div></div>
<p>A summary of all changes is available by <a href="https://github.com/thirstydeveloper/terraform-terragrunt-skeleton/compare/release/1.4...release/1.5">viewing the diffs from part 5</a>.</p>
<p>There are three last items to note about this policy.</p>
<p>First, the bucket policy does not contain any permissions for users who have the <code class="language-plaintext highlighter-rouge">Terraformer</code> tag set to <code class="language-plaintext highlighter-rouge">Admin</code>. The lack of permissions means such users will have whatever access the IAM policy attached to their IAM user grants, presumably full access to S3.</p>
<p>Second, the bucket policy does not explicitly grant the AWS account’s root user access. <a href="https://aws.amazon.com/premiumsupport/knowledge-center/s3-access-denied-bucket-policy/">AWS always allows the root user access</a> to remove or modify bucket policies of buckets owned by that root user’s account, making it unnecessary to specify here.</p>
<p>Finally, none of the permissions in the policy grant either adding or modifying the bucket policy, which means that aside from the root user, only <code class="language-plaintext highlighter-rouge">Admin</code> Terraformers can do so, assuming those admins have the requisite permissions on their IAM user).</p>
<h1 id="applying-the-state-bucket-policy">Applying the State Bucket Policy</h1>
<p>We’re now ready to deploy the bucket policy.</p>
<p>First, change the <code class="language-plaintext highlighter-rouge">Terraformer</code> tag on your IAM user to have a value of <code class="language-plaintext highlighter-rouge">Admin</code>.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws iam tag-user <span class="se">\</span>
<span class="nt">--user-name</span> <span class="k">${</span><span class="nv">IAM_USER</span><span class="k">}</span> <span class="se">\</span>
<span class="nt">--tags</span> <span class="s1">'{
"Key": "Terraformer",
"Value": "Admin"
}'</span>
</code></pre></div></div>
<p>Second, if Terragrunt created the state bucket for you, it may already have a bucket policy attached to it. Delete it using the following CLI command, replacing the bucket name as appropriate:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3api delete-bucket-policy <span class="nt">--bucket</span> terraform-skeleton-state
</code></pre></div></div>
<p>Deploy the updated CloudFormation template using the <code class="language-plaintext highlighter-rouge">init-admin</code> Makefile target we added in <a href="/tf-skeleton/2021/02/10/part-4-backend-role.html">Part 4</a>.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote">1</a></sup></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>make init-admin
aws cloudformation deploy <span class="se">\</span>
<span class="nt">--template-file</span> init/admin/init-admin-account.cf.yml <span class="se">\</span>
<span class="nt">--stack-name</span> tf-admin-init <span class="se">\</span>
<span class="nt">--capabilities</span> CAPABILITY_NAMED_IAM <span class="se">\</span>
<span class="nt">--parameter-overrides</span> <span class="se">\</span>
<span class="nv">AdminAccountId</span><span class="o">=</span>&lt;omitted&gt; <span class="se">\</span>
<span class="nv">StateBucketName</span><span class="o">=</span>terraform-skeleton-state <span class="se">\</span>
<span class="nv">StateLogBucketName</span><span class="o">=</span>terraform-skeleton-state-logs <span class="se">\</span>
<span class="nv">LockTableName</span><span class="o">=</span>terraform-skeleton-state-locks
Waiting <span class="k">for </span>changeset to be created..
Waiting <span class="k">for </span>stack create/update to <span class="nb">complete
</span>Successfully created/updated stack - tf-admin-init
aws cloudformation update-termination-protection <span class="se">\</span>
<span class="nt">--stack-name</span> tf-admin-init <span class="se">\</span>
<span class="nt">--enable-termination-protection</span>
<span class="o">{</span>
<span class="s2">"StackId"</span>: <span class="s2">"arn:aws:cloudformation:us-east-1:&lt;omitted&gt;:stack/tf-admin-init/8704b070-5f61-11eb-9ff1-0eea077046db"</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Let’s see if it works as expected.</p>
<h1 id="testing-the-state-bucket-policy">Testing the State Bucket Policy</h1>
<p>First, we can quickly verify the backend role has the access it requires by running <code class="language-plaintext highlighter-rouge">terragrunt apply-all</code>.</p>
<p>Verifying user-level access requires manipulating the <code class="language-plaintext highlighter-rouge">Terraformer</code> tag. Ideally, we’d have these tests automated and run as part of a CI pipeline. Perhaps we’ll get to that in another post.</p>
<p>Remove the <code class="language-plaintext highlighter-rouge">Terraformer</code> tag from your user altogether:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws iam untag-user <span class="nt">--user-name</span> <span class="k">${</span><span class="nv">IAM_USER</span><span class="k">}</span> <span class="nt">--tag-keys</span> Terraformer
</code></pre></div></div>
<p>and verify you can’t even list the state bucket now:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3 ls s3://terraform-skeleton-state
An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied
</code></pre></div></div>
<p>Next, set the <code class="language-plaintext highlighter-rouge">Terraformer</code> tag on your IAM user to something other than <code class="language-plaintext highlighter-rouge">Admin</code>. For instance:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws iam tag-user <span class="se">\</span>
<span class="nt">--user-name</span> <span class="k">${</span><span class="nv">IAM_USER</span><span class="k">}</span> <span class="se">\</span>
<span class="nt">--tags</span> <span class="s1">'{
"Key": "Terraformer",
"Value": "User"
}'</span>
</code></pre></div></div>
<p>Then verify you cannot update the state bucket’s policy:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3api put-bucket-policy <span class="nt">--bucket</span> terraform-skeleton-state <span class="nt">--policy</span> <span class="s2">""</span>
An error occurred <span class="o">(</span>AccessDenied<span class="o">)</span> when calling the PutBucketPolicy operation: Access Denied
</code></pre></div></div>
<p>or delete it:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3api delete-bucket-policy <span class="nt">--bucket</span> terraform-skeleton-state
An error occurred <span class="o">(</span>AccessDenied<span class="o">)</span> when calling the DeleteBucketPolicy operation: Access Denied
</code></pre></div></div>
<p>Also, verify you <em>can</em> delete a state object:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3 <span class="nb">rm </span>s3://terraform-skeleton-state/app/dev/test-stack.tfstate
delete: s3://terraform-skeleton-state/app/dev/test-stack.tfstate
</code></pre></div></div>
<p>but can’t delete an object version:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">DELETE_MARKER_VERSION</span><span class="o">=</span><span class="si">$(</span>aws s3api list-object-versions <span class="se">\</span>
<span class="nt">--bucket</span> terraform-skeleton-state <span class="se">\</span>
<span class="nt">--prefix</span> app/dev/test-stack.tfstate <span class="se">\</span>
<span class="nt">--query</span> <span class="s1">'DeleteMarkers[?IsLatest==`true`].VersionId'</span> | jq <span class="nt">-r</span> <span class="s1">'.[0]'</span> <span class="se">\</span>
<span class="si">)</span>
aws s3api delete-object <span class="se">\</span>
<span class="nt">--bucket</span> terraform-skeleton-state <span class="se">\</span>
<span class="nt">--key</span> app/dev/test-stack.tfstate <span class="se">\</span>
<span class="nt">--version-id</span> <span class="k">${</span><span class="nv">DELETE_MARKER_VERSION</span><span class="k">}</span>
An error occurred <span class="o">(</span>AccessDenied<span class="o">)</span> when calling the DeleteObject operation: Access Denied
</code></pre></div></div>
<p>The inability to remove the delete marker does introduce a hurdle users will have to clear to restore state files, but restoration is still possible. See the footnotes for more information.<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote">2</a></sup></p>
<p>Change the <code class="language-plaintext highlighter-rouge">Terraformer</code> tag to <code class="language-plaintext highlighter-rouge">Admin</code> again:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws iam tag-user <span class="se">\</span>
<span class="nt">--user-name</span> <span class="k">${</span><span class="nv">IAM_USER</span><span class="k">}</span> <span class="se">\</span>
<span class="nt">--tags</span> <span class="s1">'{
"Key": "Terraformer",
"Value": "Admin"
}'</span>
</code></pre></div></div>
<p>and delete the delete marker again, verifying it works this time.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3api delete-object <span class="se">\</span>
<span class="nt">--bucket</span> terraform-skeleton-state <span class="se">\</span>
<span class="nt">--key</span> app/dev/test-stack.tfstate <span class="se">\</span>
<span class="nt">--version-id</span> <span class="k">${</span><span class="nv">DELETE_MARKER_VERSION</span><span class="k">}</span>
<span class="o">{</span>
<span class="s2">"DeleteMarker"</span>: <span class="nb">true</span>,
<span class="s2">"VersionId"</span>: <span class="s2">"..."</span>
<span class="o">}</span>
</code></pre></div></div>
<p>That about covers it. We now have a bucket policy restricting access to Terraform’s state to only authorized principals.</p>
<h1 id="cleanup">Cleanup</h1>
<p>With the bucket policy in place, let’s turn our attention to a couple of other hardening items we can tackle in the CloudFormation template: protecting the logs bucket and removing unnecessary permissions from the backend role.</p>
<h2 id="protecting-the-logs-bucket">Protecting the Logs Bucket</h2>
<p>As discussed in <a href="/tf-skeleton/2021/01/28/part-3-aws-backend.html#limitations">Part 3</a>, when Terragrunt creates the log bucket for you, it does not enable encryption or explicitly block public access. We can rectify both of those issues now that the bucket is under CloudFormation’s control.</p>
<p>Using our <code class="language-plaintext highlighter-rouge">TerraformStateBucket</code> resource as a template, add <code class="language-plaintext highlighter-rouge">BucketEncryption</code> and <code class="language-plaintext highlighter-rouge">PublicAccessBlockConfiguration</code> properties to the log bucket:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="na">TerraformStateLogBucket</span><span class="pi">:</span>
<span class="na">Type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">AWS::S3::Bucket'</span>
<span class="na">DeletionPolicy</span><span class="pi">:</span> <span class="s">Retain</span>
<span class="na">UpdateReplacePolicy</span><span class="pi">:</span> <span class="s">Retain</span>
<span class="na">Properties</span><span class="pi">:</span>
<span class="na">BucketName</span><span class="pi">:</span> <span class="kt">!Ref</span> <span class="s">StateLogBucketName</span>
<span class="na">AccessControl</span><span class="pi">:</span> <span class="s">LogDeliveryWrite</span>
<span class="na">BucketEncryption</span><span class="pi">:</span>
<span class="na">ServerSideEncryptionConfiguration</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">ServerSideEncryptionByDefault</span><span class="pi">:</span>
<span class="na">SSEAlgorithm</span><span class="pi">:</span> <span class="s">aws:kms</span>
<span class="na">PublicAccessBlockConfiguration</span><span class="pi">:</span>
<span class="na">BlockPublicAcls</span><span class="pi">:</span> <span class="s">True</span>
<span class="na">BlockPublicPolicy</span><span class="pi">:</span> <span class="s">True</span>
<span class="na">IgnorePublicAcls</span><span class="pi">:</span> <span class="s">True</span>
<span class="na">RestrictPublicBuckets</span><span class="pi">:</span> <span class="s">True</span>
</code></pre></div></div>
<p>Run <code class="language-plaintext highlighter-rouge">make init-admin</code> to deploy.</p>
<h2 id="backend-role-permissions">Backend Role Permissions</h2>
<p>When we created the backend role in <a href="/tf-skeleton/2021/02/10/part-4-backend-role.html#the-backend-role">Part 4</a>, we granted it permissions to create S3 buckets and DynamoDB tables because Terragrunt managed our state bucket and lock table. We can remove those permissions now that CloudFormation deploys both:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/init/admin/init-admin-account.cf.yml b/init/admin/init-admin-account.cf.yml
index b8dcda0..3aed8e5 100644
</span><span class="gd">--- a/init/admin/init-admin-account.cf.yml
</span><span class="gi">+++ b/init/admin/init-admin-account.cf.yml
</span><span class="p">@@ -122,27 +122,6 @@</span> Resources:
- 'dynamodb:PutItem'
- 'dynamodb:DeleteItem'
Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${LockTableName}"
<span class="gd">- - Sid: AllowStateBucketCreation
- Effect: Allow
- Action:
- - 's3:GetBucketAcl'
- - 's3:GetBucketLogging'
- - 's3:CreateBucket'
- - 's3:PutBucketPublicAccessBlock'
- - 's3:PutBucketTagging'
- - 's3:PutBucketPolicy'
- - 's3:PutBucketVersioning'
- - 's3:PutEncryptionConfiguration'
- - 's3:PutBucketAcl'
- - 's3:PutBucketLogging'
- Resource:
- - !Sub "arn:aws:s3:::${StateBucketName}"
- - !Sub "arn:aws:s3:::${StateLogBucketName}"
- - Sid: AllowLockTableCreation
- Effect: Allow
- Action:
- - 'dynamodb:CreateTable'
- Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${LockTableName}"
</span></code></pre></div></div>
<p>Run a <code class="language-plaintext highlighter-rouge">terragrunt apply-all</code> to verify all’s well.</p>
<h1 id="whats-next">What’s Next?</h1>
<p>In this post, we’ve significantly enhanced the protections surrounding the Terraform state. Looking back at these first six entries, we’ve covered a lot of ground, and I think it will be worthwhile to take a step back and summarize what we’ve done so far. After a recap, I’d like to start covering continuous integration with Terraform and Terragrunt. We’ll see where that takes us.</p>
<h1 id="footnotes">Footnotes</h1>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>I hope that you don’t encounter any errors deploying the bucket policy. If you do, AWS is often not much help diagnosing what went wrong. Here are some pointers.</p>
<p>If <code class="language-plaintext highlighter-rouge">make init-admin</code> fails, it will likely say:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Failed to create/update the stack. Run the following <span class="nb">command </span>to fetch the
list of events leading up to the failure
aws cloudformation describe-stack-events <span class="nt">--stack-name</span> tf-admin-init
</code></pre></div> </div>
<p>If you do so, you’ll get a JSON dump of events. Since you presumably failed when creating the bucket policy, look for an event with a <code class="language-plaintext highlighter-rouge">ResourceStatus</code> field set to <code class="language-plaintext highlighter-rouge">CREATE_FAILED</code>. Here’s an abbreviated example:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"StackName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tf-admin-init"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ResourceStatus"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CREATE_FAILED"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ResourceStatusReason"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Invalid policy syntax. (Service: Amazon S3; Status Code: 400; Error Code: MalformedPolicy; Request ID: 9EB1BD50BCF1FCB7; S3 Extended Request ID: hHkqY/snGYyhc4paSxhBT1IzpmgoWjKvz5I/JYiYUKu3PLSn1CWuAceLU7QEckf/omDhF4ZdeGU=; Proxy: null)"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ResourceProperties"</span><span class="p">:</span><span class="w"> </span><span class="s2">"{</span><span class="se">\"</span><span class="s2">Bucket</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">terraform-skeleton-state</span><span class="se">\"</span><span class="s2">..."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div> </div>
<p>If you have an <code class="language-plaintext highlighter-rouge">Invalid policy syntax</code> error, I recommend removing statements until the policy works, then adding them back in one-by-one until you find the problem. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
</li>
<li id="fn:2" role="doc-endnote">
<p>Since users cannot delete object versions, they cannot restore a state file by deleting its delete marker since it is itself an object version.</p>
<p>As discussed in the AWS docs <a href="https://aws.amazon.com/premiumsupport/knowledge-center/s3-undelete-configuration/">here</a> and <a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/RestoringPreviousVersions.html">here</a>, users can still restore state files by copying a previous version to become the latest. Here’s an example of how:</p>
<p>Step 1: Find version to restore (e.g., using the <code class="language-plaintext highlighter-rouge">LastModified</code> field to limit the search area).</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3api list-object-versions <span class="se">\</span>
<span class="nt">--bucket</span> terraform-skeleton-state <span class="se">\</span>
<span class="nt">--key</span> app/dev/test-stack.tfstate <span class="se">\</span>
<span class="nt">--query</span> <span class="s1">'Versions[?contains(LastModified, `'</span><span class="s2">"2021-02-20"</span><span class="s1">'`)]'</span> <span class="se">\</span>
| jq <span class="s1">'.[] | { Key, VersionId, LastModified }'</span>
<span class="o">{</span>
<span class="s2">"Key"</span>: <span class="s2">"app/stage/test-stack.tfstate"</span>,
<span class="s2">"VersionId"</span>: <span class="s2">"lwMes1R1AfkEZ.lQ4U9d217yeU7rWbcj"</span>,
<span class="s2">"LastModified"</span>: <span class="s2">"2021-02-20T13:03:50+00:00"</span>
<span class="o">}</span>
</code></pre></div> </div>
<p>Step 2: Copy the desired object version to make it the new latest version, replacing <code class="language-plaintext highlighter-rouge">${VERSION_ID}</code> as appropriate.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">BUCKET</span><span class="o">=</span>terraform-skeleton-state
<span class="nv">PREFIX</span><span class="o">=</span>app/dev/test-stack.tfstate
<span class="nv">VERSION</span><span class="o">=</span>TgfUNdVuoZKwSOHF1QeGO_nB8iZGzE3f
aws s3api copy-object <span class="se">\</span>
<span class="nt">--copy-source</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BUCKET</span><span class="k">}</span><span class="s2">/</span><span class="k">${</span><span class="nv">PREFIX</span><span class="k">}</span><span class="s2">?versionId=</span><span class="k">${</span><span class="nv">VERSION</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
<span class="nt">--key</span> <span class="k">${</span><span class="nv">PREFIX</span><span class="k">}</span> <span class="se">\</span>
<span class="nt">--bucket</span> <span class="k">${</span><span class="nv">BUCKET</span><span class="k">}</span>
<span class="o">{</span>
...
<span class="s2">"CopyObjectResult"</span>: <span class="o">{</span>
<span class="s2">"ETag"</span>: <span class="s2">"</span><span class="se">\"</span><span class="s2">...</span><span class="se">\"</span><span class="s2">"</span>,
<span class="s2">"LastModified"</span>: <span class="s2">"2021-02-20T13:36:47+00:00"</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div> </div>
<p><a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
</li>
</ol>
</div></content><author><name>Chris</name></author><category term="tf-skeleton" /><category term="terraform" /><category term="terragrunt" /><summary type="html">Part 5 moved Terraform’s operational infrastructure into a CloudFormation stack so that Terragrunt no longer manages the state bucket, lock table, and log bucket. Doing so offers us the opportunity to protect these resources in ways not supported by Terragrunt. We will capitalize on that opportunity today.</summary></entry><entry><title type="html">Terraform Skeleton Part 5: Remote State with CloudFormation</title><link href="https://thirstydeveloper.io/tf-skeleton/2021/02/17/part-5-cfn-terraform-state.html" rel="alternate" type="text/html" title="Terraform Skeleton Part 5: Remote State with CloudFormation" /><published>2021-02-17T12:20:00+00:00</published><updated>2021-02-17T12:20:00+00:00</updated><id>https://thirstydeveloper.io/tf-skeleton/2021/02/17/part-5-cfn-terraform-state</id><content type="html" xml:base="https://thirstydeveloper.io/tf-skeleton/2021/02/17/part-5-cfn-terraform-state.html"><p><a href="/tf-skeleton/2021/01/28/part-3-aws-backend.html">Part 3</a> showed us Terraform requires some infrastructure itself to store remote state and discussed some <a href="/tf-skeleton/2021/01/28/part-3-aws-backend.html#limitations">limitations</a> of using Terragrunt to manage the creation of that infrastructure. In <a href="/tf-skeleton/2021/02/10/part-4-backend-role.html">part 4</a>, we introduced more operational infrastructure for Terraform and <a href="/tf-skeleton/2021/02/10/part-4-backend-role.html#admin-account-cloudformation-template">began managing that infrastructure with CloudFormation</a>.</p>
<p>Today, we’ll continue down the path of using CloudFormation to store operational Terraform infrastructure. Achieving a clean separation between the infrastructure Terraform needs to run and the infrastructure Terraform manages opens up additional possibilities for locking down the Terraform state.</p>
<p>We’ll make use of <a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import.html">CloudFormation’s import feature</a> to bring the state bucket and lock table Terragrunt created for us under CloudFormation’s control for a seamless transition of ownership.</p>
<h1 id="goals">Goals</h1>
<ol>
<li>Control all Terraform operational infrastructure with CloudFormation</li>
<li>Import existing operational infrastructure (buckets, tables) into CloudFormation</li>
</ol>
<p>If you prefer to jump to the end, the code implementing this post’s final result is available on branch <a href="https://github.com/thirstydeveloper/terraform-terragrunt-skeleton/tree/release/1.4">release/1.4</a> on GitHub. Additionally, you can <a href="https://github.com/thirstydeveloper/terraform-terragrunt-skeleton/compare/release/1.3...release/1.4">view the diffs from part 4</a>, if that’s more your speed.</p>
<h1 id="define-operational-infrastructure-with-cloudformation">Define Operational Infrastructure with CloudFormation</h1>
<p>There are three items we need to import into CloudFormation:</p>
<ol>
<li>The state S3 bucket</li>
<li>The log S3 bucket</li>
<li>The DynamoDB lock table</li>
</ol>
<p>The first step is to define these resources in a CloudFormation template. We’ll add resource definitions to the <code class="language-plaintext highlighter-rouge">init-admin-account.cf.yml</code> template <a href="/tf-skeleton/2021/02/10/part-4-backend-role.html#admin-account-cloudformation-template">we created in part 4</a>.</p>
<p>If you prefer, you can <a href="https://github.com/thirstydeveloper/terraform-terragrunt-skeleton/compare/release/1.3...release/1.4#diff-cc93827adede14cb1e69dde0ed0769a47014bbb00f7ec444c61b927f3901c35a">view the diffs</a> on the CloudFormation template from part 4.</p>
<p>First up, the state bucket. Add an <a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html">AWS::S3::Bucket</a> resource to the CloudFormation template for our state bucket under the <code class="language-plaintext highlighter-rouge">Resources</code> block:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">TerraformStateBucket</span><span class="pi">:</span>
<span class="na">Type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">AWS::S3::Bucket'</span>
<span class="na">DeletionPolicy</span><span class="pi">:</span> <span class="s">Retain</span>
<span class="na">UpdateReplacePolicy</span><span class="pi">:</span> <span class="s">Retain</span>
<span class="na">Properties</span><span class="pi">:</span>
<span class="na">BucketName</span><span class="pi">:</span> <span class="kt">!Ref</span> <span class="s">StateBucketName</span>
<span class="na">BucketEncryption</span><span class="pi">:</span>
<span class="na">ServerSideEncryptionConfiguration</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">ServerSideEncryptionByDefault</span><span class="pi">:</span>
<span class="na">SSEAlgorithm</span><span class="pi">:</span> <span class="s">aws:kms</span>
<span class="na">LoggingConfiguration</span><span class="pi">:</span>
<span class="na">DestinationBucketName</span><span class="pi">:</span> <span class="kt">!Ref</span> <span class="s">StateLogBucketName</span>
<span class="na">LogFilePrefix</span><span class="pi">:</span> <span class="s">TFStateLogs/</span>
<span class="na">PublicAccessBlockConfiguration</span><span class="pi">:</span>
<span class="na">BlockPublicAcls</span><span class="pi">:</span> <span class="s">True</span>
<span class="na">BlockPublicPolicy</span><span class="pi">:</span> <span class="s">True</span>
<span class="na">IgnorePublicAcls</span><span class="pi">:</span> <span class="s">True</span>
<span class="na">RestrictPublicBuckets</span><span class="pi">:</span> <span class="s">True</span>
<span class="na">VersioningConfiguration</span><span class="pi">:</span>
<span class="na">Status</span><span class="pi">:</span> <span class="s">Enabled</span>
</code></pre></div></div>
<p>The properties above match what Terragrunt used <a href="/tf-skeleton/2021/01/28/part-3-aws-backend.html#backend-configuration-with-terragrunt">when it created the state bucket in part 3</a>. Note that we’re also using the <code class="language-plaintext highlighter-rouge">StateBucketName</code> and <code class="language-plaintext highlighter-rouge">StateLogBucketName</code> <a href="/tf-skeleton/2021/02/10/part-4-backend-role.html#admin-account-cloudformation-template">parameters we added in part 4</a>.</p>
<p>CloudFormation <a href="https://docs.amazonaws.cn/en_us/AWSCloudFormation/latest/UserGuide/resource-import-existing-stack.html">requires the DeletionPolicy attribute</a> for any resources it will import, and it is a good safety measure in general.</p>
<p><a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-updatereplacepolicy.html">UpdateReplacePolicy</a> is also set. Although not required, it is needed to make our <a href="/tf-skeleton/2021/02/10/part-4-backend-role.html#preparation">CloudFormation linter pre-commit hook</a> happy.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote">1</a></sup></p>
<p>Next, we’ll tackle the log bucket. Add the following to the <code class="language-plaintext highlighter-rouge">Resources</code> block:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">TerraformStateLogBucket</span><span class="pi">:</span>
<span class="na">Type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">AWS::S3::Bucket'</span>
<span class="na">DeletionPolicy</span><span class="pi">:</span> <span class="s">Retain</span>
<span class="na">UpdateReplacePolicy</span><span class="pi">:</span> <span class="s">Retain</span>
<span class="na">Properties</span><span class="pi">:</span>
<span class="na">BucketName</span><span class="pi">:</span> <span class="kt">!Ref</span> <span class="s">StateLogBucketName</span>
<span class="na">AccessControl</span><span class="pi">:</span> <span class="s">LogDeliveryWrite</span>
</code></pre></div></div>
<p>Finally, we have the lock table:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">TerraformStateLockTable</span><span class="pi">:</span>
<span class="na">Type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">AWS::DynamoDB::Table'</span>
<span class="na">DeletionPolicy</span><span class="pi">:</span> <span class="s">Retain</span>
<span class="na">UpdateReplacePolicy</span><span class="pi">:</span> <span class="s">Retain</span>
<span class="na">Properties</span><span class="pi">:</span>
<span class="na">TableName</span><span class="pi">:</span> <span class="kt">!Ref</span> <span class="s">LockTableName</span>
<span class="na">AttributeDefinitions</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">AttributeName</span><span class="pi">:</span> <span class="s">LockID</span>
<span class="na">AttributeType</span><span class="pi">:</span> <span class="s">S</span>
<span class="na">KeySchema</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">AttributeName</span><span class="pi">:</span> <span class="s">LockID</span>
<span class="na">KeyType</span><span class="pi">:</span> <span class="s">HASH</span>
<span class="na">BillingMode</span><span class="pi">:</span> <span class="s">PAY_PER_REQUEST</span>
</code></pre></div></div>
<p>If you’re starting fresh, and Terragrunt hasn’t already created the state bucket, log bucket, and lock table for you, you can skip the next section on importing and run the <code class="language-plaintext highlighter-rouge">make init-admin</code> target we created in <a href="/tf-skeleton/2021/01/28/part-3-aws-backend.html">part 3</a> to deploy your stack. Otherwise, continue on to import the resources Terragrunt created for you.</p>
<h1 id="import-operational-infrastructure-into-cloudformation">Import Operational Infrastructure into CloudFormation</h1>
<p>While you can use the AWS management console to import the resources, I prefer to work from the command line, but the commands to do so aren’t straightforward. We’ll add some targets to our Makefile in an attempt to simplify.</p>
<p>For a clean import, we need the following capabilities in our Makefile:</p>
<ol>
<li>Use <a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-drift.html">CloudFormation Drift Detection</a> to ensure our stack is up-to-date and our resource definitions match what Terragrunt created</li>
<li>Create a <a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html">CloudFormation change set</a> to import the resources and show us what CloudFormation intends to do</li>
<li>Execute the change set to import the resources</li>
</ol>
<p>If you prefer to jump to the end, <a href="https://github.com/thirstydeveloper/terraform-terragrunt-skeleton/blob/release/1.4/Makefile">here’s the resulting Makefile</a>, and here are <a href="https://github.com/thirstydeveloper/terraform-terragrunt-skeleton/compare/release/1.3...release/1.4#diff-76ed074a9305c04054cdebb9e9aad2d818052b07091de1f20cad0bbac34ffb52">the diffs from part 4</a>.</p>
<h2 id="check-for-drift">Check for Drift</h2>
<p>The first step in the import process is to verify that the stack we will be importing into has no drift, i.e., it has no other unapplied changes. The CloudFormation API uses separate calls to <a href="https://docs.aws.amazon.com/cli/latest/reference/cloudformation/detect-stack-drift.html">start a drift detection job</a> and <a href="https://docs.aws.amazon.com/cli/latest/reference/cloudformation/describe-stack-drift-detection-status.html">check the status of a drift detection job</a>. We’ll add some helper functions to our Makefile to wrap these calls:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>define wait_cfn_drift_detect_job
@while [[ \
"$$($(CFN_STATUS_DRIFT_DETECTION) $(1) | jq -r .DetectionStatus)" == \
"DETECTION_IN_PROGRESS" \
]]; do \
echo "Detection in progress. Waiting 3 seconds..."; \
sleep 3; \
done
endef
define show_cfn_drift
$(eval DRIFT_ID=$(shell $(CFN_START_DRIFT_DETECTION) $(1) \
| jq -r .StackDriftDetectionId))
$(call wait_cfn_drift_detect_job,${DRIFT_ID})
@$(CFN_STATUS_DRIFT_DETECTION) $(DRIFT_ID) | jq '{ \
DetectionStatus, \
StackDriftStatus, \
DriftedStackResourceCount \
}'
endef
</code></pre></div></div>
<p>Next, add a make target to perform the drift detection:</p>
<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">.PHONY</span><span class="o">:</span> <span class="nf">check-init-admin-drift</span>
<span class="nl">check-init-admin-drift</span><span class="o">:</span>
<span class="nf">$(</span><span class="nb">call</span> show_cfn_drift,<span class="nv">${ADMIN_INIT_STACK_NAME}</span><span class="nf">)</span>
</code></pre></div></div>
<p>Execute drift detection with:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>➜ make check-init-admin-drift
Detection <span class="k">in </span>progress. Waiting 3 seconds...
<span class="o">{</span>
<span class="s2">"DetectionStatus"</span>: <span class="s2">"DETECTION_COMPLETE"</span>,
<span class="s2">"StackDriftStatus"</span>: <span class="s2">"IN_SYNC"</span>,
<span class="s2">"DriftedStackResourceCount"</span>: 0
<span class="o">}</span>
</code></pre></div></div>
<p>If you get a <code class="language-plaintext highlighter-rouge">StackDriftStatus</code> other than <code class="language-plaintext highlighter-rouge">IN_SYNC</code>, adjust your CloudFormation template to resolve, and verify using the drift check. Once you’re <code class="language-plaintext highlighter-rouge">IN_SYNC</code>, create the import change set as follows.</p>
<h2 id="create-import-changeset">Create Import Changeset</h2>
<p>Creating the import change set requires a CloudFormation template and information about the resources to import, including:</p>
<ol>
<li>The resource type</li>
<li>The logical name of the resource in your template</li>
<li>The unique identifier for that resource in AWS</li>
</ol>
<p>For instance, our CloudFormation template defines the state bucket with:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">Resources</span><span class="pi">:</span>
<span class="na">TerraformStateLogBucket</span><span class="pi">:</span>
<span class="na">Type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">AWS::S3::Bucket'</span>
<span class="s">...</span>
</code></pre></div></div>
<p>which means the resource type is <code class="language-plaintext highlighter-rouge">AWS::S3::Bucket</code>, and the logical name is <code class="language-plaintext highlighter-rouge">TerraformStateLogBucket</code>.</p>
<p>The unique identifier depends on the resource type. For an S3 bucket, it’s the bucket name. For a DynamoDB table, it’s the table name.</p>
<p>Below is a make target for creating the import change set:</p>
<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">import-terragrunt-changeset.json</span><span class="o">:</span>
<span class="p">@</span>aws cloudformation create-change-set <span class="se">\</span>
<span class="nt">--stack-name</span> <span class="nv">${ADMIN_INIT_STACK_NAME}</span> <span class="se">\</span>
<span class="nt">--change-set-name</span> <span class="nv">${ADMIN_INIT_STACK_NAME}</span><span class="nt">-import-terragrunt</span> <span class="se">\</span>
<span class="nt">--change-set-type</span> IMPORT <span class="se">\</span>
<span class="nt">--template-body</span> file://init/admin/init-admin-account.cf.yml <span class="se">\</span>
<span class="nt">--capabilities</span> CAPABILITY_NAMED_IAM <span class="se">\</span>
<span class="nt">--parameters</span> <span class="se">\</span>
<span class="nv">ParameterKey</span><span class="o">=</span>AdminAccountId,UsePreviousValue<span class="o">=</span>True <span class="se">\</span>
<span class="nv">ParameterKey</span><span class="o">=</span>StateBucketName,UsePreviousValue<span class="o">=</span>True <span class="se">\</span>
<span class="nv">ParameterKey</span><span class="o">=</span>StateLogBucketName,UsePreviousValue<span class="o">=</span>True <span class="se">\</span>
<span class="nv">ParameterKey</span><span class="o">=</span>LockTableName,UsePreviousValue<span class="o">=</span>True <span class="se">\</span>
<span class="nt">--resources-to-import</span> <span class="s2">"[ </span><span class="se">\</span><span class="s2">
{ </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">ResourceType</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">AWS::S3::Bucket</span><span class="se">\"</span><span class="s2">, </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">LogicalResourceId</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">TerraformStateBucket</span><span class="se">\"</span><span class="s2">, </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">ResourceIdentifier</span><span class="se">\"</span><span class="s2">: { </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">BucketName</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="nv">${STATE_BUCKET_NAME}</span><span class="se">\"</span><span class="s2"> </span><span class="se">\</span><span class="s2">
} </span><span class="se">\</span><span class="s2">
}, </span><span class="se">\</span><span class="s2">
{ </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">ResourceType</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">AWS::S3::Bucket</span><span class="se">\"</span><span class="s2">, </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">LogicalResourceId</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">TerraformStateLogBucket</span><span class="se">\"</span><span class="s2">, </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">ResourceIdentifier</span><span class="se">\"</span><span class="s2">: { </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">BucketName</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="nv">${STATE_LOG_BUCKET_NAME}</span><span class="se">\"</span><span class="s2"> </span><span class="se">\</span><span class="s2">
} </span><span class="se">\</span><span class="s2">
}, </span><span class="se">\</span><span class="s2">
{ </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">ResourceType</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">AWS::DynamoDB::Table</span><span class="se">\"</span><span class="s2">, </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">LogicalResourceId</span><span class="se">\"</span><span class="s2">:</span><span class="se">\"</span><span class="s2">TerraformStateLockTable</span><span class="se">\"</span><span class="s2">, </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">ResourceIdentifier</span><span class="se">\"</span><span class="s2">: { </span><span class="se">\</span><span class="s2">
</span><span class="se">\"</span><span class="s2">TableName</span><span class="se">\"</span><span class="s2">: </span><span class="se">\"</span><span class="nv">${LOCK_TABLE_NAME}</span><span class="se">\"</span><span class="s2"> </span><span class="se">\</span><span class="s2">
} </span><span class="se">\</span><span class="s2">
} </span><span class="se">\</span><span class="s2">
]"</span> | <span class="nb">tee </span>import-terragrunt-changeset.json
</code></pre></div></div>
<p>CloudFormation assigns each change set a unique ID needed for describing, executing, or discarding the change set with subsequent API calls. The <code class="language-plaintext highlighter-rouge">import-terragrunt-changeset.json</code> stores the change set identifier in a JSON file for our other make targets to consume.</p>
<p>Next, we want a target for describing the created change set so we can see what modifications it will make:</p>
<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">.PHONY</span><span class="o">:</span> <span class="nf">prepare-cfn-import-terragrunt</span>
<span class="nl">prepare-cfn-import-terragrunt</span><span class="o">:</span> <span class="nf">import-terragrunt-changeset.json</span>
<span class="nf">$(</span><span class="nb">eval</span> <span class="nv">CHANGE_SET_ID</span><span class="o">=</span><span class="nf">$(</span><span class="nb">shell</span> jq <span class="nt">-r</span> .Id import-terragrunt-changeset.json<span class="nf">))</span>
aws cloudformation <span class="nb">wait </span>change-set-create-complete <span class="se">\</span>
<span class="nt">--change-set-name</span> <span class="nv">${CHANGE_SET_ID}</span> <span class="se">\</span>
<span class="nt">--stack-name</span> <span class="nv">${ADMIN_INIT_STACK_NAME}</span>
<span class="p">@</span>aws cloudformation describe-change-set <span class="se">\</span>
<span class="nt">--change-set-name</span> <span class="nv">${CHANGE_SET_ID}</span> <span class="se">\</span>
<span class="nt">--stack-name</span> <span class="nv">${ADMIN_INIT_STACK_NAME}</span> <span class="se">\</span>
| jq <span class="s1">'{ Changes, Status, StatusReason }'</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">prepare-cfn-import-terragrunt</code> target depends on <code class="language-plaintext highlighter-rouge">import-terragrunt-changeset.json</code> to create the change set and communicate its unique id, then describes it once the change set finishes creating.</p>
<p>At this point, we’ll add another target for discarding the change set, in case we don’t like what we see with <code class="language-plaintext highlighter-rouge">prepare-cfn-import-terragrunt</code>:</p>
<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">.PHONY</span><span class="o">:</span> <span class="nf">discard-cfn-import-terragrunt</span>
<span class="nl">discard-cfn-import-terragrunt</span><span class="o">:</span> <span class="nf">import-terragrunt-changeset.json</span>
<span class="nf">$(</span><span class="nb">eval</span> <span class="nv">CHANGE_SET_ID</span><span class="o">=</span><span class="nf">$(</span><span class="nb">shell</span> jq <span class="nt">-r</span> .Id import-terragrunt-changeset.json<span class="nf">))</span>
aws cloudformation delete-change-set <span class="se">\</span>
<span class="nt">--change-set-name</span> <span class="nv">${CHANGE_SET_ID}</span> <span class="se">\</span>
<span class="nt">--stack-name</span> <span class="nv">${ADMIN_INIT_STACK_NAME}</span>
<span class="p">@</span><span class="nb">rm </span>import-terragrunt-changeset.json
</code></pre></div></div>
<p>Let’s add a conventional <code class="language-plaintext highlighter-rouge">clean</code> target too:</p>
<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">.PHONY</span><span class="o">:</span> <span class="nf">clean</span>
<span class="nl">clean</span><span class="o">:</span>
<span class="nb">rm </span>import-terragrunt-changeset.json
</code></pre></div></div>
<p>Now, let’s use our targets to create and describe the change set:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>➜ make prepare-cfn-import-terragrunt
<span class="o">{</span>
<span class="s2">"Id"</span>: <span class="s2">"arn:aws:cloudformation:us-east-1:&lt;omitted&gt;:changeSet/tf-admin-init-import-terragrunt/179b1efd-2961-48af-95c2-6f45b96a9925"</span>,
<span class="s2">"StackId"</span>: <span class="s2">"arn:aws:cloudformation:us-east-1:&lt;omitted&gt;:stack/tf-admin-init/8704b070-5f61-11eb-9ff1-0eea077046db"</span>
<span class="o">}</span>
aws cloudformation <span class="nb">wait </span>change-set-create-complete <span class="se">\</span>
<span class="nt">--change-set-name</span> arn:aws:cloudformation:us-east-1:&lt;omitted&gt;:changeSet/tf-admin-init-import-terragrunt/179b1efd-2961-48af-95c2-6f45b96a9925 <span class="se">\</span>
<span class="nt">--stack-name</span> tf-admin-init
<span class="o">{</span>
<span class="s2">"Changes"</span>: <span class="o">[</span>
<span class="o">{</span>
<span class="s2">"Type"</span>: <span class="s2">"Resource"</span>,
<span class="s2">"ResourceChange"</span>: <span class="o">{</span>
<span class="s2">"Action"</span>: <span class="s2">"Import"</span>,
<span class="s2">"LogicalResourceId"</span>: <span class="s2">"TerraformStateBucket"</span>,
<span class="s2">"PhysicalResourceId"</span>: <span class="s2">"terraform-skeleton-state"</span>,
<span class="s2">"ResourceType"</span>: <span class="s2">"AWS::S3::Bucket"</span>,
<span class="s2">"Scope"</span>: <span class="o">[]</span>,
<span class="s2">"Details"</span>: <span class="o">[]</span>
<span class="o">}</span>
<span class="o">}</span>,
<span class="o">{</span>
<span class="s2">"Type"</span>: <span class="s2">"Resource"</span>,
<span class="s2">"ResourceChange"</span>: <span class="o">{</span>
<span class="s2">"Action"</span>: <span class="s2">"Import"</span>,
<span class="s2">"LogicalResourceId"</span>: <span class="s2">"TerraformStateLockTable"</span>,
<span class="s2">"PhysicalResourceId"</span>: <span class="s2">"terraform-skeleton-state-locks"</span>,
<span class="s2">"ResourceType"</span>: <span class="s2">"AWS::DynamoDB::Table"</span>,
<span class="s2">"Scope"</span>: <span class="o">[]</span>,
<span class="s2">"Details"</span>: <span class="o">[]</span>
<span class="o">}</span>
<span class="o">}</span>,
<span class="o">{</span>
<span class="s2">"Type"</span>: <span class="s2">"Resource"</span>,
<span class="s2">"ResourceChange"</span>: <span class="o">{</span>
<span class="s2">"Action"</span>: <span class="s2">"Import"</span>,
<span class="s2">"LogicalResourceId"</span>: <span class="s2">"TerraformStateLogBucket"</span>,
<span class="s2">"PhysicalResourceId"</span>: <span class="s2">"terraform-skeleton-state-logs"</span>,
<span class="s2">"ResourceType"</span>: <span class="s2">"AWS::S3::Bucket"</span>,
<span class="s2">"Scope"</span>: <span class="o">[]</span>,
<span class="s2">"Details"</span>: <span class="o">[]</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">]</span>,
<span class="s2">"Status"</span>: <span class="s2">"CREATE_COMPLETE"</span>,
<span class="s2">"StatusReason"</span>: null
<span class="o">}</span>
</code></pre></div></div>
<p>Our describe command should show three <code class="language-plaintext highlighter-rouge">Import</code> actions occurring in the change list with no other changes. If you see any other actions, it means your template contains unapplied changes, and you’ll want to address those first.</p>
<h2 id="execute-import">Execute Import</h2>
<p>Having created the import change set and verified the actions it staged, we’ll now add a target for executing it:</p>
<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">.PHONY</span><span class="o">:</span> <span class="nf">cfn-import-terragrunt</span>
<span class="nl">cfn-import-terragrunt</span><span class="o">:</span> <span class="nf">import-terragrunt-changeset.json</span>
<span class="nf">$(</span><span class="nb">eval</span> <span class="nv">CHANGE_SET_ID</span><span class="o">=</span><span class="nf">$(</span><span class="nb">shell</span> jq <span class="nt">-r</span> .Id import-terragrunt-changeset.json<span class="nf">))</span>
aws cloudformation <span class="nb">wait </span>change-set-create-complete <span class="se">\</span>
<span class="nt">--change-set-name</span> <span class="nv">${CHANGE_SET_ID}</span> <span class="se">\</span>
<span class="nt">--stack-name</span> <span class="nv">${ADMIN_INIT_STACK_NAME}</span>
aws cloudformation execute-change-set <span class="se">\</span>
<span class="nt">--change-set-name</span> <span class="nv">${CHANGE_SET_ID}</span> <span class="se">\</span>
<span class="nt">--stack-name</span> <span class="nv">${ADMIN_INIT_STACK_NAME}</span>
<span class="p">@</span><span class="nb">rm </span>import-terragrunt-changeset.json
aws cloudformation <span class="nb">wait </span>stack-import-complete <span class="se">\</span>
<span class="nt">--stack-name</span> <span class="nv">${ADMIN_INIT_STACK_NAME}</span>
<span class="nf">$(</span><span class="nb">call</span> show_cfn_drift,<span class="nv">${ADMIN_INIT_STACK_NAME}</span><span class="nf">)</span>
</code></pre></div></div>
<p>Execute the import with:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>➜ make cfn-import-terragrunt
aws cloudformation <span class="nb">wait </span>change-set-create-complete <span class="se">\</span>
<span class="nt">--change-set-name</span> arn:aws:cloudformation:us-east-1:&lt;omitted&gt;:changeSet/tf-admin-init-import-terragrunt/179b1efd-2961-48af-95c2-6f45b96a9925 <span class="se">\</span>
<span class="nt">--stack-name</span> tf-admin-init
aws cloudformation execute-change-set <span class="se">\</span>
<span class="nt">--change-set-name</span> arn:aws:cloudformation:us-east-1:&lt;omitted&gt;:changeSet/tf-admin-init-import-terragrunt/179b1efd-2961-48af-95c2-6f45b96a9925 <span class="se">\</span>
<span class="nt">--stack-name</span> tf-admin-init
aws cloudformation <span class="nb">wait </span>stack-import-complete <span class="se">\</span>
<span class="nt">--stack-name</span> tf-admin-init
<span class="o">{</span>
<span class="s2">"DetectionStatus"</span>: <span class="s2">"DETECTION_COMPLETE"</span>,
<span class="s2">"StackDriftStatus"</span>: <span class="s2">"IN_SYNC"</span>,
<span class="s2">"DriftedStackResourceCount"</span>: 0
<span class="o">}</span>
</code></pre></div></div>
<p>At the end of the <code class="language-plaintext highlighter-rouge">cfn-import-terragrunt</code> target, we call our <code class="language-plaintext highlighter-rouge">show_cfn_drift</code> helper function to verify that the properties of the resources we imported match what we specified in our template. You should see <code class="language-plaintext highlighter-rouge">StackDriftStatus</code> is <code class="language-plaintext highlighter-rouge">IN_SYNC</code>, which means we’ve successfully imported the resources and the definitions for those resources in our template match reality.<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote">2</a></sup></p>
<h1 id="whats-next">What’s Next</h1>
<p>We’ve now fully separated the creation of operational infrastructure required to run Terraform from the infrastructure Terraform manages. Creating the operational infrastructure with CloudFormation has numerous benefits. We can now harden our state bucket, log bucket, and lock table in ways that were not available to us with Terragrunt managing their creation. We’ll tackle such hardening in upcoming posts.</p>
<h1 id="footnotes">Footnotes</h1>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>If you omit <code class="language-plaintext highlighter-rouge">UpdateReplacePolicy</code>, the linter will report something like:</p>
<blockquote>
<p>W3011 Both UpdateReplacePolicy and DeletionPolicy are needed to protect Resources/TerraformStateBucket from deletion</p>
</blockquote>
<p><a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
</li>
<li id="fn:2" role="doc-endnote">
<p>If you see that there is drift after executing the import change set, the resources you imported have a different configuration than what you specified in your template. For instance, maybe your template specified a bucket is KMS encrypted when in reality, the bucket is AES-256 encrypted. Drift detection will report such differences. To resolve the drift:</p>
<ol>
<li>Decide whether the drift is appropriate and should be retained</li>
<li>Modify the template to match any appropriate drift</li>
<li>Create a change set on the stack with the updated template</li>
<li>Verify the change set reports it will discard the inappropriate drift (or have no changes if there was no inappropriate drift)</li>
<li>Execute the change set</li>
</ol>
<p><a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
</li>
</ol>
</div></content><author><name>Chris</name></author><category term="tf-skeleton" /><category term="terraform" /><category term="terragrunt" /><summary type="html">Part 3 showed us Terraform requires some infrastructure itself to store remote state and discussed some limitations of using Terragrunt to manage the creation of that infrastructure. In part 4, we introduced more operational infrastructure for Terraform and began managing that infrastructure with CloudFormation.</summary></entry><entry><title type="html">Terraform Skeleton Part 4: Backend Role</title><link href="https://thirstydeveloper.io/tf-skeleton/2021/02/10/part-4-backend-role.html" rel="alternate" type="text/html" title="Terraform Skeleton Part 4: Backend Role" /><published>2021-02-10T22:30:00+00:00</published><updated>2021-02-10T22:30:00+00:00</updated><id>https://thirstydeveloper.io/tf-skeleton/2021/02/10/part-4-backend-role</id><content type="html" xml:base="https://thirstydeveloper.io/tf-skeleton/2021/02/10/part-4-backend-role.html"><p>The <a href="/tf-skeleton/2021/01/28/part-3-aws-backend.html">previous entry</a> enhanced the <a href="/series/tf-skeleton.html">terraform skeleton</a> with remote state storage using AWS S3 and DynamoDB. Access to the state was granted based on whatever AWS credentials were configured in the shell at the time terraform was executed.</p>
<p>Terraform state is a highly sensitive resource. It is likely to contain lots of sensitive information including passwords and access tokens. Additionally, recovering from a lost state file means either recreating all the infrastructure that was in it, or spending some quality time running <a href="https://www.terraform.io/docs/cli/import/index.html">terraform import</a> commands for resources that support it, and hand modifying state files for those that do not.</p>
<p>On projects I’ve supported, developers have had near-administrative level permissions on their cloud accounts. Typically, these are the credentials they will use when running terraform, which also requires near-administrative level permissions to manage the wide variety of cloud resources a project is likely to need. Unless instructed otherwise, terraform will use those administrative credentials for accessing the state, and that opens up the potential for disaster (e.g., a developer mistakenly deleting a state bucket).</p>
<p><img src="/assets/tf-skeleton/part-4/admin-state-access.png" alt="Accessing terraform state with administrative IAM user credentials" /></p>
<p>In this post, we’ll create a dedicated IAM role for backend access so that every developer on our team working with terraform will access state with the same permissions, and those permissions are scoped to just what is needed to read and write state.</p>
<p><img src="/assets/tf-skeleton/part-4/backend-role-state-access.png" alt="Accessing terraform state with least-privilege IAM role credentials" /></p>
<p>Using a dedicated backend role for state access will help avoid “works on my machine problems” and allow us, in future entries, to lock down access to terraform state due to its sensitive nature.</p>
<h1 id="goals">Goals</h1>
<ol>
<li>The skeleton uses a dedicated IAM role for accessing terraform state such that everyone on the team accesses state with the same least-privilege permissions</li>
<li>The IAM role and its permissions are controlled using infrastructure-as-code</li>
</ol>
<p>If you prefer to jump to the end, the code implementing the final result is available on <a href="https://github.com/thirstydeveloper/terraform-terragrunt-skeleton/tree/release/1.3">GitHub</a>.</p>
<h1 id="setup">Setup</h1>
<p>You will need:</p>
<ol>
<li>An AWS account to serve as your terraform “admin” account, holding the state resources</li>
<li>The <a href="https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html">AWS CLI</a> installed on your workstation</li>
<li>Credentials for that account configured in the terminal for running <code class="language-plaintext highlighter-rouge">aws</code> CLI commands</li>
</ol>
<h1 id="the-administrative-account">The Administrative Account</h1>
<p>Before we create our backend IAM role, it is worth discussing cloud account organization. Chances are strong that you’re going to have more than one account.<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote">1</a></sup> Typically, one per application environment is recommended:</p>
<p><img src="/assets/tf-skeleton/part-4/unmanaged-app-accounts.png" alt="Separate AWS accounts for dev, test, and production app-tier environments" /></p>
<p>With multiple accounts, the question of where we put our terraform state arises. We could put the state for each tier-environment in its corresponding account (e.g., app-dev state in the app-dev account) but I don’t like doing that for several reasons:</p>
<ol>
<li>It assumes our tiers line up one-to-one with accounts, which may not always be true</li>
<li>I prefer to restrict administrative access to terraform state to a subset of those who have access to environment accounts.</li>
</ol>
<p>Instead, I prefer to use a separate admin account to hold all the state, an approach recommended in <a href="https://www.terraform.io/docs/backends/types/s3.html#multi-account-aws-architecture">the documentation for the S3 backend</a>:</p>
<p><img src="/assets/tf-skeleton/part-4/admin-managed-app-accounts.png" alt="Admin AWS account managing app accounts" /></p>
<p>With this approach, our admin account contains:</p>
<ol>
<li>S3 bucket(s) for terraform state</li>
<li>S3 bucket(s) for state logs<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote">2</a></sup></li>
<li>DynamoDB table(s) for terraform locks</li>
<li>IAM roles for accessing the above</li>
</ol>
<p>The first three were created in the <a href="/tf-skeleton/2021/01/28/part-3-aws-backend.html">previous entry</a>. Now, we’ll create our backend role.</p>
<h1 id="the-backend-role">The Backend Role</h1>
<p>We have a bit of a chicken and egg problem with the backend role. We want to control all infrastructure as code, the backend role included, but we can’t use the backend role for terraform until it is created. A similar problem presents itself if you want to self-manage the creation of your S3 state bucket and DynamoDB lock table.</p>
<p>While it’s possible to use terraform to create these resources by having a stack that either stores state locally initially and/or uses user credentials instead of the backend role, I tend not to do that. I prefer to separate the infrastructure needed for terraform to run from the infrastructure terraform creates, and manage the former using <a href="https://aws.amazon.com/cloudformation/">CloudFormation</a>. A simple CloudFormation stack is more than capable of managing the few resources needed to bootstrap an AWS account to serve as our admin account and means we offload the state management of that bootstrap infrastructure to AWS’s CloudFormation tool.</p>
<h2 id="preparation">Preparation</h2>
<p>Before adding CloudFormation templates to our infra repository, I like to add a <a href="https://pre-commit.com/">pre-commit</a> hook that validates those templates. It’s easy to make syntax errors with CloudFormation; using a validation hook helps to shorten the feedback loop. I do the following:</p>
<ol>
<li>Store bootstrap CloudFormation templates under <code class="language-plaintext highlighter-rouge">init/</code> in the infra repo</li>
<li>Use a file extension of <code class="language-plaintext highlighter-rouge">.cf.yml</code> to differentiate CloudFormation templates from regular YAML files</li>
<li>Use the <a href="https://github.com/aws-cloudformation/cfn-python-lint">cfn-python-lint</a> pre-commit hook to validate the CloudFormation templates</li>
</ol>
<p>Let’s add the hook to our <code class="language-plaintext highlighter-rouge">.pre-commit-config.yaml</code>:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fa080cf..27217e9 100644
</span><span class="gd">--- a/.pre-commit-config.yaml
</span><span class="gi">+++ b/.pre-commit-config.yaml
</span><span class="p">@@ -24,3 +25,9 @@</span> repos:
rev: v0.1.10
hooks:
- id: terragrunt-hclfmt
<span class="gi">+
+- repo: https://github.com/aws-cloudformation/cfn-python-lint
+ rev: v0.44.5
+ hooks:
+ - id: cfn-python-lint
+ files: init/.*\.cf\.(yml|yaml)$
</span></code></pre></div></div>
<p>This configures the hook to scan all templates under the <code class="language-plaintext highlighter-rouge">init/</code> directory. We also need to tell our existing <code class="language-plaintext highlighter-rouge">check-yaml</code> hook to ignore the CloudFormation templates because it will throw errors on CloudFormation’s interpolation syntax. We can do that with:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index fa080cf..27217e9 100644
</span><span class="gd">--- a/.pre-commit-config.yaml
</span><span class="gi">+++ b/.pre-commit-config.yaml
</span><span class="p">@@ -8,6 +8,7 @@</span> repos:
args: [ --markdown-linebreak-ext=* ]
- id: check-yaml
args: [ --allow-multiple-documents ]
<span class="gi">+ exclude: .*\.cf\.(yml|yaml)$
</span> - id: check-json
- id: check-merge-conflict
- id: detect-aws-credentials
</code></pre></div></div>
<p>Finally, run <code class="language-plaintext highlighter-rouge">pre-commit install</code> and <code class="language-plaintext highlighter-rouge">pre-commit run -a</code> to make sure all hooks are currently passing.</p>
<h2 id="admin-account-cloudformation-template">Admin Account CloudFormation Template</h2>
<p>Create a new CloudFormation template at <code class="language-plaintext highlighter-rouge">init/admin/init-admin-account.cf.yml</code>. You’ll need <a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html">parameters</a> for the admin account ID, state bucket, log bucket, and lock table names. I like to use <a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/metadata-section-structure.html">CloudFormation’s metadata property</a> to group the parameters sensibly, should you use the AWS Management Console to deploy the stack:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">AWSTemplateFormatVersion</span><span class="pi">:</span> <span class="s1">'</span><span class="s">2010-09-09'</span>
<span class="na">Description</span><span class="pi">:</span> <span class="s">Initialize terraform admin account</span>
<span class="na">Metadata</span><span class="pi">:</span>
<span class="s">AWS::CloudFormation::Interface:</span>
<span class="s">ParameterGroups</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">Label</span><span class="pi">:</span>
<span class="na">default</span><span class="pi">:</span> <span class="s">Admin Account Config</span>
<span class="na">Parameters</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">AdminAccountId</span>
<span class="pi">-</span> <span class="na">Label</span><span class="pi">:</span>
<span class="na">default</span><span class="pi">:</span> <span class="s">Terraform State Resources</span>
<span class="na">Parameters</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">StateBucketName</span>
<span class="pi">-</span> <span class="s">StateLogBucketName</span>
<span class="pi">-</span> <span class="s">LockTableName</span>
<span class="na">Parameters</span><span class="pi">:</span>
<span class="na">AdminAccountId</span><span class="pi">:</span>
<span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
<span class="na">Description</span><span class="pi">:</span> <span class="s">Account ID of the admin account to contain the state</span>
<span class="na">StateBucketName</span><span class="pi">:</span>
<span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
<span class="na">Description</span><span class="pi">:</span> <span class="s">Name of the S3 bucket for terraform state</span>
<span class="na">StateLogBucketName</span><span class="pi">:</span>
<span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>
<span class="na">Description</span><span class="pi">:</span> <span class="s">Name of the S3 bucket for terraform state logs</span>
<span class="na">LockTableName</span><span class="pi">:</span>
<span class="na">Type</span><span class="pi">:</span> <span class="s">String</span>