github.com/muratcelep/terraform@v1.1.0-beta2-not-internal-4/website/docs/language/modules/develop/composition.html.md (about) 1 --- 2 layout: "language" 3 page_title: "Module Composition" 4 sidebar_current: "docs-modules-composition" 5 description: |- 6 Module composition allows infrastructure to be described from modular 7 building blocks. 8 --- 9 10 # Module Composition 11 12 In a simple Terraform configuration with only one root module, we create a 13 flat set of resources and use Terraform's expression syntax to describe the 14 relationships between these resources: 15 16 ```hcl 17 resource "aws_vpc" "example" { 18 cidr_block = "10.1.0.0/16" 19 } 20 21 resource "aws_subnet" "example" { 22 vpc_id = aws_vpc.example.id 23 24 availability_zone = "us-west-2b" 25 cidr_block = cidrsubnet(aws_vpc.example.cidr_block, 4, 1) 26 } 27 ``` 28 29 When we introduce `module` blocks, our configuration becomes hierarchical 30 rather than flat: each module contains its own set of resources, and possibly 31 its own child modules, which can potentially create a deep, complex tree of 32 resource configurations. 33 34 However, in most cases we strongly recommend keeping the module tree flat, 35 with only one level of child modules, and use a technique similar to the 36 above of using expressions to describe the relationships between the modules: 37 38 ```hcl 39 module "network" { 40 source = "./modules/aws-network" 41 42 base_cidr_block = "10.0.0.0/8" 43 } 44 45 module "consul_cluster" { 46 source = "./modules/aws-consul-cluster" 47 48 vpc_id = module.network.vpc_id 49 subnet_ids = module.network.subnet_ids 50 } 51 ``` 52 53 We call this flat style of module usage _module composition_, because it 54 takes multiple [composable](https://en.wikipedia.org/wiki/Composability) 55 building-block modules and assembles them together to produce a larger system. 56 Instead of a module _embedding_ its dependencies, creating and managing its 57 own copy, the module _receives_ its dependencies from the root module, which 58 can therefore connect the same modules in different ways to produce different 59 results. 60 61 The rest of this page discusses some more specific composition patterns that 62 may be useful when describing larger systems with Terraform. 63 64 ## Dependency Inversion 65 66 In the example above, we saw a `consul_cluster` module that presumably describes 67 a cluster of [HashiCorp Consul](https://www.consul.io/) servers running in 68 an AWS VPC network, and thus it requires as arguments the identifiers of both 69 the VPC itself and of the subnets within that VPC. 70 71 An alternative design would be to have the `consul_cluster` module describe 72 its _own_ network resources, but if we did that then it would be hard for 73 the Consul cluster to coexist with other infrastructure in the same network, 74 and so where possible we prefer to keep modules relatively small and pass in 75 their dependencies. 76 77 This [dependency inversion](https://en.wikipedia.org/wiki/Dependency_inversion_principle) 78 approach also improves flexibility for future 79 refactoring, because the `consul_cluster` module doesn't know or care how 80 those identifiers are obtained by the calling module. A future refactor may 81 separate the network creation into its own configuration, and thus we may 82 pass those values into the module from data sources instead: 83 84 ```hcl 85 data "aws_vpc" "main" { 86 tags = { 87 Environment = "production" 88 } 89 } 90 91 data "aws_subnet_ids" "main" { 92 vpc_id = data.aws_vpc.main.id 93 } 94 95 module "consul_cluster" { 96 source = "./modules/aws-consul-cluster" 97 98 vpc_id = data.aws_vpc.main.id 99 subnet_ids = data.aws_subnet_ids.main.ids 100 } 101 ``` 102 103 ### Conditional Creation of Objects 104 105 In situations where the same module is used across multiple environments, 106 it's common to see that some necessary object already exists in some 107 environments but needs to be created in other environments. 108 109 For example, this can arise in development environment scenarios: for cost 110 reasons, certain infrastructure may be shared across multiple development 111 environments, while in production the infrastructure is unique and managed 112 directly by the production configuration. 113 114 Rather than trying to write a module that itself tries to detect whether something 115 exists and create it if not, we recommend applying the dependency inversion 116 approach: making the module accept the object it needs as an argument, via 117 an input variable. 118 119 For example, consider a situation where a Terraform module deploys compute 120 instances based on a disk image, and in some environments there is a 121 specialized disk image available while other environments share a common 122 base disk image. Rather than having the module itself handle both of these 123 scenarios, we can instead declare an input variable for an object representing 124 the disk image. Using AWS EC2 as an example, we might declare a common subtype 125 of the `aws_ami` resource type and data source schemas: 126 127 ```hcl 128 variable "ami" { 129 type = object({ 130 # Declare an object using only the subset of attributes the module 131 # needs. Terraform will allow any object that has at least these 132 # attributes. 133 id = string 134 architecture = string 135 }) 136 } 137 ``` 138 139 The caller of this module can now itself directly represent whether this is 140 an AMI to be created inline or an AMI to be retrieved from elsewhere: 141 142 ```hcl 143 # In situations where the AMI will be directly managed: 144 145 resource "aws_ami_copy" "example" { 146 name = "local-copy-of-ami" 147 source_ami_id = "ami-abc123" 148 source_ami_region = "eu-west-1" 149 } 150 151 module "example" { 152 source = "./modules/example" 153 154 ami = aws_ami_copy.example 155 } 156 ``` 157 158 ```hcl 159 # Or, in situations where the AMI already exists: 160 161 data "aws_ami" "example" { 162 owner = "9999933333" 163 164 tags = { 165 application = "example-app" 166 environment = "dev" 167 } 168 } 169 170 module "example" { 171 source = "./modules/example" 172 173 ami = data.aws_ami.example 174 } 175 ``` 176 177 This is consistent with Terraform's declarative style: rather than creating 178 modules with complex conditional branches, we directly describe what 179 should already exist and what we want Terraform to manage itself. 180 181 By following this pattern, we can be explicit about in which situations we 182 expect the AMI to already be present and which we don't. A future reader 183 of the configuration can then directly understand what it is intending to do 184 without first needing to inspect the state of the remote system. 185 186 In the above example, the object to be created or read is simple enough to 187 be given inline as a single resource, but we can also compose together multiple 188 modules as described elsewhere on this page in situations where the 189 dependencies themselves are complicated enough to benefit from abstractions. 190 191 ## Multi-cloud Abstractions 192 193 Terraform itself intentionally does not attempt to abstract over similar 194 services offered by different vendors, because we want to expose the full 195 functionality in each offering and yet unifying multiple offerings behind a 196 single interface will tend to require a "lowest common denominator" approach. 197 198 However, through composition of Terraform modules it is possible to create 199 your own lightweight multi-cloud abstractions by making your own tradeoffs 200 about which platform features are important to you. 201 202 Opportunities for such abstractions arise in any situation where multiple 203 vendors implement the same concept, protocol, or open standard. For example, 204 the basic capabilities of the domain name system are common across all vendors, 205 and although some vendors differentiate themselves with unique features such 206 as geolocation and smart load balancing, you may conclude that in your use-case 207 you are willing to eschew those features in return for creating modules that 208 abstract the common DNS concepts across multiple vendors: 209 210 ```hcl 211 module "webserver" { 212 source = "./modules/webserver" 213 } 214 215 locals { 216 fixed_recordsets = [ 217 { 218 name = "www" 219 type = "CNAME" 220 ttl = 3600 221 records = [ 222 "webserver01", 223 "webserver02", 224 "webserver03", 225 ] 226 }, 227 ] 228 server_recordsets = [ 229 for i, addr in module.webserver.public_ip_addrs : { 230 name = format("webserver%02d", i) 231 type = "A" 232 records = [addr] 233 } 234 ] 235 } 236 237 module "dns_records" { 238 source = "./modules/route53-dns-records" 239 240 route53_zone_id = var.route53_zone_id 241 recordsets = concat(local.fixed_recordsets, local.server_recordsets) 242 } 243 ``` 244 245 In the above example, we've created a lightweight abstraction in the form of 246 a "recordset" object. This contains the attributes that describe the general 247 idea of a DNS recordset that should be mappable onto any DNS provider. 248 249 We then instantiate one specific _implementation_ of that abstraction as a 250 module, in this case deploying our recordsets to Amazon Route53. 251 252 If we later wanted to switch to a different DNS provider, we'd need only to 253 replace the `dns_records` module with a new implementation targeting that 254 provider, and all of the configuration that _produces_ the recordset 255 definitions can remain unchanged. 256 257 We can create lightweight abstractions like these by defining Terraform object 258 types representing the concepts involved and then using these object types 259 for module input variables. In this case, all of our "DNS records" 260 implementations would have the following variable declared: 261 262 ```hcl 263 variable "recordsets" { 264 type = list(object({ 265 name = string 266 type = string 267 ttl = number 268 records = list(string) 269 })) 270 } 271 ``` 272 273 While DNS serves as a simple example, there are many more opportunities to 274 exploit common elements across vendors. A more complex example is Kubernetes, 275 where there are now many different vendors offering hosted Kubernetes clusters 276 and even more ways to run Kubernetes yourself. 277 278 If the common functionality across all of these implementations is sufficient 279 for your needs, you may choose to implement a set of different modules that 280 describe a particular Kubernetes cluster implementation and all have the common 281 trait of exporting the hostname of the cluster as an output value: 282 283 ```hcl 284 output "hostname" { 285 value = azurerm_kubernetes_cluster.main.fqdn 286 } 287 ``` 288 289 You can then write _other_ modules that expect only a Kubernetes cluster 290 hostname as input and use them interchangeably with any of your Kubernetes 291 cluster modules: 292 293 ```hcl 294 module "k8s_cluster" { 295 source = "modules/azurerm-k8s-cluster" 296 297 # (Azure-specific configuration arguments) 298 } 299 300 module "monitoring_tools" { 301 source = "modules/monitoring_tools" 302 303 cluster_hostname = module.k8s_cluster.hostname 304 } 305 ``` 306 307 ## Data-only Modules 308 309 Most modules contain `resource` blocks and thus describe infrastructure to be 310 created and managed. It may sometimes be useful to write modules that do not 311 describe any new infrastructure at all, but merely retrieve information about 312 existing infrastructure that was created elsewhere using 313 [data sources](/docs/language/data-sources/index.html). 314 315 As with conventional modules, we suggest using this technique only when the 316 module raises the level of abstraction in some way, in this case by 317 encapsulating exactly how the data is retrieved. 318 319 A common use of this technique is when a system has been decomposed into several 320 subsystem configurations but there is certain infrastructure that is shared 321 across all of the subsystems, such as a common IP network. In this situation, 322 we might write a shared module called `join-network-aws` which can be called 323 by any configuration that needs information about the shared network when 324 deployed in AWS: 325 326 ``` 327 module "network" { 328 source = "./modules/join-network-aws" 329 330 environment = "production" 331 } 332 333 module "k8s_cluster" { 334 source = "./modules/aws-k8s-cluster" 335 336 subnet_ids = module.network.aws_subnet_ids 337 } 338 ``` 339 340 The `network` module itself could retrieve this data in a number of different 341 ways: it could query the AWS API directly using 342 [`aws_vpc`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) 343 and 344 [`aws_subnet_ids`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/subnet_ids) 345 data sources, or it could read saved information from a Consul cluster using 346 [`consul_keys`](https://registry.terraform.io/providers/hashicorp/consul/latest/docs/data-sources/keys), 347 or it might read the outputs directly from the state of the configuration that 348 manages the network using 349 [`terraform_remote_state`](https://www.terraform.io/docs/language/state/remote-state-data.html). 350 351 The key benefit of this approach is that the source of this information can 352 change over time without updating every configuration that depends on it. 353 Furthermore, if you design your data-only module with a similar set of outputs 354 as a corresponding management module, you can swap between the two relatively 355 easily when refactoring.