Meet the experts Terraform module design

rw-book-cover

[Music] how's everybody doing has it been a good day at the conference I come on get a little little more energy Bruno here is very nervous so we have to make him feel right at home super nervous I'm Ned bellance and today's topic is module design best practices and since I don't know anything I brought on these two fin

gentlemen who know a lot so let's start with Bruno um who are you what do you do hey hey everyone I'm Bruno I'm a developer Advocate at Hashi Corp and I'm uh opiniated on modules and Drew and I actually met through the qu contributor program a couple years ago we used to work on uh the ads provider together still do a little bit I still do a little bit and in the past I used to be responsible for large internal library of modules and uh i' like to talk about uh all my opinions yeah all right excellent Andrew welcome Hey everybody

I'm Drew Mullen I'm a principal architect at Riverpoint technology I think from a module design standpoint if you've seen any of my work it'd be through the awsa repos which are a bunch of when I was at Amazon we published a bunch of uh different kinds of tform modules and yeah been in this ecosystem for a while great so let's start with the fundamentals what do you take into consideration when you're getting started to create a module like what do you need to know ahead of time before

you write a single line of code Bruno what about you it's a good question uh it's not so much coding actually it's talking a lot of talking to the different stakeholders and the ones that are using the module eventually um I always like to think in looking at the broader perspective uh as a platform engineer or platform engineering team I usually like to think of it as running a business if you buildt modules without talking to your customers they likely don't end up buying it and if customers don't end up buying anything you're doing the wrong thing uh I think a common mistake that's

made is that people start building turfer modules without talking to their uh to their end users really understand the use case yeah so it's talking talking okay and same thing for you drew like what sort of questions would you ask the stakeholders about the module sure so a big first question ask is whether or not the module is going to be public or private okay you're going to be publishing something um to the public then you have a lot different type of scope that you might be trying to achieve and I think scope is a big

question that you're trying to answer when Bruno was pointing out like you want to talk to your stakeholders you want to understand like what scope is in and what is out because the more flexible your module is sometimes the more complicated it is and you might see less adoption you know what I mean I see so usability is a big factor in that yeah and obviously if it's a if it's a private module you can talk directly to the people that are actually going to consume it if it's going to be a public module then you are serving a wider audience that you don't necessarily have

the input from so you've decided to make a module you've gathered the requirements for it one of the fundamental things you need in a module is inputs what is your approach to what type of input variable should be there what should it should have a default value ever do you do validation like where do you start Drew with input variables I think that's the most crucial question to get at the beginning is because your module what you're presenting is an abstraction and so the

shape that your variables take is exactly the same the way that an API presents a shape so that's the contract that you start to form with your users right so how are you going to organize your variables and often it can be as simple as just a straight up Boolean or like a single um variable but also like you can structure your modules using like types and objects so that you can actually make uh inputs that you know

kind of organize the module in interesting ways that your users can might might make it easier for them to actually start using so controversial question possibly what's your feeling on very complicated data structures for inputs versus a simple string or a Boolean I can start yeah go for um I'm personally not afraid of complicated module inputs I think the the key with that is that you have to provide good examples and good validation so

validators can be a really nice way that you can give someone feedback if they're missing you know the right uh type of argument that you're expecting within the object or something like that yeah and I I quickly want to add to the default values so I think an underrated practice for default values is using null um many people think that null is is nothing null is actually n nothing null represents the absence of a value and if you pass an if you set the default value for no for an input argument for your module terraform

actually amids it and it's not passed down to the resource which ends up uh that you fall back on a default value that's uh specified in the schema of the resource in the provider and if that doesn't have a value it actually falls back usually on an upstream default value in an uh API oh interesting so if I could read that back to you what I'm understanding is if I put a default value of say an empty string M that's different than setting it to null very because the empty string would actually get past as a value for that argument

whereas null is no value set so whatever your default value is use that exactly yeah and I think any line of of code that you write it's a line that you have to maintain and setting it setting default to null uh actually uh lessons a bur enough maintenance why would you set a default of null versus just not setting a default at all if you don't set it default at all uh it actually triggers you to uh input something right okay so you're forcing the user to provide input

in that Cas if if it's an optional argument then I set the default value to no yeah and if I think it's a required uh input argument I don't specify anything and it promps them to to supply value okay that makes sense so we have our input variables and one of the other things that's closely tied to input variables is local values I have opinions on where local values should reside but uh Bruno why don't you tell me where and how do you place local values inside of a module

sure yeah yeah so I really like uh the concept of strong cohesion in uh in in terraform files uh meaning that anything that I deploy uh should lift together okay previously I used to split out all my infrastructure so you'd have an ec2 dotf and an imtf and a dnsf and even though I was deploying two or three applications all my infrastructure was scattered all over those files and locals would live in a locals. TF okay uh at some point I I started to change that a little bit up and I I now have for example application x.f and the load

balancer Liv in there the ec2 instances live in there and the DNS entries for instance live in there now the local values you don't have to I think a common misconception is that you that folks only think that they can declare a locals block only one time you can actually have local blocks multiple times so what I like to do is that the local variable the local values are declared on the top of the terraform file okay that's where I'm using them it's uh and any local that that is

generic or that's used throughout all the different terraform files I specify a locals. TF okay so you're actually placing the locals multiple locals blocks across different files but you still have somewhere to put sort of the global locals if you could call it that exactly Drew how do you feel about that approach I'm pretty similar so shared should go in a shared location if they're shared across multiple files ones that are isolated to a single file goes at the top and then sometimes I'll even get uh creative and like if I have a single

local that's only going to be used for one resource I might put it immediately above it because in that circumstance the only time that you're ever going to look and reference it is when you're looking at that individual resource okay all right so I think we're good on input variables and locals which brings us to the actual resources and data sources that are usually the point of a module I know there's like the the labels module that only just creates strings but aside from that one you're usually trying to create resources you can make resource creation optional

and you can also create multiples and I feel like that's something that's a point of contention for you Bruno on the use of count and for each inside of a module so why have you explain a little bit about how you feel about those meta arguments yeah nice one that this is where I get very Affiliated so to me looping constructs are necessary evil uh as much as I think they're sometimes nice to have to usually bite me back uh so I I live by

two saying count on it only when you must where I mean where I try to say only use account when you really need to okay and I think that only time when I really need to use account is when I run into a modeling problem which is when I want to deploy a conditional resource okay for instance I only want to have this resource on on Def or on prod I'll use account combined with a turn operator Turner operator uh but I I still somehow think it's it seems like a modeling problem and I always try to optimize for not having to set have to count eror so then I'll think should I move this resource outside this uh this

root module or child module and then with for eaches I have a saying that I live by which is for each resource a different one and I try always try to optimize for the least amount of four eaches that are declared in my my terraform configuration because anytime I create a resource with a for each on it and I have to reference it somewhere else that ends up being that ends up with a looping construct as well because then they're being referenced so if I four ident iCal resources that I need to deploy I would

actually declare them separately so the same resource block four times one for each thing that way I avoid any time where I reference that resource that another resource also ends up with a loop and I typically don't deploy 100 or 200 things or n of something so I I don't typically run into the situation where I deploy uh the same resource 200 times so I can get away with with uh declaring those resources separately and sometimes like

I said looping constricts are necessary eil um so I'll actually end up like in Google Cloud you have to enable apis first before you can use them so I know yeah so there I would use it for each I just Loop over a set of apis and I enable them Drew what's your feeling on it because I I don't know if I entirely agree with Bruno so here we do uh differ a little bit where we agree is on count count is basically used as a Boolean for me you either have one or zero other than that I hardly every we use count um

but I really do like the for each construct for resources and I think part of it may even have to go back to what I was saying about um complicated variable structures so for example I have a VPC module where the subnets are defined in a map and you can use any key you want and the nice part about when you design a variable that way is now I have a map that's you know n in length or however many keys and then I can use that to define the 4 each Loop and now each

subnet as it comes up gets as the name label actually what I used as that key right so you can actually get kind of I I I think you can actually make your outputs look um very clean when you start doing things that way because your resources actually have uh name labels on them too interes I said two different approaches to the problem yeah and I can see pluses and minuses to both because I have run into that scenario where I'm creating five of this VM which means means I need five of a network interface

for that VM and I'm going to have to use a four each Loop for both if I know exactly how many I'm going to need then not using the iteration might actually be easier but if I'm leaving it up to the consumer of the module then I might use the the more complicated input variable it may also come down to the whole private versus public thing too right if you're writing modules that are privately used then you're a lot more likely to know the scope and if something's public then you're generally writing a more flexible module so it

might it might have to do with something like that too and to add to that I I used to optimize really for dry code so I I used to be someone that declared for eaches everywhere and I was optimizing for least amount of lines in my terraform code and seeing how sophisticated and fancy I could make it until I realized it it always bites me back in terms of maintainability and usability and and readability mhm uh so that's when I started to optimize for the more the clar like least amount of 4 eaches right and I I do want to get into

module testing in a moment but there's one last important thing that modules have that we should probably address and that's outputs so Drew when you're thinking about designing the outputs that your module emits what's your thought process and what's your preferred structure for outputs yeah so a little bit of hot take here um I never so I decided early when I was starting to design modules I decided early that I never wanted to receive an issue from a user that was like can you please output this

attribute from a resource I just never wanted to see that so my strategy has been to Output resources and to do it in a such a way so that if you're doing like a like a looping construct or something like that you can actually use terraform to Output the group and then you can use those name labels like what I was mentioning so that users can actually key on and get the specific values that they want it does add a little bit of a burden on the users though because right it can be a little bit more complic at and so what I try to

do is include good examples on how you can actually use the outputs of the module so you output the entire object the whole resource yeah with all the attributes and uned blocks and okay I see interesting you say interesting yeah it's not something I do because I I think um if you do that you need to be very consistent I guess then all the modules that you publish internally would have that way of outputting things because to a user it might be a little bit confusing otherwise uh but I can see where it helps because uh down at the end we built uh modules

as like building blocks and we try to offer certain abstraction and if they want to the intent is that they build on top of it we don't want to I personally I don't want to force my company or teams to use modules everywhere MH um so yeah I think it would be nice that you then output the resources I always try to Output all the the attributes and I sometimes run into issues where someone says hey I really need this value you haven't outputed it can you can you create an output for it right I like you I think

part of that Discovery discussion that you have at the beginning could help alleviate that issue if you can identify the outputs that they're expecting to get from the module then you can have more succinct outputs that are easier for the end user to to consume or and then you could expose the entire Resource as a different output if you think that's going to be helpful to them so you have both options do you do this in those public modules as well yeah both

I I would be concerned about a really complicated resource say something like an AKs cluster that has a million attributes dropping that as an output and leaving it up to the end user to parse is um putting some work back on them whereas you might want to be a little more prescriptive about exactly what you're exposing because a lot of those attributes are never going to be relevant to the end user but I see your point all right so let's move move to day two you've published a module you're

starting to get feedback you need to update the module how do you deal with breaking changes oh we we break software all the time right um yeah I think it's that's all about communication uh it's it's perfectly fine to break software I think every every project software that doesn't break uh in terms of a breaking change is is a little bit weird uh like I said it's all about communication uh I think an industrywide standard is uh using Samer catic versioning

specification to communicate braking changes or enh enhancements or buck fixes uh so uh if you use Samford which I would highly recommend um bump a version and communicate it to your users and make sure from a user perspective so the module users pin they pin their module version so they are pinned to a particular version of that module and they don't pull in the latest breaking change MH and from a module author perspective it's communication

um bumping AV version and providing some guidance on how to deal with breaking changes I think the ads and Google provider are an excellent way of uh providing both backward and forward compatibility and providing those guides how to upgrade from V4 to V5 Drew when you're making a change that's not a breaking change uh saying enhancement do you bump the minor version do you is that a patch version what how do you think about that so uh patches are typic bug fixes or you know

minor or not literally minor but something like a dock fix or something like that uh minor patch minor versions for me are any new feature that you're adding okay a new resource or anything like that as long as it doesn't break you know can I ask a question as well uh I suppose so so what do you bump if you uh bump a so you have a a module and that depends on a provider like the random provider and the random provider now gets an upgrade as well how do you

what do you bump is it a minor or a patch yeah so typically I consider unless I'm bumping a provider a major version I consider upgrading providers um a part of like a like a minor version okay yeah and just to clarify for those who aren't as familiar the providers inherited by the root module that invokes the child module so they might want to use a slightly different version than you tested the module with when you're set setting the required

providers uh settings for the child module do you typically pin it to a major version and just let it be whatever minor version I tend to prefer to upgrade them when necessary so um you know if I'm making a structural change like for example when the S3 bucket changed a big a big time from three to four right like if you're adjusting your module that way that's a that's a breaking change in your in your module for No Doubt but like otherwise I typically won't update

the provider unless there's a new feature that directly impacts a resource that I'm using I I bump providers all the time actually because it's it's super easy to bump from 1.1.1 to 1.1.2 I just keep doing that on a regular basis using theen aot or whatever uh I I've noticed that it's well it is super hard to bump major versions so if a module is on 2.0.0 of a provider like the ads provider and they

haven't updated uh it on a weekly basis or monthly basis to keep up with the provider migrating from V2 to V4 is one a heck of a job yeah lot a lot of things changed especially in the AWS provider in that in that time maybe some breaking changes that may have bit me once that so when you are bumping the provider or making other changes in the module you probably want to test that uh Bruno what is your preferred way

of testing modules nice um as I said earlier I always think of building modules internally and providing them to different users is uh it's you're building a l product yeah and what goes unnoticed or I think it's it's it's a very underrated thought is that as a platform Eng engineering team that provides modules to the entire business or to to all different teams in a company you're in directly responsible for all those workloads running on your

module and a lot of people don't think about it that way but if if their module if the module breaks and it causes an outage they'll actually come to you and say hey something weird happened with the module right but back to testing because I treat it like a product I test it from a few different perspectives so I test the unit of work is what this module is intending to do working so I'll deploy it to a temporary sandbox account where we actually we had some cicd set up that if you made a change and we merged it part of part of the merge a check would be to deploy it run

a couple tests using terraform test terraform test is really good at deploying um uh set of resources and actually running the checks and then tearing it down um then I also like to test for breaking changes uh the input input arguments that I have uh if I is there any breaking changes in that interface the interface of the module the interface are the input arguments and the the outputs the attributes uh it do up with ter from test as well and I really like to use input argument uh

validation so this is occurred to me and I want to get your take on it if you add a new input variable to a module and it's a required one do you consider that a braking change uh yes yeah because it it prompts for diff okay well if they bump the provider version and pull my latest required uh input argument it will bump it will provide it a produc a diff and I consider a break change okay yeah because it doesn't seem like a

breaking change you're not creating a new resource or altering an existing one but the person who's consuming the module is now going to get an error because they haven't provided a value for that argument the the same it would be a breaking change from a provider perspective as well say you have the S3 resource and they suddenly add an attribute that you must Now set as well it would be a breaking change there as well yeah so to come back to the testing question Drew what about you what's your preferred process for testing your modules so I'm big fan of terraform test

for a long time we use Terra test and I think Terra test is really great for what it can do but it has this uh burden of being written in a language that not a lot of people know if they're just coming to use a terraform module so it's hard to expect your users to run those right the one thing that I'd add into testing is that um I think so when you structure your modules you typically will see the module code you'll see examples you might see you know a doc's directory or something like that but um examples are one of the first thing that

people click into like if if you look at GitHub views like my example directories are where a lot of people spend a lot of their time and um so I always try to make sure that we have tests that are testing the example repos oh interesting okay to me that's basically you've defined the major use cases that your module is trying to achieve and so writing tests that use those you're validating that the use cases you intend work and you're also uh validating that the documentation that you're offering

and the form of example is still valid right so it serves that dual purpose of I have these examples which are supposed to be guidance on how people are using the module if I make a change to the module and those examples don't work anymore that's a problem but also I get to test the intention of the module by leveraging those examples and that's pretty easy to do with the test framework right yep y I really like the idea of testing the examples I think that's great yeah yeah and to your point it's actually very low overhead you just Define the test and you point to the source you know it's uh it's pretty

straightforward same for provider development I I recently run a workshop on building tform providers and someone asked the examples that the provider provides can we automatically test those that sort of triggered my thought it's nice to do that for modules as well one of my things that I I really care about is documentation what do you think of when it comes to well structured documentation for a module what needs to be there and what would you like to see there Bruno so I always try to think of can you

imagine using terraform without documentation that would be absolute pain I cannot uh I I really like so kudos to uh the team that build a terraform uh terraform docs uh I use it a lot it's a tool that you can use to generate the documentation for your module uh when you say terraform docs you're not talking about the documentation on the Hashi Corp website you're talking about the tool that is called terraform docs okay exactly o Open Source tool on get up called terraform docs that you can use to generate um module documentation with um

if you're building providers it's a TF plugin docs um and that actually looks at your module looks at the terraform configuration and it documents the um uh input arguments and the outputs uh and whether there's a default value the description if something is require required or not y so that implies that you've written good descriptions for your inputs and your outputs correct you should yes might might be helpful I would I would likely not approve a PR if

a input argument isn't documented same for an output value um when you're organizing the inputs and the outputs alphabetical order group them by function uh good one uh well required ones on the top and optional on on the bottom and I try to make things I try to put things together that belong together yeah I have a feeling I think TF doc organizes them for you does it sorts for you so in the T in the variables at TF

yeah you can organize it that way so one other thing that I like to add in is um I think everybody loves pictures right like pictures can convey so much more than paragraphs of explanation so I try to make sure that I include you know some good descriptive information on what's being built what users should expect what the major uh you know decision points are for them and then I try to include diagrams that would show different types of um uses you can also

put those diagrams in your examples repo too so diagram like it's a load balancer with a with an ec2 so you provide a diagram with the low balancer E2 so you see it's a visualization of what you're deploying yeah exactly cool nice that is very helpful because sometimes you look at the module naming and you think you know what's in there but having that picture it lines up better okay yes this is the module for me then again if you need a a visualization to understand what a module does I think that's also

quite painful but I think it's a nice addition yeah I think you just threw some shade on ouch I like a good picture it's okay okay when should you not write a module oh there's a lot of times you don't need to write a module I'm strongly against the idea of pushing and forcing everyone in your company to use modules I think a lot of folks use it to force like security standards and compliance standards you don't Force Security and

compliance standards using modules you force them with organizational policies and everything that's in ads or in Google Cloud to prevent someone from deploying a S3 bucket data is public access on I think it's great uh to provide uh modules if they solve a particular problem I really think that a module should do one thing and it should do it good I I'm not a big fan of a module that exposes all the possible inputs and

exposes all the possible outputs because you have done created a wrapper module and I could have used just used provider level resources um so if there's a good use case for it I uh I'll provide a module uh if it if it offers a certain abstraction um to a team and sometimes I build similar the same type of modules but for different teams so some teams not everyone needs the the same level of abstraction a data scientist team needs

a very different level of abstraction to a workload team that completely runs everything on eks and I I would give them a different eks module than the ones I would give to the data scientists uh yeah true one thing I might add is uh don't write modules that do everything y so we're writing them to inject opinions and to help users to deploy you know complicated structures in a simple way so make sure that you know you have tightly scoped modules scope creep is a real thing amen I've

certainly encountered it myself well this has been fascinating I've learned a little bit hopefully the audience has learned a little bit I'd like to take a moment to thank you Bruno and you drew for agreeing to be on this panel and doing all the work so I can just sit here and enjoy it um I want to let the audience know that we are going to be hosting a Q&A session tomorrow as part of the hallway track so I'm sure some of you have additional questions you like to ask these fine Gent so if you do please come up to the

hallway track tomorrow at 3:30 I think it's the last hallway track why are we always last are they are they trying to tell me something Best For Last yes I like that we're saved the best for last uh so please join us tomorrow at 3:30 to do some Q&A um we're also available after this talk if you have questions uh I said I'm Ned bellance Bruna shotsberger I go by B shsb on socials I'm Drew Mullen I go by Drew Mullen on GitHub absolutely abely fantastic thank you again and thank you everybody for

joining us have a great evening [Music]