Conditional AWS EC2 resources in Terraform

 ⋅ 3 min read

Bringing up conditional AWS EC2 resources using Terraform

One of the things that every Terraform developer will end up having to do at some point is conditionally bringing up on-demand or spot instances in their AWS infrastructure, and I'm going to show you how to do that.

The challenge

The main challenge in doing this is not the bring up itself, but propogating your EC2 tags down to the instance. This is because to bring up a spot instance, you'll need to use aws_spot_instance_request, which takes tags as an argument but applies them to the spot request itself and not the instance.

Settings tags on the spot instance

The solution involves using count to conditionally bring up the instance, creating an IAM policy that allows the instance to create tags and then running remote-exec to SSH into the instance to create its own tags.

Prerequisites

  1. AWS AMI with AWS CLI installed.
  2. Terraform >= 0.12.
variables.tf
variable "instance_type" {
  type        = string
  description = "Instance type for your AWS instance"
}

variable "ami_id" {
  type        = string
  description = "AMI ID for your AWS instance"
}

variable "ssh_key" {
  type        = string
  description = "EC2 SSH keypair for your AWS instance"
}

variable "instance_lifecycle" {
  type        = string
  description = "Lifecyle of your AWS instance. Example: ondemand, spot"
  default     = "spot"
}
ec2.tf
resource "aws_instance" "ec2_ondemand" {
  count                       = var.instance_lifecycle == "ondemand" ? 1 : 0
  ami                         = var.ami_id
  instance_type               = var.instance_type
 
  tags = {
    Name        = "my-instance"
    Lifecycle   = var.instance_lifecycle
  }
}

resource "aws_spot_instance_request" "ec2_spot" {
  count                       = var.instance_lifecycle == "spot" ? 1 : 0
  wait_for_fulfillment        = true
  spot_type                   = "one-time"
  ami                         = var.ami_id
  instance_type               = var.instance_type

  tags = {
    Name        = "my-instance"
    Lifecycle   = var.instance_lifecycle
  }

  # Workaround to make sure the spot request tags are propogated down to the instance itself.
  provisioner "remote-exec" {
    connection {
      user        = "ubuntu" # This might change for your OS of choice.
      host        = self.private_ip
      private_key = file("~/.ssh/${var.ssh_key}.pem")
    }

    inline = [
      join("", formatlist("aws ec2 create-tags --resources ${self.spot_instance_id} --tags Key=\"%s\",Value=\"%s\" --region=${var.region}; ", keys(self.tags), values(self.tags)))
    ]
  }
}

# If you're going to refer to the created instance anywhere else, you should now use `data.aws_instance.ec2_instance`
data "aws_instance" "ec2_instance" {
  depends_on = [aws_instance.ec2_ondemand, aws_spot_instance_request.ec2_spot]
  filter {
    name   = "tag:Name"
    values = ["my-instance"]
  }
  filter {
    name   = "tag:Lifecycle"
    values = [var.instance_lifecycle]
  }
}
iam.tf
resource "aws_iam_role" "iam_role" {
  name = "my-iam-role"

  lifecycle {
    create_before_destroy = true
  }

  force_detach_policies = true

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF

}

resource "aws_iam_policy" "iam_role_policy_ec2tags" {
  name        = "my-instance-create-tags-policy"
  description = "Allow EC2 instances to create Tags for themselves"

  # The policy is for '*' rather than the own instance because EC2 doesn't allow us to filter by self.
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
          "Action": "ec2:CreateTags",
          "Effect": "Allow",
          "Sid": "AllowEC2InstanceCreateTags",
          "Resource": "*"
        }
    ]
}
EOF

}

resource "aws_iam_policy_attachment" "iam_role_policy_ec2tags" {
  name       = "iam_role_policy_ec2tags"
  roles      = [aws_iam_role.iam_role.name]
  policy_arn = aws_iam_policy.iam_role_policy_ec2tags.arn
  lifecycle {
    create_before_destroy = true
  }
}