1. Introduction

In this tutorial, we’ll look at the ways to skip resource creation if it already exists in some form. We’re also going to review the challenges of this problem and reasons why we might consider other options. Let’s first understand why this might not be a good idea to solve this problem in the first case.

2. IaaC Tools Purpose

As we all know, IaaC tools, such as Terraform, are there to represent the current state of infrastructure. This is the literal purpose of any IaaC tool – to represent the infrastructure as a code that can be versioned, patched, reviewed, etc.

If we’re using any IaaC tool, we ideally want to manage almost everything with it. That is mostly due to the benefits listed above but also because we do not want to end up in a state where half of the infrastructure is managed by an IaaC tool and the other half is not. In this case, managing infrastructure becomes challenging and cumbersome.

However, it is fine if we’re just in the process of migration to any IaaC tool. In this case, it is fine to have two security groups in the cloud, for example, where one is managed by Terraform and another is still managed manually.

3. Importing as an Alternative

Therefore, this problem is worth solving only in case we’re in the process of migration. Even in this case, skipping resource creation if it exists does not make much sense because of the reasons above. Skipping resource creation because of any kind of clash, not just by resource name, means that this offending resource is actually not managed by Terraform but by something else, possibly even manually.

So, what we really want is to manage a particular resource type with a particular name via Terraform. Let’s assume that we have an AWS VPC, which was initially provided manually. And we don’t want to skip creation if this VPC already exists, but rather make this VPC managed by Terraform.

3.1. Manual Import

So, we want to manage a particular resource type with a particular name via Terraform. To do this, we can and should utilize Terraform import functionality. This feature is available to us via the Terraform import command, and it does precisely what it sounds like—it imports resources to Terraform for it to manage them thereafter.

For this feature to work, we need to do two things. First, we need to identify the resource we want to import and its ID. This can most probably be done via the management console. Second, we need to write our own resource block configuration for the imported resource manually.

Here is how our own VPC resource configuration might look like in Terraform:

resource "aws_vpc" "main" {
  cidr_block       = "10.0.0.0/16"
  instance_tenancy = "default"

  tags = {
    Name = "main"
  }
}

We also need to import the resource into an actual Terraform state file. This is obvious since Terraform has no idea that this configuration is supposed to represent an already-existent object in the cloud, not a new one. Let’s assume, that our VPC in the AWS cloud has an ID equal to i-abcd12345. In this case, the import command should look like this:

terraform import aws_vpc.main i-abcd12345

Once done, Terraform becomes aware of the resource’s existence. This practically means that aws_vpc.main is now tracking the remote VPC, initially managed outside Terraform.

3.2. Generation of a Resource Block

We need to be very careful when writing the configuration. We need to ensure the newly created configuration matches the resource configuration in the cloud. If the configuration of the resource in the Terraform code doesn’t match the configuration of the resource in the cloud, we’ll have a problem. Terraform would assume that the resource in the cloud is misconfigured and try to alter it to comply with the code configuration.

But it is not very appealing to have us write this configuration. It is not only tedious but also error-prone. Therefore, starting from Terraform 1.5, we have import blocks (do not confuse them with the Terraform import command we’ve just talked about) with an automatic configuration generation feature.

The way import blocks work is very similar to the way the typical terraform import command works. However, when using import blocks, we can ask Terraform to generate an HCL resource configuration. However, it requires two prerequisites: the import block itself, which we as developers write, and an empty file where Terraform would dump the resource configuration.

The import block itself is fairly small:

import {
  to = aws_vpc.main
  id = "i-abcd12345"
}

We basically tell Terraform to import a cloud resource with ID i-abcd12345 to the HCL resource that does not exist yet but would be created by Terraform with the local name aws_vpc.main. It’s that simple. And then we just run this command:

terraform plan -generate-config-out=generated_vpc.tf

Here, Terraform’s resource configuration will be available in the generated_vpc.tf file. After the resource is imported and the job is done, we may optionally remove the import block and put the resource configuration into the file that we want (for example, the main.tf, as it is conventionally named).

4. Workarounds

However, if we still don’t want to manage the cloud resource by Terraform, and we would want to resolve a clash between Terraform resource and another one by name no matter what, we have some options. But again, we’d better have a solid justification for what we’re doing.

4.1. Local-exec Provisioner

The first thing we can try is to write our own local-exec provisioner with the on_failure flag set to fail. This provisioner would check the existence of the resource via the cloud CLI tool (aws CLI tool in our example). If it found one, then we will just fail the provisioner with a non-zero status code:

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    "env" = "dev"
  }

  provisioner "local-exec" {
    command = var.offending_vpc_id == null ? "true" : "! aws ec2 wait vpc-exists --vpc-ids ${var.offending_vpc_id} --cli-read-timeout 60"
    on_failure = fail
  }
}

variable "offending_vpc_id" {
  default = "vpc-07dfb6a9d39845d81"
  nullable = true
  type = string
}

This approach would work, but it has some drawbacks. First, we need to ensure the cloud CLI is installed on the build agent that would execute the shell command. Another issue is that the provisioner usage itself is a last resort and is generally a bad practice because it violates the IaaC overall. This would nevertheless work, although we check the VPC existence by ID, not by its name, but here it is just a simple example for illustration purposes.

4.2. Other Workarounds

There are some other options, like using a custom variable to determine the count of the resource. In this case, if we have a name clash or any other problem – we can just set the appropriate value of the variable to disable the resource creation like this:

variable "create_new_vpc" {
  type = bool
  default = false
}

resource "aws_vpc" "main" {

  count = var.create_new_vpc ? 1 : 0

  cidr_block = "10.0.0.0/16"

  tags = {
    "env" = "dev"
  }
}

In this case, if we have the variable create_new_vpc value equal to false, then the resource creation would be omitted. But this solution is also, of course, not flawless. We need to know the existence of the name clash in advance to set the appropriate value for the variable. This solution also would potentially create a resource that would be accessed with square bracket notation, as an array. This is due to the usage of the count meta-argument.

5. Conclusion

In this article, we’ve discussed the problem of skipping the resource creation if it already exists in Terraform. If we have this problem, we’re either in the process of migration to a given IaaC tool such as Terraform or we’re really doing something wrong. In either case, there are some options, but we should always understand the root cause of the problem. Therefore, importing the resource into Terraform would be the best decision to make.