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

Identical .hcl files in too many places. how to make .hcl files DRY? #1451

Closed
moosahmed opened this issue Dec 2, 2020 · 12 comments
Closed
Labels

Comments

@moosahmed
Copy link

moosahmed commented Dec 2, 2020

We have a repo structure like so:

my-account
- dev
    - eks
        - main.yaml
        - terragrunt.hcl
- rvw
    - eks
        - main.yaml
        - terragrunt.hcl
- stg
    - eks
        - main.yaml
        - terragrunt.hcl
- prd
    - eks
        - main.yaml
        - terragrunt.hcl

All 4 of these terragrunt.hcl files are identical. The only differences are in the main.yaml which is used to feed the variables in the terragrunt.hcl. If I want to change anything in the terragrunt.hcl I have to do that in 4 different locations. There is a lot of logic in our hcl files where we generate a bunch of different variables in locals {} block and have a robust inputs block as the downstream tf modules have a lot of variables.

Ideally I would just have one generic DRY terragrunt.hcl file, in a generic location like
my-account/terragrunt_files/eks.hcl and running terragrunt apply from my-account/dev/eks/ has a file that points to my-account/terragrunt_files/eks.hcl

That way I need to make the change just once then apply to the envs. For a slightly safer method, we would have some sort of versioning. As in it would first be pointing to my-account/terragrunt_files/eks.hcl?ref=v1.0.0 and then we would update to point to my-account/terragrunt_files/eks.hcl?ref=v2.0.0

This example is just using eks, we ofcourse have multiple components. For example extrapolating this example to namespace lets say. if we have 20 namespaces per env. we have 20*4(env) identical terragrunt.hcl s that source our namespace module. Upgrading our namespace module then requires us to make the exact same change in 80 different files.

So other people run into this? how do you keep this dry?

@brikis98
Copy link
Member

brikis98 commented Dec 2, 2020

Terraform and Terragrunt are flexible tools, with many ways to use them, each with different trade-offs. If you want to make things more DRY, there are multiple ways to do this. Here's an example I've been playing around with.

Let's say you have your Terraform code in a modules repo:

modules
├── app
│   └── main.tf
├── mysql
│   └── main.tf
└── vpc
    └── main.tf

To deploy these modules across all your environments (e.g., dev, stage, prod), you could create a live repo that looks like this:

live
├── dev
│   └── us-east-1
│       ├── app
│       │   └── terragrunt.hcl
│       ├── mysql
│       │   └── terragrunt.hcl
│       └── vpc
│           └── terragrunt.hcl
├── env
│   ├── app
│   │   └── terragrunt.hcl
│   ├── mysql
│   │   └── terragrunt.hcl
│   └── vpc
│       └── terragrunt.hcl
├── prod
│   └── us-east-1
│       ├── app
│       │   └── terragrunt.hcl
│       ├── mysql
│       │   └── terragrunt.hcl
│       └── vpc
│           └── terragrunt.hcl
└── stage
    └── us-east-1
        ├── app
        │   └── terragrunt.hcl
        ├── mysql
        │   └── terragrunt.hcl
        └── vpc
            └── terragrunt.hcl

This looks similar to what you had originally, but the env folder is something new, and if we look into the code, you'll see that it allows all of those terragrunt.hcl files to be much more DRY.

If you open up, for example, env/vpc/terragrunt.hcl, here's what you'll find:

locals {
  # abspath(".") should be something like <PATH>/live/<ENV>/<REGION>/<MODULE>
  parsed_path = regex(".*/live/(?P<env>.*?)/(?P<region>.*?)/(?P<module>.*)", abspath("."))
  env         = local.parsed_path.env
  region      = local.parsed_path.region
  module      = local.parsed_path.module

  # Centrally manage CIDR blocks
  cidr_block = {
    dev   = "10.0.0.0/16"
    stage = "10.10.0.0/16"
    prod  = "10.20.0.0/16"
  }

  # Centrally manage what version of the VPC module is used in each environment. This makes it easier to promote
  # a version from dev -> stage -> prod.
  module_version = {
    dev   = "v1.2.4"
    stage = "v1.2.3"
    prod  = "v1.2.3"
  }
}

terraform {
  source = "github.com/<org>/modules.git//vpc?ref=${local.module_version[local.env]}"
}

inputs = {
  aws_region = local.region
  name       = "vpc-${local.env}"
  cidr_block = local.cidr_block[local.env]
}

This file contains all the logic of how to deploy your VPC module in your live environments. To use it, here's what dev/us-east-1/vpc/terragrunt.hcl, stage/us-east-1/vpc/terragrunt.hcl, and prod/us-east-1/vpc/terragrunt.hcl look like:

include {
  path = "../../../env/vpc/terragrunt.hcl"
}

That's it! Those child terragrunt.hcl files in dev, stage, and prod become marker files, mainly there to help you see exactly what's deployed in each environment, but all the actually logic lives in the terragrunt.hcl files in the env folder, keeping things DRY. That logic includes what CIDR blocks to use in each environment, how to name the VPC, and even what version of the vpc module to use in each environment.

Similarly, here's what env/mysql/terragrunt.hcl might look like:

locals {
  # abspath(".") should be something like <PATH>/live/<ENV>/<REGION>/<MODULE>
  parsed_path = regex(".*/live/(?P<env>.*?)/(?P<region>.*?)/(?P<module>.*)", abspath("."))
  env         = local.parsed_path.env
  region      = local.parsed_path.region
  module      = local.parsed_path.module

  # Centrally manage what version of the VPC module is used in each environment. This makes it easier to promote
  # a version from dev -> stage -> prod.
  module_version = {
    dev   = "v1.2.4"
    stage = "v1.2.3"
    prod  = "v1.2.3"
  }  
}

terraform {
  source = "github.com/<org>/modules.git//mysql?ref=${local.module_version[local.env]}"
}

dependency "vpc" {
  config_path = "../vpc"
}

inputs = {
  aws_region = local.region
  name       = "mysql-${local.env}"
  vpc_id     = dependency.vpc.outputs.vpc_id
  subnet_ids = dependency.vpc.outputs.subnet_ids
}

And here's what dev/us-east-1/mysql/terragrunt.hcl might look like:

include {
  path = "../../../env/mysql/terragrunt.hcl"
}

inputs = {
  instance_type = "db.t3.micro"
}

Whereas prod/us-east-1/mysql/terragrunt.hcl might look like this:

include {
  path = "../../../env/mysql/terragrunt.hcl"
}

inputs = {
  instance_type = "db.m4.large"
}

So now the child terragrunt.hcl files in each environment solely differ by the inputs = { ... } they pass, which, by design, are going to be different in each environment. Alternatively, you could manage all the options in one place, by doing something like this in env/mysql/terragrunt.hcl:

# (... rest of the file omitted for simplicity...)

locals {
  instance_types = {
    dev   = "db.t3.micro"
    stage = "db.t3.large"
    prod  = "db.m4.large"
  }
}

inputs = {
  instance_type = local.instance_types[local.env] 

  # (... other inputs omitted for simplicity...)
}

There are many ways to put all these pieces together. Play around with it to see what works best for you!

@moosahmed
Copy link
Author

moosahmed commented Dec 2, 2020

Thanks @brikis98 ! this is helpful kind of along the lines of what I was looking for. I also played with using the cli option --terragrunt-config

something like:

live
├── dev
│   └── us-east-1
│       ├── app
│       │   └── main.yaml
│       ├── mysql
│       │   └── main.yaml
│       └── vpc
│           └── main.yaml
├── env
│   ├── app
│   │   └── terragrunt.hcl
│   ├── mysql
│   │   └── terragrunt.hcl
│   └── vpc
│       └── terragrunt.hcl
├── prod
│   └── us-east-1
│       ├── app
│       │   └── main.yaml
│       ├── mysql
│       │   └── main.yaml
│       └── vpc
│           └── main.yaml
└── stage
    └── us-east-1
        ├── app
        │   └── main.yaml
        ├── mysql
        │   └── main.yaml
        └── vpc
            └── main.yaml

and then running form within live/prod/us-east-1/vpc: terragrunt apply --terragrunt-config <path to env/vpc/terragrunt.hcl> which then renders the variables from main.yaml to feed into the generic terragrunt.hcl file. You see any pros and cons of this option compared to the one you recommended?

Also in either option how do I control the versioning of the env/terragrunt.hcl files itself? as with a new version of my module, there can be new required inputs, requiring the change in the env/terragrunt.hcl but since all the envs are pointing to the same terragrunt.hcl either within the include block or within the cli option. this will force the change to all envs, when terragrunt apply is run.

@brikis98
Copy link
Member

brikis98 commented Dec 3, 2020

and then running form within live/prod/us-east-1/vpc: terragrunt apply --terragrunt-config <path to env/vpc/terragrunt.hcl> which then renders the variables from main.yaml to feed into the generic terragrunt.hcl file. You see any pros and cons of this option compared to the one you recommended?

It seems simpler to me to put a terragrunt.hcl in the child modules (e.g., in dev/us-east-1/app/terragrunt.hcl) and run terragrunt apply than to put those vars in a yaml file and have to remember to run terragrunt with the --terragrunt-config arg.

Also in either option how do I control the versioning of the env/terragrunt.hcl files itself? as with a new version of my module, there can be new required inputs, requiring the change in the env/terragrunt.hcl but since all the envs are pointing to the same terragrunt.hcl either within the include block or within the cli option. this will force the change to all envs, when terragrunt apply is run.

The vars that change should be in the local yaml or terragrunt.hcl file.

@josh-padnick
Copy link
Contributor

Just a small usability thought: Many users are going to miss the idea that additional inputs are coming from an include earlier in the file (i.e. include { path = "../../../env/vpc/terragrunt.hcl"}). It'd be nice we could somehow make that more explicit by putting the include directly in the inputs map, but I know that gets tricky given that a terragrunt.hcl file will also get inputs from other includes, which themselves get inputs from YAML files.

In general, I wish we could make it more explicit where all the inputs come from in a given terragrunt.hcl file.

@yorinasub17
Copy link
Contributor

In general, I wish we could make it more explicit where all the inputs come from in a given terragrunt.hcl file.

I think this will be addressed with imports. The imports feature also gives us some flexibility around breaking up the sources of terragrunt.hcl. E.g., we can have the root terragrunt.hcl provide the remote state and provide config as we do now, but also define secondary terragrunt.hcl files that define the repeated aspects of specific modules.

Unfortunately, we haven't been able to properly prioritize that feature since we did the design work (primarily because of COVID impacting my availability for terragrunt contributions), but perhaps this will be the driving force for it to be prioritized with some of our feature work?

@josh-padnick
Copy link
Contributor

Ah very nice! Yep, that exactly addresses my concern. In practice, I found that when customers would look at a "leaf" terragrunt.hcl file for the first time, they had hard time piecing together what input variables it ultimately applies, so it's great to see an explicit concept like this. Thanks for the prompt reply!

@hotdawg789
Copy link

hotdawg789 commented Dec 21, 2020

This is a nice idea and I like the marker idea. This falls on me when I have a root terragrunt.hcl file and wanting to have multiple includes. I have terraform and hcl in same repo:

.terragrunt.hcl
modules
├── app
│   └── main.tf
├── mysql
│   └── main.tf
└── vpc
    └── main.tf
live
├── dev
│   └── us-east-1
│       ├── app
│       │   └── terragrunt.hcl
│       ├── mysql
│       │   └── terragrunt.hcl
│       └── vpc
│           └── terragrunt.hcl
├── env
│   ├── app
│   │   └── terragrunt.hcl
│   ├── mysql
│   │   └── terragrunt.hcl
│   └── vpc
│       └── terragrunt.hcl
├── prod
│   └── us-east-1
│       ├── app
│       │   └── terragrunt.hcl
│       ├── mysql
│       │   └── terragrunt.hcl
│       └── vpc
│           └── terragrunt.hcl
└── stage
    └── us-east-1
        ├── app
        │   └── terragrunt.hcl
        ├── mysql
        │   └── terragrunt.hcl
        └── vpc
            └── terragrunt.hcl

If you open up, for example, env/vpc/terragrunt.hcl, i have a include at the top. this would allow me to have backend s3 config in root terragrunt.hcl:

include {
  path = find_in_parent_folders()
}
locals {
  # abspath(".") should be something like <PATH>/live/<ENV>/<REGION>/<MODULE>
  parsed_path = regex(".*/live/(?P<env>.*?)/(?P<region>.*?)/(?P<module>.*)", abspath("."))
  env         = local.parsed_path.env
  region      = local.parsed_path.region
  module      = local.parsed_path.module

  # Centrally manage CIDR blocks
  cidr_block = {
    dev   = "10.0.0.0/16"
    stage = "10.10.0.0/16"
    prod  = "10.20.0.0/16"
  }

  # Centrally manage what version of the VPC module is used in each environment. This makes it easier to promote
  # a version from dev -> stage -> prod.
  module_version = {
    dev   = "v1.2.4"
    stage = "v1.2.3"
    prod  = "v1.2.3"
  }
}

terraform {
  source = "../../../../modules/vpc/"
}

inputs = {
  aws_region = local.region
  name       = "vpc-${local.env}"
  cidr_block = local.cidr_block[local.env]
}

as you know, i receive "Only one level of includes is allowed." haha and yes A key feature here is the use of the path_relative_to_include to monkey patch the S3 key of the parent config based on who is importing.

@boldandbusted
Copy link

@brikis98 To piggyback on @hotdawg789 's dilemma, how can I use your example layout and still use Terragrunt's backend configuration options and keep that DRY? Many of the examples available have inheriting the backend and provider configs from a root 'terraform.hcl' file, but in this layout, we cannot reliably use find_in_parent_folders(), if I'm understanding your example correctly. Cheers!

@yorinasub17
Copy link
Contributor

As I alluded to above, a key building block for really making this work is the import feature. This will allow multiple include, as well as flexible merging of config. We are hoping to get to implementing sometime soon - you can follow this ticket to be notified when it gets implemented.

@chris1248
Copy link

The original post is a good question, and unless I read of a good response from terragrunt, I see one answer as to simply stop using terragrunt. It does not fit this use case very well at all. I'd love to stop using terragrunt myself. Not a fan.

@yorinasub17
Copy link
Contributor

We have now implemented enough of the import feature to support a much more DRY configuration. Read through the updated docs on this for the latest.

Beyond that, I believe #759 is the next step to further DRY things up.

Closing this as the original question has been answered. If there are any follow ups, please open a new ticket with updated context. Thanks!

@mickael-ange
Copy link

I came up with the usage of symbolic links to avoid duplicated terragrunt.hcl files. Terragrunt does not support scanning (Walk) directory for terragrunt.hcl files when the directories are symlinks. To workaround this limitation I patched Terragrunt until a better solution emerged.
See #1611 (comment) for more details.

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

No branches or pull requests

8 participants