DEV Community

Cover image for Introduction to Terraform
Ivan Cvitkovic
Ivan Cvitkovic

Posted on

Introduction to Terraform

Infrastructure as Code

Almost every software engineer has experienced that provisioning servers, operating systems, storage, and other infrastructure components is a demanding and time-consuming process. Provisioning is usually done manually, and when we take into account that in addition to the development environment, the staging and production workloads should also be deployed, we come to a large amount of work. In such a large task, human error can easily occur, which means that only a small configuration error can cause problems and disable the proper operation of the application.

To avoid potential outages we come to the concept of Infrastructure as Code. IaC is the managing and provisioning of infrastructure through code instead of through manual processes. Using the infrastructure as code allows configuration files on virtual machines, disks, network and other components to be stored together with the application code within source control management system. Storing them within the SCM allows you to version and track changes over time. This approach helps you to avoid undocumented, ad-hoc configuration changes.

Deploying your infrastructure as code also means that you can divide your infrastructure into modular components that can then be combined in different ways through automation. Automating infrastructure provisioning means that there is no need for manual interventions and thus eliminates the human error factor. There are several IaC tools and one of the most popular is Terraform.

Terraform basic concepts

Terraform is HashiCorp's infrastructure as code tool. It lets you define resources and infrastructure in human-readable, declarative configuration files, and manages your infrastructure's lifecycle. It lets you define both cloud and on-premise resources trough configuration language called HCL, which looks like simplified JSON (JavaScript Object Notation). Terraform can manage low-level components like compute, storage, and networking resources, as well as high-level components like DNS entries and SaaS features.

But how does it all actually work under the hood? Terraform creates resources on cloud platforms and on-premises based on their application programming interfaces (APIs), while communication with APIs takes place through providers. Providers are plugins that interact with various platforms and manage their resources, serving as a logical abstraction of an upstream API.

Terraform provider

Together with the community, Terraform has written more than 1800 providers, including those for major cloud providers like Azure, AWS and Google Cloud Platform. There are also providers for Docker, Kubernetes, Helm and more. You can find the full list at the Terraform Registry.

As in everything else, the best way to learn Terraform is through practice, but keep in mind that creating resources on cloud providers can cost you money. If you no longer need certain resources after exercise, simply delete them and clean up your environment. Also, for practice you can use various free tiers on platforms like Microsoft Azure and Amazon Web Services or some provider to create resources locally on your development machine. There is a lot of beginner friendly examples, like this one, on HashiCorp Learn website which can help you get started.

Demo: Terraform Docker provider

The complete source code for this demo is on GitHub, and you can find the repository here.

We'll start with the main.tf file. Inside that file is placed terraform {} block which contains all the settings, including the required providers Terraform will use to provision your infrastructure components. Each provider consists of source attribute which defines where is provider actually located. By default it will be installed from the Terraform Registry, but optionally you can pass hostname parameter. In this example configuration, the docker provider's source is defined as kreuzwerker/docker.

The version attribute is optional, but it's highly recommended to use it so that Terraform does not install a version of the provider that does not work with your configuration. Each module should at least declare the minimum provider version it is known to work with, using the >= version constraint syntax. If the version is not passed, Terraform will download and install the latest one. Although this is not the case here, keep in mind that you can define multiple different providers in your configuration block and use them together.

terraform {
  required_providers {
    docker = {
      source = "kreuzwerker/docker"
    version = ">= 2.13.0" }
  }
}
Enter fullscreen mode Exit fullscreen mode

Under the provider block we don't have much stuff, it just configures the specified provider, in this case docker.

The resource section is the most important part of every Terraform project. Each resource block describes one or more infrastructure components, which can be a physical or virtual object. Before the configuration block there are two parameters that define the resource type and its name. In this case we declare the Docker image and Docker container where both resources are named nginx.

resource "docker_image" "nginx" {
  name         = "nginx:latest"
  keep_locally = false
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.latest
  name  = var.container_name
  ports {
    internal = 80
    external = 8080
  }
}
Enter fullscreen mode Exit fullscreen mode

In the docker_container resource, under image parameter we are referencing previously declared docker_image. In the ports section we publish local port 80, which is the default for Nginx, to port 8080 on our local machine. For the container name, instead of some hard-coded string, we decided to use value assigned to a variable container_name.

You can name your configuration files however you want because Terraform loads all files in the current directory ending in .tf, but we choose to follow the naming convention and name the file variables.tf. Variables file is quite simple, each variable is declared by keyword and name. Inside the configuration block we define variable type, and can optionally pass default value or sensitive flag which limits Terraform UI output when the variable is used in configuration.

variable "container_name" {
  type = string
}
Enter fullscreen mode Exit fullscreen mode

When variables are declared in your configuration, they can be set in a number of ways. They can be passed individually with the -var command line option, as environment variables in TF_VAR_ format or inside variable definitions files (.tfvars). We used the latter option to set a name for our Docker container:

container_name = "nginx-example"
Enter fullscreen mode Exit fullscreen mode

Now we are ready to deploy our infrastructure, and to do that we need to initialize a working directory containing Terraform configuration files with terraform init command. This command performs several different initialization steps in order to prepare the current working directory for use with Terraform which include downloading and installating the provider.

After the initialization process is complete we can create a new execution plan and then apply it. The easiest way to achieve this is by using terraform apply command. Execution plan is displayed in the command line and user is prompted to apply that plan. You should get similar output:

$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # docker_container.nginx will be created
  + resource "docker_container" "nginx" {
      + attach           = false
      + bridge           = (known after apply)
      + command          = (known after apply)
      + container_logs   = (known after apply)
      + entrypoint       = (known after apply)
      + env              = (known after apply)
      + exit_code        = (known after apply)
      + gateway          = (known after apply)
      + hostname         = (known after apply)
      + id               = (known after apply)
      + image            = (known after apply)
      + init             = (known after apply)
      + ip_address       = (known after apply)
      + ip_prefix_length = (known after apply)
      + ipc_mode         = (known after apply)
      + log_driver       = (known after apply)
      + logs             = false
      + must_run         = true
      + name             = "nginx-example"
      + network_data     = (known after apply)
      + read_only        = false
      + remove_volumes   = true
      + restart          = "no"
      + rm               = false
      + security_opts    = (known after apply)
      + shm_size         = (known after apply)
      + start            = true
      + stdin_open       = false
      + tty              = false

      + healthcheck {
          + interval     = (known after apply)
          + retries      = (known after apply)
          + start_period = (known after apply)
          + test         = (known after apply)
          + timeout      = (known after apply)
        }


Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

docker_image.nginx: Creating...
docker_image.nginx: Still creating... [10s elapsed]
docker_image.nginx: Still creating... [20s elapsed]
docker_image.nginx: Still creating... [30s elapsed]
docker_image.nginx: Still creating... [40s elapsed]
docker_image.nginx: Still creating... [50s elapsed]
docker_image.nginx: Still creating... [1m0s elapsed]
docker_image.nginx: Still creating... [1m10s elapsed]
docker_image.nginx: Still creating... [1m20s elapsed]
docker_image.nginx: Creation complete after 1m23s [id=sha256:c316d5a335a5cf324b0dc83b3da82d7608724769f6454f6d9a621f3ec2534a5anginx:latest]
docker_container.nginx: Creating...
docker_container.nginx: Creation complete after 1s [id=0e3fda53befd697a51a3b15a615c236c62f65469f4a02450c1899289f998f128]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Enter fullscreen mode Exit fullscreen mode

Now, navigate to your web browser and check the localhost:8080, you should see Nginx web server up and running.

Nginx Browser

When you are done with the exercise, resources can be deleted with a simple command terraform destroy.

Top comments (1)

Collapse
 
cvitaa11 profile image
Ivan Cvitkovic

If you are interested in destroying resources with Terraform check out this -> spacelift.io/blog/how-to-destroy-t... 😊