Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Terraform 0.12 support #731

Merged
merged 58 commits into from
Jun 10, 2019
Merged

Terraform 0.12 support #731

merged 58 commits into from
Jun 10, 2019

Conversation

brikis98
Copy link
Member

@brikis98 brikis98 commented Jun 6, 2019

This PR upgrades Terragrunt to work with Terraform 0.12. Key changes:

  1. We are migrating from having the Terragrunt configuration in terraform.tfvars to terragrunt.hcl. This is necessary because Terraform 0.12 is more strict about what can be in terraform.tfvars files, and the terragrunt = { ... } configuration block we used to work is no longer allowed.

  2. The configuration in terragrunt.hcl is largely the same as what we used to have in terraform.tfvars, except (a) we no longer need the terragrunt = { ... } wrapper, (b) we now use HCL2 syntax, (c) any input variables that need to be passed to the Terraform module need to be moved into an inputs = { ... } block.

  3. HCL2 brings two major benefits: (a) first-class expressions, so not everything has to be wrapped in ${...} and (b) a better parser, so we can now use Terragrunt's built-in functions anywhere in terragrunt.hcl.

PR progress:

  • Update code
  • Update tests
  • Update docs
  • Add migration guide
  • Detect old Terraform version and show error

Note, this PR looks huge, but the vast majority of it is renaming all the terraform.tfvars files to terragrunt.hcl and updating to 0.12 syntax with first-class expressions.

Fixes #466.

@brikis98 brikis98 requested review from autero1 and eak12913 as code owners June 6, 2019 17:20
@brikis98 brikis98 changed the title WIP: Terraform 0.12 support Terraform 0.12 support Jun 6, 2019
Copy link
Contributor

@yorinasub17 yorinasub17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed roughly half of this so far and ran out of stream. Left a few nits, but so far looks great! You can see the marker on how far I got.

TF_VAR_instance_count=10 \
TF_VAR_tags='{"Name":"example-app"}' \
terraform apply
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if you already have TF_VAR_instance_type="t2.large" set, with the intention of overriding what is in the inputs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good point! I've updated the code to explicitly not override any env vars the user has already set, and added tests for the same:

fdce37c
993e195

}

# This now works with Terragrunt 0.19.x and newer!
foo = get_env("FOO", "default")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be wrapped in inputs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, fixed! 47391c6


for key, value := range asEnvVars {
terragruntOptions.Env[key] = value
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this means we can't set the env var TF_VAR_xxx outside of terragrunt to override the inputs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the great catch. Fixed and added tests:

fdce37c
993e195

@@ -560,120 +313,57 @@ func TestResolveEnvInterpolationConfigString(t *testing.T) {
include *IncludeConfig
terragruntOptions *options.TerragruntOptions
expectedOut string
expectedErr error
expectedErr string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious: Why did you switch to checking the string?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before, the error was returned directly from my code, so I could check it exactly. Now, the error is wrapped by the HCL2 parser, so doing an exactly equality check is harder. Therefore, I'm just doing a string contains check for the error name.

assert.Nil(t, actualErr, "For string '%s' include %v and options %v, unexpected error: %v", testCase.str, testCase.include, testCase.terragruntOptions, actualErr)
assert.Equal(t, testCase.expectedOut, actualOut, "For string '%s' include %v and options %v", testCase.str, testCase.include, testCase.terragruntOptions)
}
testCase := testCase
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: We usually add the boilerplate comment about this being done to bring the var into the scope of the forloop.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: 9f19fd9

}

# Input variables to set for your Terraform module
inputs = {
Copy link
Contributor

@lorengordon lorengordon Jun 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The block vs attr distinction is a bit confusing, but either hcl2 or terraform 0.12 impose some validation on this distinction. Just wanting to clarify here that terraform and remote_state truly are blocks, where inputs truly is an attr. Meaning this would not work (with the = denoting the attr notation):

terraform = {
  # ...
}

remote_state = {
  # ...  
}

and notionally, because those two are actually blocks, you could have several of them in the same config, somehow?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found the HCL2 parser was a bit weird with these. To be able to define the attributes within these terraform and remote_state, I had to make them blocks... They really should only appear once, so I wanted to make them attributes, but I got confusing errors when I did that. Maybe attributes can't have well-defined properties within? Not sure.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm frustrated with this change in hcl2 at the moment. Not sure if you've seen it already, but there is a property you can set on the schema to treat a block as an attr. The many, many PRs linked to this issue might point to a way forward to use attrs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

```hcl
# terragrunt.hcl

# terraform is a block, so make sure NOT to include an equals sign
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Groan. I see you address this here.

@brikis98
Copy link
Member Author

brikis98 commented Jun 8, 2019

Just added a bunch of fixes:

  1. Switch to using go-getter directly instead of terraform init -from-module. The latter seems to be poorly maintained, as it had some unfortunate bugs in 0.11 (terraform init -from-module does not work with -get=false hashicorp/terraform#18460) and now in 0.12, it flat out crashes/panics.

  2. Since I'm no longer running terraform init twice, I had to backport the hooks functionality so you could still execute hooks before/after the usage of go-getter.

  3. Terraform 0.12 stores state differently in .tfstate files (all possible keys are now stored, unset ones defaulting to null), so I had to fix the state equality check too.

@brikis98
Copy link
Member Author

brikis98 commented Jun 8, 2019

Oh, one more fix:

Looks like with the --terragrunt-non-interactive, we were assuming "yes" for all prompts, including the question you get for xxx-all commands of whether the xxx command should be applied to "external dependencies" (those outside the working directory). So if you ran destroy-all in folder foo, it not only destroyed everything in folder foo, but also all external dependencies of anything in foo. That is not a safe default, so I have reversed that behavior. It was exposed by a test failure that, I guess, 0.11 was somehow covering up?

terragruntOptions.Logger.Printf("Downloading Terraform configurations from %s into %s", terraformSource.CanonicalSourceURL, terraformSource.DownloadDir)

// go-getter will not download into folders that already exist, so initially, we download into a temp folder
tmpDownloadDir, err := ioutil.TempDir("", "terragrunt-download-temp")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to respect TERRAGRUNT_DOWNLOAD_DIR here? I'm thinking about issues on Windows with long filepaths, and a workaround of exporting something like TERRAGRUNT_DOWNLOAD_DIR = "C:\.terragrunt-cache". If we first download instead to TEMP, that makes the workaround ineffective...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's a good point. Fixed in 65741db.

// Clean up the tmp folder
if err := os.RemoveAll(tmpDownloadDir); err != nil {
return errors.WithStackTrace(err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use defer here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: e239808

@brikis98
Copy link
Member Author

brikis98 commented Jun 9, 2019

Alright, this PR is driving me bonkers with an endless yak shave. First the bugs with terraform init -from-module, which I worked around by moving to go-getter; then bugs with copying folder contents and concurrency; I worked around those, and now TestTerragruntStackCommands is failing with the error:

NoSuchBucket: The specified bucket does not exist

Digging into it, I think this is happening in the part of the code that automatically creates the S3 bucket. What's bewildering is that, as far as I can tell from the logs, the WaitUntilS3BucketExists is returning successfully, which means the S3 HeadBucket operation is saying that the bucket does exist. However, when the code moves on to enable versioning on the bucket, I get back the error the bucket doesn't exist. I even added a 5 second sleep between creation and enabling versioning, in case it was an eventual consistency issue, and it didn't help. WTF? 😕

@lorengordon
Copy link
Contributor

Any chance it might be a credential issue? Maybe temporary keys expiring at an inopportune moment? Or perhaps credential in combination with region, where the bucket is created in one region but then the versioning logic is somehow referencing another region?

@brikis98
Copy link
Member Author

Any chance it might be a credential issue? Maybe temporary keys expiring at an inopportune moment? Or perhaps credential in combination with region, where the bucket is created in one region but then the versioning logic is somehow referencing another region?

No, I don't think it's a credentials issue, as there are a bunch of tests running in parallel, all with the same credentials, and while all the tests before/during/after TestTerragruntStackCommands pass, this one test fails consistently.

@brikis98
Copy link
Member Author

And of course TestTerragruntStackCommands passes without issues on my own computer... It only fails in CI...

@brikis98
Copy link
Member Author

OK, I've put in a workaround that does the S3 bucket creation/configuration in a retry loop with some sleep between retries. Tests seem to be passing consistently now. Finally merging this!

@brikis98 brikis98 merged commit 2fe4b90 into master Jun 10, 2019
@brikis98 brikis98 deleted the tf12 branch June 10, 2019 11:48
@brikis98
Copy link
Member Author

This code is now available in: https://github.com/gruntwork-io/terragrunt/releases/tag/v0.19.0

@barryib
Copy link

barryib commented Jun 11, 2019

@brikis98 awesome. Thanks for your great work. I tried this release, but I was wondering how do we have to handle var files (required and optional) in Terragrunt, because Terraform is now rising and deprecation warning for unknown variables. I opened the following issue #737 to track this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Upgrading to Terraform 0.12: separate configuration file for Terragrunt?
5 participants