One place for hosting & domains

      Variables

      How To Improve Flexibility Using Terraform Variables, Dependencies, and Conditionals


      Introduction

      Hashicorp Configuration Language (HCL), which Terraform uses, provides many useful structures and capabilities that are present in other programming languages. Using loops in your infrastructure code can greatly reduce code duplication and increase readability, allowing for easier future refactoring and greater flexibility. HCL also provides a few common data structures, such as lists and maps (also called arrays and dictionaries respectively in other languages), as well as conditionals for execution path branching.

      Unique to Terraform is the ability to manually specify the resources one depends on. While the execution graph it builds when running your code already contains the detected links (which are correct in most scenarios), you may find yourself in need of forcing a dependency relationship that Terraform was unable to detect.

      In this article, we’ll review the data structures HCL provides, its looping features for resources (the count key, for_each, and for), and writing conditionals to handle known and unknown values, as well as explicitly specifying dependency relationships between resources.

      Prerequisites

      • A DigitalOcean account. If you do not have one, sign up for a new account.

      • A DigitalOcean Personal Access Token, which you can create via the DigitalOcean control panel. Instructions to do that can be found in this link: How to Generate a Personal Access Token.

      • Terraform installed on your local machine and a project set up with the DigitalOcean provider. Complete Step 1 and Step 2 of the How To Use Terraform with DigitalOcean tutorial, and be sure to name the project folder terraform-flexibility, instead of loadbalance. During Step 2, you do not need to include the pvt_key variable and the SSH key resource.

      • A fully registered domain name added to your DigitalOcean account. For instructions on how to do that, visit the official docs.

      Note: This tutorial has specifically been tested with Terraform 0.13.

      Data Types in HCL

      In this section, before you learn more about loops and other features of HCL that make your code more flexible, we’ll first go over the available data types and their uses.

      The Hashicorp Configuration Language supports primitive and complex data types. Primitive data types are strings, numbers, and boolean values, which are the basic types that can not be derived from others. Complex types, on the other hand, group multiple values into a single one. The two types of complex values are structural and collection types.

      Structural types allow values of different types to be grouped together. The main example is the resource definitions you use to specify what your infrastructure will look like. Compared to the structural types, collection types also group values, but only ones of the same type. The three collection types available in HCL that we are interested in are lists, maps, and sets.

      Lists

      Lists are similar to arrays in other programming languages. They contain a known number of elements of the same type, which can be accessed using the array notation ([]) by their whole-number index, starting from 0. Here is an example of a list variable declaration holding names of Droplets you’ll deploy in the next steps:

      variable "droplet_names" {
        type    = list(string)
        default = ["first", "second", "third"]
      }
      

      For the type, you explicitly specify that it’s a list whose element type is string, and then provide its default value. Values enumerated in brackets signify a list in HCL.

      Maps

      Maps are collections of key-value pairs, where each value is accessed using its key of type string. There are two ways of specifying maps inside curly brackets: by using colons (:) or equal signs (=) for specifying values. In both situations, the value must be enclosed with quotes. When using colons, the key must too be enclosed.

      The following map definition containing Droplet names for different environments is written using the equal sign:

      variable "droplet_env_names" {
        type = map(string)
      
        default = {
          development = "dev-droplet"
          staging = "staging-droplet"
          production = "prod-droplet"
        }
      }
      

      If the key starts with a number, you must use the colon syntax:

      variable "droplet_env_names" {
        type = map(string)
      
        default = {
          "1-development": "dev-droplet"
          "2-staging": "staging-droplet"
          "3-production": "prod-droplet"
        }
      }
      

      Sets

      Sets do not support element ordering, meaning that traversing sets is not guaranteed to yield the same order each time and that their elements can not be accessed in a targeted way. They contain unique elements repeated exactly once, and specifying the same element multiple times will result in them being coalesced with only one instance being present in the set.

      Declaring a set is similar to declaring a list, the only difference being the type of the variable:

      variable "droplet_names" {
        type    = set(string)
        default = ["first", "second", "third", "fourth"]
      }
      

      Now that you’ve learned about the types of data structures HCL offers and reviewed the syntax of lists, maps, and sets, which we’ll use throughout this tutorial, you’ll move on to trying some flexible ways of deploying multiple instances of the same resource in Terraform.

      Setting the Number of Resources Using the count Key

      In this section, you’ll create multiple instances of the same resource using the count key. The count key is a parameter available on all resources that specifies how many instances of it to create.

      You’ll see how it works by writing a Droplet resource, which you’ll store in a file named droplets.tf, in the project directory you created as part of the prerequisites. Create and open it for editing by running:

      Add the following lines:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        count  = 3
        image  = "ubuntu-18-04-x64"
        name   = "web"
        region = "fra1"
        size   = "s-1vcpu-1gb"
      }
      

      This code defines a Droplet resource called test_droplet, running Ubuntu 18.04 with 1GB RAM.

      Note that the value of count is set to 3, which means that Terraform will attempt to create three instances of the same resource. When you are done, save and close the file.

      You can plan the project to see what actions Terraform would take by running:

      • terraform plan -var "do_token=${DO_PAT}"

      The output will be similar to this:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # digitalocean_droplet.test_droplet[0] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web" ... } # digitalocean_droplet.test_droplet[1] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web" ... } # digitalocean_droplet.test_droplet[2] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web" ... } Plan: 3 to add, 0 to change, 0 to destroy. ...

      The output details that Terraform would create three instances of test_droplet, all with the same name web. While possible, it is not preferred, so let’s modify the Droplet definition to make the name of each instance different. Open droplets.tf for editing:

      Modify the highlighted line:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        count  = 3
        image  = "ubuntu-18-04-x64"
        name   = "web.${count.index}"
        region = "fra1"
        size   = "s-1vcpu-1gb"
      }
      

      Save and close the file.

      The count object provides the index parameter, which contains the index of the current iteration, starting from 0. The current index is substituted into the name of the Droplet using string interpolation, which allows you to dynamically build a string by substituting variables. You can plan the project again to see the changes:

      • terraform plan -var "do_token=${DO_PAT}"

      The output will be similar to this:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # digitalocean_droplet.test_droplet[0] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web.0" ... } # digitalocean_droplet.test_droplet[1] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web.1" ... } # digitalocean_droplet.test_droplet[2] will be created + resource "digitalocean_droplet" "test_droplet" { ... name = "web.2" ... } Plan: 3 to add, 0 to change, 0 to destroy. ...

      This time, the three instances of test_droplet will have their index in their names, making them easier to track.

      You now know how to create multiple instances of a resource using the count key, as well as fetch and use the index of an instance during provisioning. Next, you’ll learn how to fetch the Droplet’s name from a list.

      Getting Droplet Names From a List

      In situations when multiple instances of the same resource need to have custom names, you can dynamically retrieve them from a list variable you define. During the rest of the tutorial, you’ll see several ways of automating Droplet deployment from a list of names, promoting flexibility and ease of use.

      You’ll first need to define a list containing the Droplet names. Create a file called variables.tf and open it for editing:

      Add the following lines:

      terraform-flexibility/variables.tf

      variable "droplet_names" {
        type    = list(string)
        default = ["first", "second", "third", "fourth"]
      }
      

      Save and close the file. This code defines a list called droplet_names, containing the strings first, second, third, and fourth.

      Open droplets.tf for editing:

      Modify the highlighted lines:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        count  = length(var.droplet_names)
        image  = "ubuntu-18-04-x64"
        name   =  var.droplet_names[count.index]
        region = "fra1"
        size   = "s-1vcpu-1gb"
      }
      

      To improve flexibility, instead of manually specifying a constant number of elements, you pass in the length of the droplet_names list to the count parameter, which will always return the number of elements in the list. For the name, you fetch the element of the list positioned at count.index, using the array bracket notation. Save and close the file when you’re done.

      Try planning the project again. You’ll receive output similar to this:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # digitalocean_droplet.test_droplet[0] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "first" ... } # digitalocean_droplet.test_droplet[1] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "second" ... } # digitalocean_droplet.test_droplet[2] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "third" ... } # digitalocean_droplet.test_droplet[3] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "fourth" ... Plan: 4 to add, 0 to change, 0 to destroy. ...

      As a result of modifications, four Droplets would be deployed, successively named after the elements of the droplet_names list.

      You’ve learned about count, its features and syntax, and using it together with a list to modify the resource instances. You’ll now see its disadvantages, and how to overcome them.

      Understanding the Disadvantages of count

      Now that you know how count is used, you’ll see its disadvantages when modifying the list it’s used with.

      Let’s try deploying the Droplets to the cloud:

      • terraform apply -var "do_token=${DO_PAT}"

      Enter yes when prompted. The end of your output will be similar to this:

      Output

      Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

      Now let’s create one more Droplet instance by enlarging the droplet_names list. Open variables.tf for editing:

      Add a new element to the beginning of the list:

      terraform-flexibility/variables.tf

      variable "droplet_names" {
        type    = list(string)
        default = ["zero", "first", "second", "third", "fourth"]
      }
      

      When you’re done, save and close the file.

      Plan the project:

      • terraform plan -var "do_token=${DO_PAT}"

      You’ll receive output like this:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create ~ update in-place Terraform will perform the following actions: # digitalocean_droplet.test_droplet[0] will be updated in-place ~ resource "digitalocean_droplet" "test_droplet" { ... ~ name = "first" -> "zero" ... } # digitalocean_droplet.test_droplet[1] will be updated in-place ~ resource "digitalocean_droplet" "test_droplet" { ... ~ name = "second" -> "first" ... } # digitalocean_droplet.test_droplet[2] will be updated in-place ~ resource "digitalocean_droplet" "test_droplet" { ... ~ name = "third" -> "second" ... } # digitalocean_droplet.test_droplet[3] will be updated in-place ~ resource "digitalocean_droplet" "test_droplet" { ... ~ name = "fourth" -> "third" ... } # digitalocean_droplet.test_droplet[4] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "fourth" ... } Plan: 1 to add, 4 to change, 0 to destroy. ...

      The output shows that Terraform would rename the first four Droplets and create a fifth one called fourth, because it considers the instances as an ordered list and identifies the elements (Droplets) by their index number in the list. This is how Terraform initially considers the four Droplets:

      Index Number 0 1 2 3
      Droplet Name first second third fourth

      When the a new Droplet zero is added to the beginning, its internal list representation looks like this:

      Index Number 0 1 2 3 4
      Droplet Name zero first second third fourth

      The four initial Droplets are now shifted one place to the right. Terraform then compares the two states represented in tables: at position 0, the Droplet was called first, and because it’s different in the second table, it plans an update action. This continues until position 4, which does not have a comparable element in the first table, and instead a Droplet provisioning action is planned.

      This means that adding a new element to the list anywhere but to the very end would result in resources being modified when they do not need to be. Similar update actions would be planned if an element of the droplet_names list was removed.

      Incomplete resource tracking is the main downfall of using count for deploying a dynamic number of differing instances of the same resource. For a constant number of constant instances, count is a simple solution that works well. In situations like this, though, when some attributes are being pulled in from a variable, the for_each loop, which you’ll learn about later in this tutorial, is a much better choice.

      Referencing the Current Resource (self)

      Another downside of count is that referencing an arbitrary instance of a resource by its index is not possible in some cases.

      The main example is destroy-time provisioners, which run when the resource is planned to be destroyed. The reason is that the requested instance may not exist (it’s already destroyed) or would create a mutual dependency cycle. In such situations, instead of referring to the object through the list of instances, you can access only the current resource through the self keyword.

      To demonstrate its usage, you’ll now add a destroy-time local provisioner to the test_droplet definition, which will show a message when run. Open droplets.tf for editing:

      Add the following highlighted lines:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        count  = length(var.droplet_names)
        image  = "ubuntu-18-04-x64"
        name   =  var.droplet_names[count.index]
        region = "fra1"
        size   = "s-1vcpu-1gb"
      
        provisioner "local-exec" {
          when    = destroy
          command = "echo 'Droplet ${self.name} is being destroyed!'"
        }
      }
      

      Save and close the file.

      The local-exec provisioner runs a command on the local machine Terraform is running on. Because the when parameter is set to destroy, it will run only when the resource is going to be destroyed. The command it runs echoes a string to stdout, which substitutes the name of the current resource using self.name.

      Because you’ll be creating the Droplets in a different way in the next section, destroy the currently deployed ones by running the following command:

      • terraform destroy -var "do_token=${DO_PAT}"

      Enter yes when prompted. You’ll receive the local-exec provisioner being run four times:

      Output

      ... digitalocean_droplet.test_droplet["first"] (local-exec): Executing: ["/bin/sh" "-c" "echo 'Droplet first is being destroyed!'"] digitalocean_droplet.test_droplet["second"] (local-exec): Executing: ["/bin/sh" "-c" "echo 'Droplet second is being destroyed!'"] digitalocean_droplet.test_droplet["second"] (local-exec): Droplet second is being destroyed! digitalocean_droplet.test_droplet["third"] (local-exec): Executing: ["/bin/sh" "-c" "echo 'Droplet third is being destroyed!'"] digitalocean_droplet.test_droplet["third"] (local-exec): Droplet third is being destroyed! digitalocean_droplet.test_droplet["fourth"] (local-exec): Executing: ["/bin/sh" "-c" "echo 'Droplet fourth is being destroyed!'"] digitalocean_droplet.test_droplet["fourth"] (local-exec): Droplet fourth is being destroyed! digitalocean_droplet.test_droplet["first"] (local-exec): Droplet first is being destroyed! ...

      In this step, you learned the disadvantages of count. You’ll now learn about the for_each loop construct, which overcomes them and works on a wider array of variable types.

      Looping Using for_each

      In this section, you’ll consider the for_each loop, its syntax, and how it helps flexibility when defining resources with multiple instances.

      for_each is a parameter available on each resource, but unlike count, which requires a number of instances to create, for_each accepts a map or a set. Each element of the provided collection is traversed once and an instance is created for it. for_each makes the key and value available under the each keyword as attributes (the pair’s key and value as each.key and each.value, respectively). When a set is provided, the key and value will be the same.

      Because it provides the current element in the each object, you won’t have to manually access the desired element as you did with lists. In case of sets, that’s not even possible, as it has no observable ordering internally. Lists can also be passed in, but they must first be converted into a set using the toset function.

      The main advantage of using for_each, aside from being able to enumerate all three collection data types, is that only the actually affected elements will be modified, created, or deleted. If you change the order of the elements in the input, no actions will be planned, and if you add, remove, or modify an element from the input, appropriate actions will be planned only for that element.

      Let’s convert the Droplet resource from count to for_each and see how it works in practice. Open droplets.tf for editing by running:

      Modify the highlighted lines:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        for_each = toset(var.droplet_names)
        image    = "ubuntu-18-04-x64"
        name     = each.value
        region   = "fra1"
        size     = "s-1vcpu-1gb"
      }
      

      You can remove the local-exec provisioner. When you’re done, save and close the file.

      The first line replaces count and invokes for_each, passing in the droplet_names list in the form of a set using the toset function, which automatically converts the given input. For the Droplet name, you specify each.value, which holds the value of the current element from the set of Droplet names.

      Plan the project by running:

      • terraform plan -var "do_token=${DO_PAT}"

      The output will detail steps Terraform would take:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # digitalocean_droplet.test_droplet["first"] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "first" ... } # digitalocean_droplet.test_droplet["fourth"] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "fourth" ... } # digitalocean_droplet.test_droplet["second"] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "second" ... } # digitalocean_droplet.test_droplet["third"] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "third" ... } # digitalocean_droplet.test_droplet["zero"] will be created + resource "digitalocean_droplet" "test_droplet" { ... + name = "zero" ... } Plan: 5 to add, 0 to change, 0 to destroy. ...

      Unlike when using count, Terraform now considers each instance individually, and not as elements of an ordered list. Every instance is linked to an element of the given set, as signified by the shown string element in the brackets next to each resource that will be created.

      Apply the plan to the cloud by running:

      • terraform apply -var "do_token=${DO_PAT}"

      Enter yes when prompted. When it finishes, you’ll remove one element from the droplet_names list to demonstrate that other instances won’t be affected. Open variables.tf for editing:

      Modify the list to look like this:

      terraform-flexibility/variables.tf

      variable "droplet_names" {
        type    = list(string)
        default = ["first", "second", "third", "fourth"]
      }
      

      Save and close the file.

      Plan the project again, and you’ll receive the following output:

      Output

      ... An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: - destroy Terraform will perform the following actions: # digitalocean_droplet.test_droplet["zero"] will be destroyed - resource "digitalocean_droplet" "test_droplet" { ... - name = "zero" -> null ... } Plan: 0 to add, 0 to change, 1 to destroy. ...

      This time, Terraform would destroy only the removed instance (zero), and would not touch any of the other instances, which is the correct behavior.

      In this step, you’ve learned about for_each, how to use it, and its advantages over count. Next, you’ll learn about the for loop, its syntax and usage, and when it can be used to automate certain tasks.

      Looping Using for

      The for loop works on collections, and creates a new collection by applying a transformation to each element of the input. The exact type of the output will depend on whether the loop is surrounded by brackets ([]) or braces ({}), which give a list or a map, respectively. As such, it is suitable for querying resources and forming structured outputs for later processing.

      The general syntax of the for loop is:

      for element in collection:
      transform(element)
      if condition
      

      Similarly to other programming languages, you first name the traversal variable (element) and specify the collection to enumerate. The body of the loop is the transformational step, and the optional if clause can be used for filtering the input collection.

      You’ll now work through a few examples using outputs. You’ll store them in a file named outputs.tf. Create it for editing by running the following command:

      Add the following lines to output pairs of deployed Droplet names and their IP addresses:

      terraform-flexibility/outputs.tf

      output "ip_addresses" {
        value = {
          for instance in digitalocean_droplet.test_droplet:
          instance.name => instance.ipv4_address
        }
      }
      

      This code specifies an output called ip_addresses, and specifies a for loop that iterates over the instances of the test_droplet resource you’ve been customizing in the previous steps. Because the loop is surrounded by curly brackets, its output will be a map. The transformational step for maps is similar to lambda functions in other programming languages, and here it creates a key-value pair by combining the instance name as the key with its private IP as its value.

      Save and close the file, then refresh Terraform state to account for the new output by running:

      • terraform refresh -var "do_token=${DO_PAT}"

      The Terraform refresh command updates the local state with the actual infrastructure state in the cloud.

      Then, check the contents of the outputs:

      Output

      ip_addresses = { "first" = "ip_address" "fourth" = "ip_address" "second" = "ip_address" "third" = "ip_address" }

      Terraform has shown the contents of the ip_addresses output, which is a map constructed by the for loop. (The order of the entries may be different for you.) The loop will work seamlessly for every number of entries—meaning that you can add a new element to the droplet_names list and the new Droplet, which would be created without any further manual input, would also show up in this output automatically.

      By surrounding the for loop in square brackets, you can make the output a list. For example, you could output only Droplet IP addresses, which is useful for external software that may be parsing the data. The code would look like this:

      terraform-flexibility/outputs.tf

      output "ip_addresses" {
        value = [
          for instance in digitalocean_droplet.test_droplet:
          instance.ipv4_address
        ]
      }
      

      Here, the transformational step simply selects the IP address attribute. It would give the following output:

      Output

      ip_addresses = [ "ip_address", "ip_address", "ip_address", "ip_address", ]

      As was noted before, you can also filter the input collection using the if clause. Here is how you would write the loop if you’d filter it by the fra1 region:

      terraform-flexibility/outputs.tf

      output "ip_addresses" {
        value = [
          for instance in digitalocean_droplet.test_droplet:
          instance.ipv4_address
          if instance.region == "fra1"
        ]
      }
      

      In HCL, the == operator checks the equality of the values of the two sides—here it checks if instance.region is equal to fra1. If it is, the check passes and the instance is transformed and added to the output, otherwise it is skipped. The output of this code would be the same as the prior example, because all Droplet instances are in the fra1 region, according to the test_droplet resource definition. The if conditional is also useful when you want to filter the input collection for other values in your project, like the Droplet size or distribution.

      Because you’ll be creating resources differently in the next section, destroy the currently deployed ones by running the following command:

      • terraform destroy -var "do_token=${DO_PAT}"

      Enter yes when prompted to finish the process.

      We’ve gone over the for loop, its syntax, and examples of usage in outputs. You’ll now learn about conditionals and how they can be used together with count.

      Directives and Conditionals

      In one of the previous sections, you’ve seen the count key and how it works. You’ll now learn about ternary conditional operators, which you can use elsewhere in your Terraform code, and how they can be used with count.

      The syntax of the ternary operator is:

      condition ? value_if_true : value_if_false
      

      condition is an expression that computes to a boolean (true or false). If the condition is true, then the expression evaluates to value_if_true. On the other hand, if the condition is false, the result will be value_if_false.

      The main use of ternary operators is to enable or disable single resource creation according to the contents of a variable. This can be achieved by passing in the result of the comparison (either 1 or 0) to the count key on the desired resource.

      Let’s add a variable called create_droplet, which will control if a Droplet will be created. First, open variables.tf for editing:

      Add the highlighted lines:

      terraform-flexibility/variables.tf

      variable "droplet_names" {
        type    = list(string)
        default = ["first", "second", "third", "fourth"]
      }
      
      variable "create_droplet" {
        type = bool
        default = true
      }
      

      This code defines the create_droplet variable of type bool. Save and close the file.

      Then, to modify the Droplet declaration, open droplets.tf for editing by running:

      Modify your file like the following:

      terraform-flexibility/droplets.tf

      resource "digitalocean_droplet" "test_droplet" {
        count  = var.create_droplet ? 1 : 0
        image  = "ubuntu-18-04-x64"
        name   =  "test_droplet"
        region = "fra1"
        size   = "s-1vcpu-1gb"
      }
      

      For count, you use a ternary operator to return either 1 if the create_droplet variable is true, and 0 if false, which will result in no Droplets being provisioned. Save and close the file when you’re done.

      Plan the project execution plan with the variable set to false by running:

      • terraform plan -var "do_token=${DO_PAT}" -var "create_droplet=false"

      You’ll receive the following output:

      Output

      Refreshing Terraform state in-memory prior to plan... The refreshed state will be used to calculate this plan, but will not be persisted to local or remote state storage. ------------------------------------------------------------------------ No changes. Infrastructure is up-to-date. This means that Terraform did not detect any differences between your configuration and real physical resources that exist. As a result, no actions need to be performed.

      Because create_droplet was passed in the value of false, the count of instances is 0, and no Droplets will be created.

      You’ve reviewed how to use the ternary conditional operator together with the count key to enable a higher level of flexibility in choosing whether to deploy desired resources. Next you’ll learn about explicitly setting resource dependencies for your resources.

      Explicitly Setting Resource Dependencies

      While creating the execution plan for your project, Terraform detects dependency chains between resources and implicitly orders them so that they will be built in the appropriate order. In the majority of cases, it is able to detect relationships by scanning all expressions in resources and building a graph.

      However, when one resource requires access control settings to already be deployed at the cloud provider, in order to be provisioned, there is no clear sign to Terraform that they are related. In turn, Terraform will not know they are dependent on each other behaviorally. In such cases, the dependency must be manually specified using the depends_on argument.

      The depends_on key is available on each resource and used to specify to which resources one has hidden dependency links. Hidden dependency relationships form when a resource depends on another one’s behavior, without using any of its data in its declaration, which would prompt Terraform to connect them one way.

      Here is an example of how depends_on is specified in code:

      resource "digitalocean_droplet" "droplet" {
        image  = "ubuntu-18-04-x64"
        name   = "web"
        region = "fra1"
        size   = "s-1vcpu-1gb"
      
        depends_on = [
          # Resources...
        ]
      }
      

      It accepts a list of references to other resources, and it does not accept arbitrary expressions.

      depends_on should be used sparingly, and only when all other options are exhausted. Its use signifies that what you are trying to declare is stepping outside the boundaries of Terraform’s automated dependency detection system; it may signify that the resource is explicitly depending on more resources than it needs to.

      You’ve now learned about explicitly setting additional dependencies for a resource using the depends_on key, and when it should be used.

      Conclusion

      In this article, we’ve gone over the features of HCL that improve flexibility and scalability of your code, such as count for specifying the number of resource instances to deploy and for_each as an advanced way of looping over collection data types and customizing instances. When used correctly, they greatly reduce code duplication and operational overhead of managing the deployed infrastructure.

      You’ve also learned about conditionals and ternary operators, and how they can be utilized to control if a resource will get deployed. While Terraform’s automated dependency analysis system is quite capable, there may be cases where you need to manually specify resource dependencies using the depends_on key.

      To learn more about Terraform, check out our How To Manage Infrastructure with Terraform series.



      Source link

      Working with Environment Variables in Vue.js


      While this tutorial has content that we believe is of great benefit to our community, we have not yet tested or
      edited it to ensure you have an error-free learning experience. It’s on our list, and we’re working on it!
      You can help us out by using the “report an issue” button at the bottom of the tutorial.

      In this post, we’ll learn how to work with distinct configurations between development and production mode for Vue.js projects that use the CLI’s webpack template.

      In a web app, we most likely have to access a backend API server through a URL. This URL can be something like http://localhost:8080/api while in development, and https://site.com/api in production when the project is deployed. Environment variables allow us for an easy way to change this URL automatically, according to the current state of the project.

      An easy way to use environment variables with Vue and the webpack template is through files with a .env extension. These files become responsible for storing information that’s specific to the environment (development, testing, production,…)

      The majority of this post applies to apps using v2.x of the Vue CLI, but environment variables are just as easy to manage in the Vue CLI v3.

      Using .env Files in Vue

      The simplest way to use .env files in Vue is to create an application that already supports environment files. Let’s use the vue-cli and the webpack template for that.

      With Node 8 or higher installed, run the following, where my-app is your app name:

      $ npx vue-cli init webpack my-app
      

      This command will create an application with several files ready for use. In this post, we’re focusing only on the environment configuration, which can be accessed in the config directory:

      Project file structure

      There are two files in the config directory: dev.env.js and prod.env.js, and you’ll also have a test.env.js file if you’ve configured tests while initiating the project. These files are used in development and production mode, or in other words, when you are running the application through the npm run dev command, the dev.env.js file is used, and when you compile the project for production with the npm run build command, the prod.env.js file is used instead.

      Let’s change the development file to:

      dev.env.js

      'use strict'
      const merge = require('webpack-merge')
      const prodEnv = require('./prod.env')
      
      module.exports = merge(prodEnv, {
        NODE_ENV: '"development"',
        ROOT_API: '"http://localhost/api"'
      })
      

      Our development environment file has an additional variable called ROOT_API, with the value http://localhost/api.

      Now let’s change the production file to:

      prod.env.js

      'use strict'
      module.exports = {
        NODE_ENV: '"production"',
        ROOT_API: '"http://www.site.com/api"'
      }
      

      Here we have the same ROOT_API variable, but with a different value, which should only be used in production mode. Note how string variables need the double quotes inside the single quotes.

      Using the Environment Files in Your Code

      After creating the ROOT_API variable, we can use it anywhere in Vue through the global process.env object:

      process.env.ROOT_API
      

      For example, open the src/components/HelloWorld.vue file and in the <script> tag add the following:

      mounted() {
        console.log(process.env.ROOT_API)
      }
      

      After running npm run dev, you will see the console.log information in the browser dev tools:

      Running the app

      If you run the npm run build command, the dist directory will be created with the application ready to be deployed to a production environment, and the variable ROOT_API will display the value http://www.site.com./api, as specified in prod.env.js.

      Thus, we can work with different variables for each different environment, using the ready-made configuration that the webpack template provides us. If you use another template, make sure you find an equivalent feature or use a library like dotenv to manage your environment variables.

      What About Vue CLI 3?

      If your app is using the new Vue CLI, you’ll want to instead have files like .env and .env.prod at the root of your project and include variables like this:

      .env

      VUE_APP_ROOT_API=http://localhost/api
      

      .env.prod

      VUE_APP_ROOT_API=http://www.site.com/api
      

      The VUE_APP_ prefix is important here, and variables without that prefix won’t be available in your app.



      Source link

      Cómo usar variables y constantes en Go


      Las variables son un concepto de programación que es importante dominar. Son símbolos que sustituyen valores que se usan en un programa.

      En este tutorial, se abarcarán algunos aspectos básicos de variables y las prácticas recomendadas para utilizarlos en los programas de Go que cree.

      Información sobre variables

      En términos técnicos, una variable asigna una ubicación de almacenamiento a un valor vinculado a un nombre o identificador simbólico. Usamos el nombre de variable para hacer referencia a ese valor almacenado en un programa informático.

      Podemos concebir una variable como una etiqueta con nombre, que se vincula a un valor.

      Variables en Go

      Supongamos que tenemos un número entero, 103204948, y queremos almacenar esta larga cifra en una variable en lugar de volver a escribirla una y otra vez. Para hacerlo, podemos usar un nombre fácil recordar, como la variable i. Para almacenar un valor en una variable, usaremos la siguiente sintaxis:

      i := 1032049348
      

      Podemos imaginar esta variable como una etiqueta que está vinculada al valor.

      Ejemplo de una variable Go

      La etiqueta tiene escrito el nombre de una variable i y está vinculada al valor entero 1032049348.

      La frase i := 1032049348 es una instrucción de declaración y asignación que consta de algunas partes:

      • el nombre de la variable (i)
      • la instrucción de declaración de variable corta (​​​​​​:=​​​​​​)
      • el valor vinculado al nombre de la variable (1032049348)
      • el tipo de datos inferido por Go (int)

      Más adelante, veremos la manera de establecer el tipo de forma explícita en la siguiente sección.

      Juntas, estas partes conforman la instrucción que establece que la variable i es igual al valor del número entero 1032049348.

      En el momento en que se establece una variable igual a un valor, se inicia o se crea esa variable. Una vez que lo hagamos, estaremos listos para usar la variable en lugar del valor.

      Una vez que establezcamos que i es igual al valor 1032049348, podremos usar i en lugar del número entero, por lo que lo imprimiremos:

      package main
      
      import "fmt"
      
      func main() {
          i := 1032049348
          fmt.Println(i)
      }
      

      Output

      1032049348

      Mediante variables también se pueden realizar cálculos de forma rápida y sencilla. Con i := 1032049348, podemos restar el valor entero 813 con la siguiente sintaxis:

      fmt.Println(i - 813)
      

      Output

      1032048535

      En este ejemplo, Go hace los cálculos por nosotros y resta 813 a la variable i para mostrar la suma 1032048535.

      Ya que mencionamos los cálculos, las variables pueden configurarse de modo que sean iguales al resultado de una ecuación matemática. También puede añadir dos números juntos y almacenar el valor de la suma en la variable x:

      x := 76 + 145
      

      Posiblemente haya observado que este ejemplo se asemeja a uno de álgebra. Así como usamos letras y otros símbolos para representar números y cantidades en fórmulas y ecuaciones, las variables son nombres simbólicos que representan el valor de un tipo de datos. Para corregir la sintaxis de Go, deberá asegurarse que su variable esté en el lado izquierdo de cualquier ecuación.

      Imprimiremos x:

      package main
      
      import "fmt"
      
      func main() {
          x := 76 + 145
          fmt.Println(x)
      }
      

      Output

      221

      Go mostró el valor 221 porque la variable x se estableció en un valor igual a la suma de 76 y 145.

      Las variables pueden representar cualquier tipo de datos, no solo números enteros:

      s := "Hello, World!"
      f := 45.06
      b := 5 > 9 // A Boolean value will return either true or false
      array := [4]string{"item_1", "item_2", "item_3", "item_4"}
      slice := []string{"one", "two", "three"}
      m := map[string]string{"letter": "g", "number": "seven", "symbol": "&"}
      

      Si imprime cualquiera de estas variables, Go mostrará lo que es equivalente a esa variable. Trabajar con la instrucción de asignación para el tipo de datos slice de cadena:

      package main
      
      import "fmt"
      
      func main() {
          slice := []string{"one", "two", "three"}
          fmt.Println(slice)
      }
      

      Output

      [one two three]

      Asignamos el valor de slice de []string{"one", "two", "three"} a la variable slice y luego usamos la función fmt.Println para imprimir ese valor llamando a slice.

      Las variables funcionan quitando una área pequeña de memoria de su computadora que acepta valores especificados, los cuales luego se asocian con ese espacio.

      Declarar variables

      En Go, hay varias formas de declarar una variable y, en algunos casos, más de una forma de declarar exactamente la variable y el valor idénticos.

      Podemos declarar una variable llamada i del tipo de datos int sin iniciarla. Esto significa que declararemos un espacio para ubicar un valor, pero no le daremos un valor inicial:

      var i int
      

      Esto crea una variable del tipo de datos int declarada como i.

      Podemos inicializar el valor usando el operador igual (=), como en el siguiente ejemplo:

      var i int = 1
      

      En Go, las dos formas de instrucción se denominan declaraciones variables largas.

      También podemos usar una declaración variable corta:

      i := 1
      

      En este caso, contamos con una variable llamada i y un tipo de datos int. Cuando no especifiquemos un tipo de datos, Go lo inferirá.

      Con las tres formas de declarar variables, la comunidad de Go adoptó los siguientes idiomas:

      • Utilice únicamente una forma larga, var i int, cuando no inicialice la variable.

      • Utilice la forma corta, i := 1, en la declaración y el inicio.

      • Si no desea que Go infiera su tipo de datos, pero de todos modos quiere usar una declaración variable corta, puede ajustar su valor en el tipo que desee con la siguiente sintaxis:

      i := int64(1)
      

      No se considera idiomático en Go el uso del formulario de instrucción variable larga cuando se inicia el valor:

      var i int = 1
      

      Es recomendable mantenerse al corriente sobre la manera en que la comunidad de Go declara variables para que otros puedan leer sus programas sin problemas.

      Valores cero

      Todos los tipos incorporados tienen un valor cero. Cualquier variable asignada se puede usar incluso cuando nunca tenga un valor asignado. Podemos ver los valores cero para los siguientes tipos:

      package main
      
      import "fmt"
      
      func main() {
          var a int
          var b string
          var c float64
          var d bool
      
          fmt.Printf("var a %T = %+vn", a, a)
          fmt.Printf("var b %T = %qn", b, b)
          fmt.Printf("var c %T = %+vn", c, c)
          fmt.Printf("var d %T = %+vnn", d, d)
      }
      

      Output

      var a int = 0 var b string = "" var c float64 = 0 var d bool = false

      Usamos el verbo %T en la instrucción fmt.Printf. Esto indica a la función que imprima el data type de la variable.

      En Go, debido a que todos los valores tienen un valor zero, no puede haber valores undefined como en otros lenguajes. Por ejemplo, un boolean en algunos lenguajes podría ser undefined, true o false, lo que permite tres estados para la variable. En Go, no podemos exceder el valor de estados two para un valor boolean.

      Nombrar variables: reglas y estilos

      La asignación de nombres a variables es bastante flexible, pero hay reglas que se deben tener en cuenta:

      • Los nombres de variables solo deben constar de una palabra (no deben llevar espacios).
      • Deben contener solo letras, números y guiones bajos (_).
      • No pueden comenzar con un número.

      Siguiendo estas reglas, observaremos los nombres de variables válidos y no válidos:

      Válido No válido Por qué no es válido
      userName user-name No se permiten a guiones.
      name4 4name No puede comenzar con un número.
      user $user No puede llevar símbolos.
      userName Nombre de usuario No puede constar de más de una palabra.

      Además, al asignar nombres a variables tenga en cuenta que se distinguen mayúsculas y minúsculas. Estos nombres ​​​1​​​userName​​​1​​​, ​​​2​​​USERNAME​​​2​​​, ​​​3​​​UserName​​​3​​​ y ​​​4​​​uSERnAME​​​4​​​ son variables completamente diferentes. Es recomendable evitar usar nombres de variables similares en un programa para garantizar que tanto usted como sus colaboradores, actuales y futuros, puedan mantener sus variables de forma correcta.

      Si bien para las variables se distinguen mayúsculas y minúsculas, el caso de la primera letra de una variable tiene un significado especial en Go. Si una variable comienza con una letra mayúscula, se puede acceder a dicha variable fuera del paquete en el que se declaró (o recibió el valor exported). Si una variable comienza con una letra minúscula, solo está disponible dentro del paquete en el que se declara.

      var Email string
      var password string
      

      Email comienza con una letra mayúscula y otros paquetes pueden acceder a él. password comienza con minúscula y el acceso a ella solo es posible dentro del paquete en el que se declara.

      En Go, es común utilizar nombres de variables muy concisos (o cortos). Dada la opción entre userName y user para una variable, sería idiomático elegir user.

      El ámbito también desempeña un papel en la unidad del nombre de variable. La regla es que cuanto más pequeño es el ámbito de la variable, más pequeño es el nombre de esta:

      names := []string{"Mary", "John", "Bob", "Anna"}
      for i, n := range names {
          fmt.Printf("index: %d = %qn", i, n)
      }
      

      Utilizamos la variable names en un ámbito más amplio, por lo que sería normal darle un nombre más significativo para ayudar a recordar lo que representa en el programa. Sin embargo, usamos las variables i y n inmediatamente en la siguiente línea de código, y luego no las volvemos a usar. Debido a esto, no confundirá a alguien que lea el código respecto de dónde se usan las variables o qué significan.

      A continuación, veremos algunas notas sobre el estilo de las variables. El estilo consiste en usar MixedCaps o mixedCaps en lugar de guiones bajos para nombres de varias palabras.

      Estilo convencional Estilo no convencional Por qué no convencional
      userName user_name Los guiones bajos no son convencionales.
      i Índice Priorizar i sobre index, ya que es más corto.
      serveHTTP serveHttp Los acrónimos se deben escribir con mayúsculas.

      Lo más importante respecto del estilo es preservar la uniformidad y que el equipo del que forma parte esté de acuerdo acerca de este.

      Volver a asignar variables

      Como la plantea la palabra “variable”, podemos cambiar variables de Go. Esto significa que podemos conectar un valor diferente y una variable previamente asignada realizando nuevamente una asignación. Es útil renovar una asignación porque durante el curso de un programa, es posible que debamos aceptar valores generados por los usuarios en variables ya inicializadas. Es posible que también deba cambiar la asignación por algo previamente definido.

      Saber que es posible podemos volver a asignar una variable puede ser útil al trabajar en un programa de gran magnitud que alguien más escribió y en el cual no están claras las variables ya definidas.

      Asignaremos el valor de 76 a una variable llamada i del tipo int y luego le asignaremos un nuevo valor de 42:

      package main
      
      import "fmt"
      
      func main() {
          i := 76
          fmt.Println(i)
      
          i = 42
          fmt.Println(i)
      }
      

      Output

      76 42

      En este ejemplo se muestra que podemos primero asignar a la variable i el valor de un número entero y luego podemos volver a asignar a esta variable i un valor; esta vez, el 42.

      Nota: Cuando declare *e *inicialice una variable, puede usar :=. Sin embargo, cuando quiera simplemente cambiar el valor de una variable ya declarada, solo necesita usar el operador “igual” (=).

      Debido a que Go es un lenguaje typed, no podemos asignar un tipo a otro. Por ejemplo, no podemos asignar el valor “Sammy” a una variable de tipo int:

      i := 72
      i = "Sammy"
      

      Tratar de asignar diferentes tipos entre sí provocará un error en el tiempo de compilación:

      Output

      cannot use "Sammy" (type string) as type int in assignment

      Go no nos permitirá usar un nombre de variable más de una vez:

      var s string
      var s string
      

      Output

      s redeclared in this block

      Si intentamos usar una instrucción variable corta más de una vez para el mismo nombre de variable, también observaremos un error de compilación. Esto puede ocurrir por equivocación, por lo cual entender el mensaje de error es útil:

      i := 5
      i := 10
      

      Output

      no new variables on left side of :=

      Casi como en el caso de una declaración de variable, tener en cuenta el nombre de sus variables mejorará la legibilidad de su programa para usted y otros, cuando la vuelva a revisarla en el futuro.

      Asignación múltiple

      Go también nos permite asignar varios valores a diferentes variables dentro de la misma línea. Cada uno de estos valores puede ser de un tipo de datos diferente:

      j, k, l := "shark", 2.05, 15
      fmt.Println(j)
      fmt.Println(k)
      fmt.Println(l)
      

      Output

      shark 2.05 15

      En este ejemplo, la variable j se asignó a la cadena “shark”, la variable k se asignó al flotante 2.05 y la variable l se asignó al número entero 15.

      Este enfoque para asignar varias variables a diferentes valores en una línea puede hacer que la cantidad de líneas de su código se mantenga en un valor bajo. Sin embargo, es importante no comprometer la legibilidad por menos líneas de código.

      Variables globales y locales

      Al usar variables dentro de un programa, es importante tener en cuenta el ámbito de variable. El ámbito de una variable se refiere a espacios concretos desde los cuales es posible acceder a ella dentro del código de un programa determinado. Esto es decir que no es posible el acceso a todas las variables desde todas las partes de un programa determinado; algunas variables serán globales y algunas locales.

      Las variables globales existen fuera de las funciones. Las locales existen dentro de las funciones.

      Veamos las variables globales y locales en acción:

      package main
      
      import "fmt"
      
      
      var g = "global"
      
      func printLocal() {
          l := "local"
          fmt.Println(l)
      }
      
      func main() {
          printLocal()
          fmt.Println(g)
      }
      

      Output

      local global

      Aquí usamos var g = "global"para crear una variable global fuera de la función. Luego definimos la función ​​​1​​​printLocal(). Dentro de la función, se asigna una variable local llamada l que luego se imprime. El programa se cierra llamando a printLocal() e imprimiendo la variable global g.

      Debido a que g es una variable global, podemos hacer referencia a ella en printLocal(). Modificaremos el programa anterior para hacerlo:

      package main
      
      import "fmt"
      
      
      var g = "global"
      
      func printLocal() {
          l := "local"
          fmt.Println(l)
          fmt.Println(g)
      }
      
      func main() {
          printLocal()
          fmt.Println(g)
      }
      

      Output

      local global global

      Comenzamos declarando una variable global g, var g = "global". En la función main, llamaremos a la función printLocal, que declara una variable local l e imprime fmt.Println(l). Luego, printLocal imprime la variable global g, fmt.Println(g). Aunque g no se definió en printLocal, podría acceder a ella debido a que se declaró en un alcance global. Por último, la función main imprime g también.

      Ahora, intentaremos invocar la variable local fuera de la función:

      package main
      
      import "fmt"
      
      var g = "global"
      
      func printLocal() {
          l := "local"
          fmt.Println(l)
      }
      
      func main() {
          fmt.Println(l)
      }
      
      

      Output

      undefined: l

      No podemos usar una variable local fuera de la función en la que se asigne. Si intenta hacerlo, verá un error undefined cuando realice la compilación.

      Veamos otro ejemplo en el que usamos el mismo nombre de variable para una variable global y una local:

      package main
      
      import "fmt"
      
      var num1 = 5
      
      func printNumbers() {
          num1 := 10
          num2 := 7  
      
          fmt.Println(num1)
          fmt.Println(num2)
      }
      
      func main() {
          printNumbers()
          fmt.Println(num1)
      }
      

      Output

      10 7 5

      En este programa, declaramos la variable num1 dos veces. Primero, declaramos num1 en el ámbito global, var num1 = 5, y otra vez dentro del alcance local de la función printNumbers, num1 := 10. Cuando imprimimos num1 desde el programa main, vemos el valor 5 impreso. Esto se debe a que main solo detecta la declaración de variable global. Sin embargo, cuando imprimimos num1 de la función printNumbers, esta detecta la instrucción local e imprime el valor 10. Aunque printNumbers crea una nueva variable llamada num1 y le asigna un valor de 10, esto no afecta a la instancia global de num1 con el valor 5.

      Al trabajar con variables, también deberá tener en cuenta las partes de su programa que necesitarán acceso a cada variable y adoptar una variable global o local correspondiente. En diferentes programas de Go, verá que las variables locales son normalmente más comunes.

      Constantes

      Las constantes son como variables, con la excepción de que no pueden modificarse una vez declaradas. Las constantes son útiles para definir un valor que se usará más de una vez en su programa, pero que no debería poder cambiar.

      Por ejemplo, si queremos declarar la tasa impositiva para un sistema de carritos de compras, podríamos usar una constante y luego calcular los impuestos en diferentes áreas de nuestro programa. En algún momento del futuro, si el impuesto se modifica solo deberemos cambiar ese valor en un punto de nuestro programa. Si usamos una variable, es posible que cambiemos el valor accidentalmente en algún lugar de nuestro programa, lo que provocaría un cálculo inadecuado.

      Para declarar una constante, podemos usar la siguiente sintaxis:

      const shark = "Sammy"
      fmt.Println(shark)
      

      Output

      Sammy

      Si intentamos modificar una constante después de que se declare, veremos un error en tiempo de compilación:

      Output

      cannot assign to shark

      Es posibe que las constantes no tengan el valor untyped. Esto puede ser útil al trabajar con números como los datos de tipo entero. Si la constante tiene el valor untyped, se convierte de forma explícita. esto no es posible en el caso de las constantes con el valor typed. Veamos la forma de usar constantes:

      package main
      
      import "fmt"
      
      const (
          year     = 365
          leapYear = int32(366)
      )
      
      func main() {
          hours := 24
          minutes := int32(60)
          fmt.Println(hours * year)    
          fmt.Println(minutes * year)   
          fmt.Println(minutes * leapYear)
      }
      

      Output

      8760 21900 21960

      Si declara una constante con un tipo, se tratará de ese tipo exacto. Aquí, cuando declaramos la constante leapYear la definimos como elemento del tipo de datos int32. Por lo tanto, es una constante typed. Esto significa que solo puede funcionar con tipos de datos int32. Declaramos la constante year sin tipo, por lo que se considera untyped. Debido a esto, puede utilizarla con cualquier tipo de datos de números enteros.

      Cuando se definió hours, infirió que era de tipo int porque no le asignamos de forma explícita un tipo, hours := 24. Cuando declaramos minutes, la declaramos de forma explícita como int32, minutes := int32(60).

      Ahora, revisaremos cada cálculo y la razón por la que funciona:

      hours * year
      

      En este caso, hours es int y years es untyped. Cuando el programa realiza la compilación, de forma explícita convierte years en int, lo cual permite que la operación de multiplicación tenga éxito.

      minutes * year
      

      En este caso, minutes es int32 y year es untyped. Cuando el programa realiza la compilación, de forma explícita convierte years en int32, lo cual permite que la operación de multiplicación tenga éxito.

      minutes * leapYear
      

      En este caso, minutes es int32 y leapYear es una constante typed de int32. En este caso, el compilador no deberá intervenir debido a que ambas variables ya son del mismo tipo.

      Si intentamos multiplicar dos tipos typed que no son compatibles, el programa no realizará la compilación:

      fmt.Println(hours * leapYear)
      

      Output

      invalid operation: hours * leapYear (mismatched types int and int32)

      En este caso, hours se infirió como int y leapYear se declaró explícitamente como int Debido a que Go es un lenguaje “typed”, int y un int32 no son compatibles para operaciones matemáticas. Para multiplicarlos, necesitaría convertir uno a int32 o int.

      Conclusión

      A lo largo de este tutorial, revisamos algunos de los casos de uso común de variables dentro de Go. Las variables son un componente importante de la programación y sirven como símbolos que reemplazan valores de tipos de datos que usamos en un programa.



      Source link