Conditional AWS EC2 resources in Terraform
⋅ 3 min readOne 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
- AWS AMI with AWS CLI installed.
- 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
}
}