0
votes

Terraform v0.12.x, AWS provider

I'm trying to write a generic module that will either return an existing EBS snapshot of a passed-in EBS volume name, or take a snapshot of a passed-in EBS volume name. Either way it should return a snapshot id.

Here's the code to get an existing snapshot.

data "aws_ebs_snapshot" "snapshot" {
  most_recent = true
  filter {
    name   = "tag:Name"
    values = [var.ebs_id]
  }
  filter {
    name   = "status"
    values = ["completed"]
  }
}

output "snapshot_id" {
  value       = data.aws_ebs_snapshot.snapshot.id
  description = "Jenkins master snapshot id"
}

and here's the code to take a snapshot.

data "aws_ebs_volume" "ebs" {
  most_recent = true
  filter {
    name   = "tag:Name"
    values = [var.ebs_id]
  }
}

// Take a snapshot of the green EBS resource
resource "aws_ebs_snapshot" "snapshot" {
  volume_id = data.aws_ebs_volume.ebs.id
}

output "snapshot_id" {
  value       = aws_ebs_snapshot.snapshot.id
  description = "Jenkins master snapshot id"
}

Is it possible to do this? If so how? I know I can separate them into 2 separate modules, but humor me.

# try/catch block is of course pseudo-code
try {
  # Get an existing snapshot id
  data "aws_ebs_snapshot" "snapshot" {
    most_recent = true
    filter {
      name   = "tag:Name"
      values = [var.ebs_name]
    }
    filter {
      name   = "status"
      values = ["completed"]
    }
  }

  output "snapshot_id" {
    value       = data.aws_ebs_snapshot.snapshot.id
    description = "Jenkins master snapshot id"
  }
}
catch() {
  # Get the volume id and take a snapshot
  data "aws_ebs_volume" "ebs" {
    most_recent = true
    filter {
      name   = "tag:Name"
      values = [var.ebs_id]
    }
  }
   
  // Take a snapshot of the green EBS resource
  resource "aws_ebs_snapshot" "snapshot" {
    volume_id = data.aws_ebs_volume.ebs.id
  }

  output "snapshot_id" {
    value       = aws_ebs_snapshot.snapshot.id
    description = "Jenkins master snapshot id"
  }
}

I know try/catch blocks are not used this way in Terraform, so how can I achieve what I want?

2
No, it isn't. HCL is a declarative language (like JSON or YAML), not a real programming language with constructs like exception handling.Adrian
I updated my question. Without using try/catch, how can I achieve what I want?Chris F
Yes you can do this, and yes there is try/catch within Terraform, but the solution would not involve the try in this situation because it involves individual values, and not resources. Also, the code would look hacky for what you want, so a re-architecting is probably more ideal here.Matt Schuchard
@MattSchuchard please show me in an answer.Chris F

2 Answers

2
votes

At first glance you might think that you could check if the snapshot exists via the data source, and then use something like count on the resource to create one if the data source didn't return anything. Unfortunately that's not how Terraform works because the data source will throw an error if it can't find a match, causing Terraform to exit.

See the official response from HashiCorp here, when asked for the sort of capability you are looking for.

The sort of dynamic decision-making that is being requested here runs counter to Terraform's design goals, since it makes the configuration a description of what might possibly be rather than what is.

In general this sort of thing is handled better via AWS CLI scripts, or something like a Python/boto3 script, instead of Terraform.

2
votes

The situation you've described doesn't seem like one where it's necessary to make a dynamic decision based on the remote system, because you can tell entirely from the input variables whether the caller is specifying a snapshot id or a volume id:

variable "ebs_name" {
  type    = string
  default = null
}

variable "ebs_id" {
  type    = string
  default = null
}

data "aws_ebs_snapshot" "snapshot" {
  count = var.ebs_name != null ? 1 : 0

  most_recent = true

  filter {
    name   = "tag:Name"
    values = [var.ebs_name]
  }
  filter {
    name   = "status"
    values = ["completed"]
  }
}

data "aws_ebs_volume" "ebs" {
  count = var.ebs_id != null ? 1 : 0

  most_recent = true

  filter {
    name   = "tag:Name"
    values = [var.ebs_id]
  }
}
   
// Take a snapshot of the green EBS resource
resource "aws_ebs_snapshot" "snapshot" {
  count = var.ebs_id != null ? 1 : 0

  volume_id = data.aws_ebs_volume.ebs[count.index].id
}

output "snapshot_id" {
  # Return either the generated snapshot or the given
  # snapshot. If the caller specified both for some
  # reason then the generated snapshot takes priority.
  # This will produce an error if neither var.ebs_name
  # nor var.ebs_id is set, because the result will have
  # no elements.
  value = concat(
    aws_ebs_snapshot.snapshot[*].id,
    data.aws_ebs_snapshot.snapshot[*].id,
  )[0]
  description = "Jenkins master snapshot id"
}

For completeness in case someone else finds this answer in future I want to note that the Module Composition guide suggests just directly writing out the straightforward read or create code for each case rather than making dynamic decisions in cases like these, but I showed the dynamic example above because you suggested (by reference to the possibility of using two modules to address this) that you'd already considered and decided against using a composition style.