<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: ericksoen</title>
    <description>The latest articles on Forem by ericksoen (@ericksoen).</description>
    <link>https://forem.com/ericksoen</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F337479%2F4daa4103-0010-48d6-983a-61c92da986e5.jpeg</url>
      <title>Forem: ericksoen</title>
      <link>https://forem.com/ericksoen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ericksoen"/>
    <language>en</language>
    <item>
      <title>Teaching Terraform from the ground up: Benchmarking S3 force_destroy</title>
      <dc:creator>ericksoen</dc:creator>
      <pubDate>Wed, 17 Feb 2021 14:40:03 +0000</pubDate>
      <link>https://forem.com/ericksoen/teaching-terraform-from-the-ground-up-benchmarking-s3-forcedestroy-2nlf</link>
      <guid>https://forem.com/ericksoen/teaching-terraform-from-the-ground-up-benchmarking-s3-forcedestroy-2nlf</guid>
      <description>&lt;p&gt;A few months back, a colleague reached out to me to troubleshoot a Terraform destroy operation that had taken several hours to execute. They had already terminated and restarted the operation multiple times with no change in outcome.&lt;/p&gt;

&lt;p&gt;By the time they reached out, the only remaining resource was an &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket"&gt;S3 bucket&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In my earlier post &lt;a href="https://dev.to/ericksoen/teaching-terraform-from-the-ground-up-55nm"&gt;Teaching Terraform from the Ground Up&lt;/a&gt;, I described how Terraform abstracts multiple AWS API calls into a single resource definition. Debugging this abstraction when it fails to operate as we expect is sometimes challenging and frequently requires us to dig into the provider source code to understand root causes.&lt;/p&gt;

&lt;p&gt;This is exactly the issue my colleague experienced when they enabled the &lt;code&gt;force_destroy&lt;/code&gt; property on their S3 bucket resource and then attempted to delete it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging &lt;code&gt;force_destroy&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket#force_destroy"&gt;Terraform resource documentation&lt;/a&gt; for the &lt;code&gt;force_destroy&lt;/code&gt; property states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A boolean that indicates all objects (including any locked objects) should be deleted from the bucket so that the bucket can be destroyed without error. These objects are not recoverable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When I debug Terraform abstractions, I frequently consult AWS API documentation to determine if a specified behavior also exists in the core AWS API. If it does, e.g., &lt;code&gt;aws s3api delete-bucket --force-destroy&lt;/code&gt;, we should be able to replicate our issue &lt;em&gt;outside&lt;/em&gt; of Terraform and dramatically reduce the surface area of our issue. In this case the &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucket.html"&gt;S3 DeleteBucket&lt;/a&gt; documentation states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All objects (including all object versions and delete markers) in the bucket &lt;em&gt;must be deleted&lt;/em&gt; before the bucket itself can be deleted (emphasis mine)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The documentation for the &lt;a href="https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3api/delete-bucket.html"&gt;AWS CLI&lt;/a&gt; and &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Bucket.delete"&gt;Boto3 S3&lt;/a&gt; also confirm his behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source Code Review
&lt;/h2&gt;

&lt;p&gt;Since &lt;code&gt;force_destroy&lt;/code&gt; exists in the Terraform resource &lt;strong&gt;&lt;em&gt;and not&lt;/em&gt;&lt;/strong&gt; in the underlying AWS API, a viable theory is that Terraform implements an abstraction around this property.&lt;/p&gt;

&lt;p&gt;Let's dive into the &lt;a href="https://github.com/hashicorp/terraform-provider-aws/blob/master/aws/resource_aws_s3_bucket.go#L1348-#L1371"&gt;AWS S3 Bucket resource source code&lt;/a&gt; to see what API calls are made when that property is set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isAWSErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"BucketNotEmpty"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"force_destroy"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
        &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;deleteAllS3ObjectVersions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s3Conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;objectLockEnabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The conditional statements look boilerplate, but the &lt;code&gt;deleteAllS3ObjectVersions&lt;/code&gt; method call is interesting (in this case &lt;code&gt;d.Id()&lt;/code&gt; returns the name of the bucket), so let's pull on that thread some more.&lt;/p&gt;

&lt;p&gt;This &lt;a href="https://github.com/hashicorp/terraform-provider-aws/blob/bc48c2178fff1c5b1b142481069e45fcb5870bd5/aws/resource_aws_s3_bucket_object.go#L537-#L602"&gt;source code&lt;/a&gt; is more verbose, so we'll again simplify it to the relevant details in our pseudo-code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListObjectVersionsInput&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucketName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LisObjectVersionsPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListObjectVersionsOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lastPage&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;lastPage&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;objectVersion&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Versions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="n"&gt;objectKey&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objectVersion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;objectVersionID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objectVersion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VersionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;deleteS3ObjectVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucketName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;objectKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;objectVersionID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;force&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;lastPage&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A plain-English explanation of this operation might contain the following instructions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List all the object versions associated with the S3 bucket&lt;/li&gt;
&lt;li&gt;Paginate through the response object versions&lt;/li&gt;
&lt;li&gt;Delete each object version &lt;strong&gt;one-by-one&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That can be quite a few &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html"&gt;S3 DeleteObject&lt;/a&gt; API calls depending on the number of objects in your bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform &lt;code&gt;force_destroy&lt;/code&gt; performance benchmarks
&lt;/h2&gt;

&lt;p&gt;Now that we've seen that the number of AWS API calls scales linearly based on the number of objects in our S3 bucket, we can quantify some performance benchmarks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;# of Objects&lt;/th&gt;
&lt;th&gt;# of API Calls (est.)&lt;/th&gt;
&lt;th&gt;Delete Seconds (min)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;11.3 (.18)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;36.3 (.60)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;282.3 (4.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;1010&lt;/td&gt;
&lt;td&gt;2323.8 (38.73)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To summarize those findings, it takes almost &lt;strong&gt;five minutes&lt;/strong&gt; to delete 1000 objects and at 10,000 objects, it takes almost &lt;strong&gt;forty minutes&lt;/strong&gt;. As my peer discovered, this can be a prohibitively expensive operation for buckets without any object lifecycle management.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's a developer to do?!?
&lt;/h2&gt;

&lt;p&gt;Deleting large volumes of objects using the &lt;code&gt;force_destroy&lt;/code&gt; parameter is likely a non-starter, so what other options are available?&lt;/p&gt;

&lt;p&gt;If we want a Terraform-only solution, we can add an object lifecycle management rule to expire all bucket objects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;lifecycle_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;expiration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Management rules are evaluated and executed around 12 AM UTC each day so this may introduce a temporal delay in deleting your S3 bucket. If time is essential, the AWS API provides a bulk &lt;a href="https://awscli.amazonaws.com/v2/documentation/api/latest/reference/s3api/delete-objects.html"&gt;delete-objects&lt;/a&gt; that can operate on up to 1000 keys per execution.&lt;/p&gt;

&lt;p&gt;A sample implementation using the AWS CLI and &lt;code&gt;jq&lt;/code&gt; is demonstrated below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;bucket_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_BUCKET_NAME_GOES_HERE
aws s3api list-object-versions &lt;span class="nt"&gt;--max-items&lt;/span&gt; 1000 &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="nv"&gt;$bucket_name&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; object_versions.json
&lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;object_versions.json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .NextToken&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do

    &lt;/span&gt;jq &lt;span class="nt"&gt;--compact-output&lt;/span&gt; &lt;span class="s1"&gt;'{ "Objects": [ { "Key": .Versions[].Key } ] }'&lt;/span&gt; object_versions.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; delete.json
    aws s3api delete-objects &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="nv"&gt;$bucket_name&lt;/span&gt; &lt;span class="nt"&gt;--delete&lt;/span&gt; file://delete.json
    aws s3api list-object-versions &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="nv"&gt;$bucket_name&lt;/span&gt; &lt;span class="nt"&gt;--max-items&lt;/span&gt; 1000 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; object_versions.json
    &lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;object_versions.json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .NextToken&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This implementation performs the delete operation for 10,000 objects in less than 20 seconds.&lt;/p&gt;

&lt;p&gt;If no automation is required, you can also use the &lt;strong&gt;Empty Bucket&lt;/strong&gt; option from the AWS Console. I always get nervous when I need to execute manual operations in the AWS Console. If you can avoid this option, I would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap Up
&lt;/h2&gt;

&lt;p&gt;A recent tweet by Angie Jones reminded me of why I started blogging:&lt;/p&gt;


&lt;blockquote class="twitter-tweet"&gt;
&lt;p&gt;I ran into a basic configuration issue and couldn't find a solution online.&lt;br&gt;&lt;br&gt;After I figured it out, I wrote a simple post with the exact error message and the solution.&lt;br&gt;&lt;br&gt;~100K views.&lt;br&gt;&lt;br&gt;Everything doesn't have to be a think piece. Don't be afraid to share 🙏🏾&lt;/p&gt;— Angie Jones (&lt;a class="comment-mentioned-user" href="https://dev.to/techgirl1908"&gt;@techgirl1908&lt;/a&gt;
) &lt;a href="https://twitter.com/techgirl1908/status/1361026673290797060?ref_src=twsrc%5Etfw"&gt;February 14, 2021&lt;/a&gt;
&lt;/blockquote&gt; 

&lt;p&gt;It was likely a similar tweet by her that first inspired me several years back. To paraphrase, &lt;em&gt;document what you learn along the way to make it easier for developers who come after you&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And if the issue is obscure enough &lt;em&gt;and&lt;/em&gt; you're the type who can't remember what you did yesterday much less several months ago, that developer may end up being you ❤️.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Teaching Terraform from the ground up...</title>
      <dc:creator>ericksoen</dc:creator>
      <pubDate>Wed, 26 Feb 2020 18:19:38 +0000</pubDate>
      <link>https://forem.com/ericksoen/teaching-terraform-from-the-ground-up-55nm</link>
      <guid>https://forem.com/ericksoen/teaching-terraform-from-the-ground-up-55nm</guid>
      <description>&lt;h1&gt;
  
  
  Overview
&lt;/h1&gt;

&lt;p&gt;I'm going to write this &lt;strong&gt;...getting started with Terraform&lt;/strong&gt; guide the way I wished I had learned Terraform. A lot of the tutorials I first used emphasized the &lt;em&gt;declarative definitions for the infrastructure we want to exist&lt;/em&gt; and &lt;em&gt;Terraform will figure out how to create it&lt;/em&gt; (that's an exact quote). When you understand the abstraction that Terraform manages, those are really powerful tools. But when you're first getting started, opaque AND abstract can be a significant barrier to entry—at least it was for me.&lt;/p&gt;

&lt;p&gt;For the duration of this tutorial, we'll go through multiple iterations to create an AWS S3 bucket in the &lt;code&gt;us-east-1&lt;/code&gt; region that complies with business requirements regarding versioning (enabled) and cost-allocation (via tags). We'll also deploy all our infrastructure using the &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege" rel="noopener noreferrer"&gt;Grant Least Privilege model&lt;/a&gt; endorsed by AWS since this will help elucidate some of the interesting ways creating declarative infrastructure intersects with AWS permissions.&lt;/p&gt;

&lt;p&gt;By the end of this demo, we'll have created this infrastructure twice using both the AWS CLI and Terraform. The AWS CLI will help ground the abstraction in more straightforward and obvious API calls. In a subsequent step, we use Terraform to create the same infrastructure. If you're a more visual learner, you might find that the AWS Console is an easier tool to manage than the AWS CLI, which is A-OK in my book—just make sure your Console user and Terraform user have the same IAM access levels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment setup
&lt;/h2&gt;

&lt;p&gt;This tutorial assumes some familiarity with AWS and that users already have an account configured if they want to follow along. If you're new to AWS, the &lt;a href="https://blog.gruntwork.io/an-introduction-to-terraform-f17df9c6d180#a9b0" rel="noopener noreferrer"&gt;introduction to Terraform guide&lt;/a&gt; published by Gruntworks has a helpful guide in creating a new account on the AWS free tier.&lt;/p&gt;

&lt;p&gt;If you haven't installed the &lt;a href="https://aws.amazon.com/cli/" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt; or &lt;a href="https://www.terraform.io/downloads.html" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; previously, go ahead and download them as appropriate.&lt;/p&gt;

&lt;p&gt;On my workstation, I'm running the following versions of the each tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;aws &lt;span class="nt"&gt;--version&lt;/span&gt;
aws-cli/1.16.106 Python/3.6.0 Windows/10 botocore/1.12.96

&lt;span class="nv"&gt;$ &lt;/span&gt;terraform &lt;span class="nt"&gt;--version&lt;/span&gt;
Terraform v0.12.20
+ provider.aws v2.49.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The S3 API used by both tools is well-established and mature so you hopefully won't encounter any issues related to application version. If you do, make sure sure to rule out a version inconsistency as an underlying cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Permission Setup
&lt;/h2&gt;

&lt;p&gt;We'll simplify user creation by performing it exclusively using the the AWS CLI. You can download the IAM user policy that we'll user as our starter set of permissions from a &lt;a href="https://gist.github.com/ericksoen/110e72114e391f6c1c55cc0515430e13" rel="noopener noreferrer"&gt;Github snippet&lt;/a&gt;. If you're curious what the same policy would like like in Terraform, I've included a second &lt;a href="https://gist.github.com/ericksoen/0bbd6e715ff14f0be05fed8b45e13ce1" rel="noopener noreferrer"&gt;Github snippet&lt;/a&gt; with the starter policy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-user &lt;span class="nt"&gt;--user-name&lt;/span&gt; terraform-user

aws iam put-user-policy &lt;span class="nt"&gt;--user-name&lt;/span&gt; terraform-user &lt;span class="nt"&gt;--policy-name&lt;/span&gt; least-privilege &lt;span class="nt"&gt;--policy-document&lt;/span&gt; file://policy.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll be periodically adding more permissions to the policy document and then re-running the &lt;code&gt;aws iam put-user-policy ...&lt;/code&gt; CLI command, so make sure to download and keep the file someplace handy.&lt;/p&gt;

&lt;p&gt;After creating the user and adding the policy, login to the AWS console, navigate to Identity and Access Management (IAM) and then find the user that you just created. Switch to the &lt;em&gt;Security credentials&lt;/em&gt; tab and click &lt;em&gt;Create access key&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbd83agga9nrnaz9nhsis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbd83agga9nrnaz9nhsis.png" alt="Create user access keys with the AWS Console"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use the following template to add a new profile in your AWS credentials file with the name (just make sure to update the &lt;code&gt;{{name}}&lt;/code&gt; template values):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;[TfUser]
aws_access_key_id = {{access_key_value}}
aws_secret_access_key = {{secret_access_key_value}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating infrastructure using the AWS CLI
&lt;/h2&gt;

&lt;p&gt;Let's get started and create an S3 bucket using the AWS CLI. Bucket names do need to be globally unique, so you may need to add some extra characters at the end of the bucket name in order to create it successfully.&lt;/p&gt;

&lt;p&gt;To simplify things, I'm going to set my bucketName as a variable to keep things consistent (if you're not using a &lt;code&gt;bash&lt;/code&gt; terminal, make sure to check what the appropriate syntax is to create and use variables).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;bucketName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;globally-unique-bucket-name-99999
aws s3api create-bucket &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="nv"&gt;$bucketName&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--profile&lt;/span&gt; TfUser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we've created our first AWS resource, we can start work on complying with the second requirement: versioning should be enabled on the bucket. To do that, we can make a different S3 API call via the AWS CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-bucket-versioning &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="nv"&gt;$bucketName&lt;/span&gt; &lt;span class="nt"&gt;--versioning-configuration&lt;/span&gt; &lt;span class="nv"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Enabled &lt;span class="nt"&gt;--profile&lt;/span&gt; TfUser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course! We initially get a permissions error since we haven't added &lt;code&gt;s3:PutBucketVersioning&lt;/code&gt; to our IAM policy. Once we add that permission to our policy and re-deploy the policy, we are able to enable bucket versioning, allowing us to keep multiple variants of an object in the same bucket.&lt;/p&gt;

&lt;p&gt;To comply with the final organizational mandate to add tags to all AWS resources to facilitate cost reporting, we'll go ahead and add a &lt;code&gt;Dept&lt;/code&gt;/&lt;code&gt;Engineering&lt;/code&gt; key-value pair to our bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-bucket-tagging &lt;span class="nt"&gt;--bucket&lt;/span&gt; &lt;span class="nv"&gt;$bucketName&lt;/span&gt; &lt;span class="nt"&gt;--tagging&lt;/span&gt; &lt;span class="nv"&gt;TagSet&lt;/span&gt;&lt;span class="o"&gt;=[{&lt;/span&gt;&lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Dept,Value&lt;span class="o"&gt;=&lt;/span&gt;Engineering&lt;span class="o"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll most likely see a &lt;code&gt;s3:PutBucketTagging&lt;/code&gt; permission error so go ahead and update the policy document with the missing permission and re-deploy.&lt;/p&gt;

&lt;p&gt;And you're done! You made three different API calls to create an S3 bucket, enable versioning, and add tags. However, if you need to maintain that infrastructure over time, e.g., adding new bucket tags, as your business scales, making individual API calls won't scale with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform
&lt;/h2&gt;

&lt;p&gt;Let's examine what it would take to manage infrastructure for the same S3 bucket using Terraform. We'll deploy infrastructure that satisfies all the same business requirements—go ahead and pick a new S3 bucket name since bucket names are globally unique.&lt;/p&gt;

&lt;p&gt;One of the first things to do is to provide credentials for Terraform to use when it invokes the AWS API. In Terraform we do this using providers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
    &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TfUser"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll add these credentials as well as the infrastructure definitions from later steps in a file named &lt;code&gt;main.tf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the above code sample we pass credentials to Terraform using a profile name, but there are a &lt;a href="https://www.terraform.io/docs/providers/aws/index.html" rel="noopener noreferrer"&gt;multitude of other ways&lt;/a&gt;, e.g., environment variables or passing as variables, that are also available. If we need to interact with more than one AWS accounts or use different permission sets to deploy your infrastructure, you can define multiple provider references, although that's outside the scope of this tutorial, so I'll provide a link to the &lt;a href="https://www.terraform.io/docs/configuration/providers.html#alias-multiple-provider-instances" rel="noopener noreferrer"&gt;Terraform guide&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;We've configured credentials for Terraform to use so it's finally time to create our first piece of declarative infrastructure, which you can do with the code sample below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"b"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"globally-unique-bucket-name-999999"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After we initialize Terraform (&lt;code&gt;terraform init&lt;/code&gt;), review the planned infrastructure changes (&lt;code&gt;terraform plan&lt;/code&gt;) and apply the changes, we receive a &lt;strong&gt;403 Forbidden error&lt;/strong&gt; trying to read the S3 bucket we just created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging your first error
&lt;/h2&gt;

&lt;p&gt;This is one of those times where the abstraction that Terraform manages can work against users, especially ones just learning the tool. This can be mitigated, however, with a little extra context. The recommended behavior for &lt;em&gt;most&lt;/em&gt; Terraform resources it that &lt;em&gt;resource &lt;code&gt;Create&lt;/code&gt; and &lt;code&gt;Update&lt;/code&gt; functions should return the resource &lt;code&gt;Read&lt;/code&gt; function&lt;/em&gt;. What this means, practically, is that for every &lt;code&gt;Create&lt;/code&gt; or &lt;code&gt;Put&lt;/code&gt; permission we provide, we will want a corresponding &lt;code&gt;Get&lt;/code&gt;, &lt;code&gt;List&lt;/code&gt; or &lt;code&gt;Head&lt;/code&gt; permission depending on the vagaries of the AWS API.&lt;/p&gt;

&lt;p&gt;And that's exactly what we find if we look at the &lt;a href="https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/resource_aws_s3_bucket.go#L772-L773" rel="noopener noreferrer"&gt;s3 bucket resource implementation&lt;/a&gt;: the &lt;code&gt;Read&lt;/code&gt; function invokes the &lt;code&gt;HeadBucket&lt;/code&gt; endpoint, which is an IAM permission we have not yet provided to our least-privileged user.&lt;/p&gt;

&lt;p&gt;However, even after adding &lt;code&gt;s3:HeadBucket&lt;/code&gt; to our permitted actions in our &lt;code&gt;policy.json&lt;/code&gt; and re-deploying the policy, we still see the forbidden error (this is also consistent with the behavior of the AWS CLI, &lt;code&gt;aws s3api head-bucket --bucket globally-unique-bucket-name --profile TfUser&lt;/code&gt; since the &lt;code&gt;s3:HeadBucket&lt;/code&gt; permission oddly also requires &lt;code&gt;s3:ListBucket&lt;/code&gt;). Update the permitted actions to include &lt;strong&gt;both&lt;/strong&gt; permissions and re-run &lt;code&gt;terraform apply&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  More errors?!?
&lt;/h2&gt;

&lt;p&gt;We're past the initial error, but now we see an even more cryptic error: &lt;code&gt;Error: error getting S3 Bucket CORS configuration: Access Denied: Access Denied&lt;/code&gt;. If you recall from our resource declaration, we don't have any reference to COR configuration, so why is it failing? Unlike the AWS CLI, which more often than not has a one-to-one map between API calls and IAM permissions, Terraform resources frequently have a one-to-many map between resources and API calls (it's abstracting away many of the underlying implementation details). For example, the &lt;code&gt;Read&lt;/code&gt; function for the S3 bucket resource makes more than 10 calls to different S3 API endpoints.&lt;/p&gt;

&lt;p&gt;Rather than list out all the discrete actions in our IAM policy actions, we can add &lt;code&gt;s3:GetBucket*&lt;/code&gt; and &lt;code&gt;s3:Get*Configuration&lt;/code&gt;, which make uses of the IAM wildcard syntax. To figure out the exact IAM actions to add, you can either add them one at a time as you debug each successive &lt;code&gt;AccessDenied&lt;/code&gt; error you receive or sort through the Terraform resource source code (I did the latter and still managed to miss a few).&lt;/p&gt;

&lt;p&gt;With the updated policy in place, you should be able to successfully deploy your first infrastructure 🤞. If you &lt;strong&gt;still&lt;/strong&gt; run into issues, I'd love to hear about them in the comments so I can warn future users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable bucket versioning and tagging
&lt;/h2&gt;

&lt;p&gt;The previous sections already teased this behavior, but many Terraform resources make multiple API calls to create the infrastructure for a single resource. That is to say, we don't create one resource for the S3 bucket, a different resource to enable bucket versioning, and a third to add bucket tags. Instead, we define a single resource that handles all three. Let's go ahead and update our Terraform to add bucket versioning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"b"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"globally-unique-bucket-name-99999"&lt;/span&gt;

    &lt;span class="nx"&gt;versioning&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember that Terraform is managing the abstraction over the API, so if you guessed that part of the &lt;code&gt;Create&lt;/code&gt; and &lt;code&gt;Update&lt;/code&gt; &lt;a href="https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/resource_aws_s3_bucket.go#L1682" rel="noopener noreferrer"&gt;method behaviors for the S3 resource&lt;/a&gt; includes making an API call to the &lt;code&gt;PutBucketVersioning&lt;/code&gt; endpoint, you're right! You should already have that permission in place from the CLI demo, but go ahead and add that action to your IAM policy if you don't for some reason.&lt;/p&gt;

&lt;p&gt;In the Terraform plan output, we see that we are going to perform an in-place update of the existing resource (certain resource property changes will force you to destroy and re-create resources):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;terraform apply
Plan: 0 to add, 1 to change, 0 to destroy
...
versioning &lt;span class="o"&gt;{&lt;/span&gt;
    ~ enabled    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt; -&amp;gt; &lt;span class="nb"&gt;true
    &lt;/span&gt;mfa_delete &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the updated policy in place, you should be able to deploy your infrastructure changes without any errors. Starting to get the hang of it? If not, we'll repeat the same process one more time with tags to reinforce the behavior.&lt;/p&gt;

&lt;p&gt;Update your resource definition one more time to include the &lt;code&gt;tags&lt;/code&gt; property along with the required value. Similarly, you'll want to make sure your IAM actions include the &lt;code&gt;s3:PutBucketTagging&lt;/code&gt; permission before deploying your infrastructure changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"b"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"globally-unique-bucket-name-99999"&lt;/span&gt;

    &lt;span class="nx"&gt;versioning&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"Dept"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Engineering"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plan output again shows that we'll be updating our resource in place to add our bucket tags. Approve the changes to modify your infrastructure one last time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform State
&lt;/h2&gt;

&lt;p&gt;There's still one key feature of Terraform that most tutorials cover that we haven't discussed: Terraform state files. I'm going to provide a short form version since you can likely defer a more complete understanding until you really need it (when you do, the official &lt;a href="https://www.terraform.io/docs/state/purpose.html" rel="noopener noreferrer"&gt;Terraform documentation&lt;/a&gt; is quite good, as is most of their documentation). For now, think of your state file as a &lt;code&gt;.JSON&lt;/code&gt; file that can be stored locally and is responsible for mapping your configuration to resources in the real world.&lt;/p&gt;

&lt;p&gt;To demonstrate this, we're going to temporarily suspend bucket versioning on this bucket via the AWS Console. Once you've done that, run &lt;code&gt;terraform plan&lt;/code&gt;. The first step in the &lt;code&gt;plan&lt;/code&gt; life cycle reads the current configuration of your real world resources via the S3 API: in our case, it returns &lt;code&gt;versioning=false&lt;/code&gt; since you just suspended bucket versioning. The next step compares that real world configuration against the infrastructure configuration where &lt;code&gt;versioning=true&lt;/code&gt; that you defined using Terraform resources. Any differences between the real world and your resource configuration are displayed in your execution plan output.&lt;/p&gt;

&lt;p&gt;Because we can version control our infrastructure configuration files (sometimes referred to as Infrastructure as Code or IaC), this becomes a powerful way to define repeatable, scalable processes to create and manage infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap Up
&lt;/h2&gt;

&lt;p&gt;Although S3 bucket resources are one of the cheaper AWS resources to leave lying around, it's always prudent to clean things up and put them away when you're done using them. Run one final Terraform command &lt;code&gt;terraform destroy&lt;/code&gt; to remove the resources managed by Terraform. This action will make &lt;code&gt;Delete&lt;/code&gt; API calls to the endpoints for tagging, versioning, and the bucket itself, so add the appropriate IAM actions to your permission statement.&lt;/p&gt;

&lt;p&gt;There's a lot more complexity that we haven't covered here. We've already alluded to the fact that managing IAM resources like users, roles, and policies are hard (and perhaps moreso using Terraform resource definitions). &lt;/p&gt;

&lt;p&gt;We also haven't touched on any of the multi-cloud benefits of Terraform, although as you've seen, the resources you've defined so far are intimately connected to the AWS API. If you wanted to deploy the same serverless function code to AWS, Azure, and GCP, the resource definitions, credentials, and providers you use to manage them will differ substantially. &lt;/p&gt;

&lt;p&gt;Finally, we haven't covered Terraform modules, a handy way to create reusable building blocks for your infrastructure, or the Terraform dependency graph.&lt;/p&gt;

&lt;p&gt;Those are all beyond the scope of this getting started guide, but if you found this introduction helpful and wanted to learn more about any of those next-level topics, I'd love to hear about it in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;p&gt;The following resources were either explicitly referenced in the tutorial above, e.g., the AWS CLI and Terraform resource documentation, or were valuable resources when I first started learning Terraform.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.hashicorp.com/terraform/getting-started/intro" rel="noopener noreferrer"&gt;Terraform: Getting Started - AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.gruntwork.io/an-introduction-to-terraform-f17df9c6d180" rel="noopener noreferrer"&gt;Gruntworks: An Introduction to Terraform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-tagging.html" rel="noopener noreferrer"&gt;AWS CLI: S3 Put-Bucket-Tagging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/s3api/put-bucket-versioning.html" rel="noopener noreferrer"&gt;AWS CLI: S3 Put-Bucket-Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/s3api/create-bucket.html" rel="noopener noreferrer"&gt;AWS CLI: S3 Create-Bucket&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.terraform.io/docs/providers/aws/r/s3_bucket.html" rel="noopener noreferrer"&gt;Terraform &lt;code&gt;aws_s3_bucket&lt;/code&gt; resource&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Acknowledgements
&lt;/h2&gt;

&lt;p&gt;Special thanks to Clarissa Sobota and George Brauneis who tested and provided invaluable feedback on some early drafts of this post.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>infrastructure</category>
      <category>operations</category>
    </item>
  </channel>
</rss>
