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.