When to use ebs_block_device? - terraform

I want to create an EC2 instance with Terraform. This instance should have some EBS.
In the documentation I read that Terraform provides two ways to create an EBS:
ebs_block_device
aws_ebs_volume with aws_volume_attachment
I want to know, when should I use ebs_block_device?
Documentation
Unfortunately the documentation isn't that clear (at least for me) about:
When to use ebs_block_device?
How is the exact actual behavior?
See Resource: aws_instance:
ebs_block_device - (Optional) One or more configuration blocks with additional EBS block devices to attach to the instance. Block device configurations only apply on resource creation. See Block Devices below for details on attributes and drift detection. When accessing this as an attribute reference, it is a set of objects.
and
Currently, changes to the ebs_block_device configuration of existing resources cannot be automatically detected by Terraform. To manage changes and attachments of an EBS block to an instance, use the aws_ebs_volume and aws_volume_attachment resources instead. If you use ebs_block_device on an aws_instance, Terraform will assume management over the full set of non-root EBS block devices for the instance, treating additional block devices as drift. For this reason, ebs_block_device cannot be mixed with external aws_ebs_volume and aws_volume_attachment resources for a given instance.
Research
I read:
No change when modifying aws_instance.ebs_block_device.volume_size, which says that Terraform doesn't show any changes with plan/apply and doesn't change anything in AWS, although changes were made..
AWS "ebs_block_device.0.volume_id": this field cannot be set, which says that Terraform shows an error while running plan.
Ebs_block_device forcing replacement every terraform apply, which says that Terraform replaces all EBS.
aws_instance dynamic ebs_block_device forces replacement, which says that Terraform replaces all EBS, although no changes were made.
adding ebs_block_device to existing aws_instance forces unneccessary replacement, which says that Terraform replaces the whole EC2 instance with all EBS.
aws_instance dynamic ebs_block_device forces replacement, which says that Terraform replaces the whole EC2 instance with all EBS, although no changes were made.
I know that the issues are about different versions of Terraform and Terraform AWS provider and some issues are already fixed, but what is the actual intended behavoir?
In almost all issues the workaround/recommendation is to use aws_ebs_volume with aws_volume_attachment instead of ebs_block_device.
Question
When should I use ebs_block_device? What is the use case for this feature?

When should I use ebs_block_device?
When you need another volume other than the root volume because
Unlike the data stored on a local instance store (which persists only
as long as that instance is alive), data stored on an Amazon EBS
volume can persist independently of the life of the instance.
When you launch an instance, the root device volume contains the image used to boot the instance.
Instances that use Amazon EBS for the root device automatically have
an Amazon EBS volume attached. When you launch an Amazon EBS-backed
instance, we create an Amazon EBS volume for each Amazon EBS snapshot
referenced by the AMI you use
Here's an example of an EC2 with additional EBS volume.
provider "aws" {
region = "eu-central-1"
}
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
ebs_block_device {
device_name = "/dev/sda1"
volume_size = 8
volume_type = "gp3"
throughput = 125
delete_on_termination = false
}
}
Note: delete_on_termination for root_block_device is set to true by default.
You can read more on AWS block device mapping here.
EDIT:
aws_volume_attachment is used when you want to attach an existing EBS volume to an EC2 instance. It helps to manage the relationship between the volume and the instance, and ensure that the volume is attached to the desired instance in the desired state.
Here's an example usage:
resource "aws_volume_attachment" "ebs_att" {
device_name = "/dev/sdh"
volume_id = aws_ebs_volume.example.id
instance_id = aws_instance.web.id
}
resource "aws_instance" "web" {
ami = "ami-21f78e11"
availability_zone = "us-west-2a"
instance_type = "t2.micro"
tags = {
Name = "HelloWorld"
}
}
resource "aws_ebs_volume" "example" {
availability_zone = "us-west-2a"
size = 1
}
and the ebs_block_device is used when you want to create a new EBS volume and attach it to an EC2 instance at the same time as the instance is being created.
NOTE:
If you use ebs_block_device on an aws_instance, Terraform will assume
management over the full set of non-root EBS block devices for the
instance, and treats additional block devices as drift. For this
reason, ebs_block_device cannot be mixed with external aws_ebs_volume + aws_volume_attachment resources for a given instance.
Source

I strongly suggest using only resource aws_ebs_volume. When creating an instance, the root block will be created automatically. For extra EBS storage, you will want Terraform to manage them independently.
Why?
Basically you have 2 choices to create an instance with 1 extra disk:
resource "aws_instance" "instance" {
ami = "ami-xxxx"
instance_type = "t4g.micro"
#... other arguments ...
ebs_block_device {
volume_size = 10
volume_type = "gp3"
#... other arguments ...
}
}
OR
resource "aws_instance" "instance" {
ami = "ami-xxxx"
instance_type = "t4g.micro"
#... other arguments ...
}
resource "aws_ebs_volume" "volume" {
size = 10
type = "gp3"
}
resource "aws_volume_attachment" "attachment" {
volume_id = aws_ebs_volume.volume.id
instance_id = aws_instance.instance.id
device_name = "/dev/sdb"
}
The first method is more compact, creates fewer Terraform resource and makes terraform import easier. But if you need to recreate your instance what will happen? Terraform will remove the instance, and redeploy it from scratch with new volumes. If you use the argument delete_on_termination to false, the volumes will still exist but they won't be attached to your instance.
In the contrary, when using a dedicated resource, the instance recreation will recreate the attachements (because the instance id changes) and then, reattach your existing volumes to your instance, which is what we need 90% of the time.
Also, if at some point you need to manipulate your volume in the Terraform state (terraform state commands), it will be much easier to do it on the individual resource aws_ebs_volume.
Finally, at some point in your Terraform journey, you will want to industrialize your code by adding loops, variables and so on. A common use case is to make the number of volumes variables: you provide a list of volumes and Terraform create 1, 2 or 10 volumes according to this list.
And for this you have also have 2 options :
variable "my_volume" { map(any) }
my_volume = {
"/dev/sdb": {
"size": 10
"type": "gp3"
}
}
resource "aws_instance" "instance" {
ami = "ami-xxxx"
instance_type = "t4g.micro"
#... other arguments ...
dynamic "ebs_block_device" {
for_each = var.my_volumes
content {
volume_size = ebs_block_device.value["size"]
volume_type = ebs_block_device.value["type"]
#... other arguments ...
}
}
}
OR
resource "aws_instance" "instance" {
ami = "ami-xxxx"
instance_type = "t4g.micro"
#... other arguments ...
}
resource "aws_ebs_volume" "volume" {
for_each = var.
size = 10
type = "gp3"
}
resource "aws_volume_attachment" "attachment" {
volume_id = aws_ebs_volume.volume.id
instance_id = aws_instance.instance.id
device_name = "/dev/sdb"
}

Related

How to import existing EC2 with EBS block device?

I want to use Terraform for my existing AWS infrastructure. Therefore, I imported an existing EC2 instance with EBS block devices. Then I wrote the Terraform code. But Terraform wants to replace all EBS block devices. Is there any way to not replace the EBS block devices?
Import
I imported my AWS EC2 instance as described in Import:
Import
Instances can be imported using the id, e.g.,
$ terraform import aws_instance.web i-12345678
Code
After I imported the state file, I wrote my Terraform code.
This is the part for the EBS block device:
ebs_block_device {
device_name = "/dev/sdf"
volume_type = "gp2"
volume_size = 20
tags = {
Name = "my-test-docker"
}
}
Plan
After I wrote the code, I run the plan command, see Command: plan.
This is the part for the EBS block device:
- ebs_block_device { # forces replacement
- delete_on_termination = false -> null
- device_name = "/dev/sdf" -> null
- encrypted = false -> null
- iops = 100 -> null
- snapshot_id = "snap-0e22b434e3106fd51" -> null
- tags = {
- "Name" = "my-test-docker"
} -> null
- throughput = 0 -> null
- volume_id = "vol-065138961fea23bf4" -> null
- volume_size = 20 -> null
- volume_type = "gp2" -> null
}
+ ebs_block_device { # forces replacement
+ delete_on_termination = true
+ device_name = "/dev/sdf"
+ encrypted = (known after apply)
+ iops = (known after apply)
+ kms_key_id = (known after apply)
+ snapshot_id = (known after apply)
+ tags = {
+ "Name" = "my-test-docker"
}
+ throughput = (known after apply)
+ volume_id = (known after apply)
+ volume_size = 20
+ volume_type = "gp2"
}
Research
I tried to set the volume_id in my code, but Terraform didn't allow to set this property.
Actually, the EBS block device shouldn't be replaced after creation, see ebs_block_device:
ebs_block_device - (Optional) One or more configuration blocks with additional EBS block devices to attach to the instance. Block device configurations only apply on resource creation. See Block Devices below for details on attributes and drift detection. When accessing this as an attribute reference, it is a set of objects.
Background
One of the reasons for using ebs_block_device instead of aws_ebs_volume with aws_volume_attachment was aws_volume_attachment Error waiting for Volume .
Another reason was
Ebs volumes are not attached and available at boot time for userdata script to mount them.
AWS Attach/Format device with CLOUDINIT
fs_setup/disk_setup: option to wait for the device to exist before continuing
ebs_block_device is still working fine for new (not imported) resources.
The formating and mounting part of my cloud-init user data script:
fs_setup:
- label: docker
filesystem: ext4
device: /dev/xvdf
overwrite: false
mounts:
- [ "/dev/xvdf", "/mnt/docker-data", "auto", "defaults,nofail", "0", "2" ]
Question
Is there any way to give Terraform a hint to match EBS block device in code with EBS block devices in the state file?
Rather than using ebs_block_device, you should be using aws_ebs_volume and aws_volume_attachment resources to import non-root EBS blocks, according to Terraform docs:
Currently, changes to the ebs_block_device configuration of existing resources cannot be automatically detected by Terraform. To manage changes and attachments of an EBS block to an instance, use the aws_ebs_volume and aws_volume_attachment resources instead. If you use ebs_block_device on an aws_instance, Terraform will assume management over the full set of non-root EBS block devices for the instance, treating additional block devices as drift.

terraform: ec2 machine change does not trigged dns change

I've got simple code for many EC2 machines setup. It does not update DNS record after machine is changed. I need to run it second time - only then DNS will be changed. What am I doing wrong?
resource "aws_instance" "ec2" {
for_each = var.instances
ami = each.value.ami
instance_type = each.value.type
ebs_optimized = true
}
resource "cloudflare_record" "web" {
for_each = var.instances
zone_id = var.cf_zone_id
name = "${each.key}.${var.env}.aws.${var.domain}."
value = aws_instance.ec2[each.key].public_ip
type = "A"
ttl = 1
depends_on = [
aws_instance.ec2
]
}
So, one thing about Terraform is that it provisions your infrastructure, but whatever happens with your infrastructure between your latest apply and now won't be reflected by the Terraform state file. If there is indeed a change in your infrastructure, then you would see it in the next Terraform plan. There is nothing wrong with what you're doing, only that you might have understood Terraform wrongly. There is a neat concept called "Immutable Infrastructure". Read it up in Hashicorp's blog: https://www.hashicorp.com/resources/what-is-mutable-vs-immutable-infrastructure

Creating a "random" instance with Terraform - autocreate valid configurations

I'm new to Terraform and like to create "random" instances.
Some settings like OS, setup script ... will stay the same. Mostly the region/zone would change.
How can I do that?
It seems Terraform already knows about which combinations are valid. For example with AWS EC2 or lightsail it will complain if you choose a wrong combination. I guess this will reduce the amount of work. I'm wondering though if this is valid for each provider.
How could you automatically create a valid configuration, with only the region or zone changing each time Terraform runs?
Edit: Config looks like:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
}
}
}
provider "aws" {
# profile = "default"
# region = "us-west-2"
accesskey = ...
secretkey = ...
}
resource "aws_instance" "example" {
ami = "ami-830c94e3"
instance_type = "t2.micro"
}
Using AWS as an example, aws_instance has two required parameters: ami and instance_type.
Thus to create an instance, you need to provide both of them:
resource "aws_instance" "my" {
ami = "ami-02354e95b39ca8dec"
instance_type = "t2.micro"
}
Everything else will be deduced or set to their default values. In terms of availability zones and subnets, if not explicitly specified, they will be chosen "randomly" (AWS decides how to place them, so if fact they can be all in one AZ).
Thus, to create 3 instances in different subnets and AZs you can do simply:
provider "aws" {
region = "us-east-1"
}
data "aws_ami" "al2_ami" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm*"]
}
}
resource "aws_instance" "my" {
count = 3
ami = data.aws_ami.al2_ami.id
instance_type = "t2.micro"
}
A declarative system like Terraform unfortunately isn't very friendly to randomness, because it expects the system to converge on a desired state, but random configuration would mean that the desired state would change on each action and thus it would never converge. Where possible I would recommend using "randomization" or "distribution" mechanisms built in to your cloud provider, such as AWS autoscaling over multiple subnets.
However, to be pragmatic Terraform does have a random provider, which represents the generation of random numbers as a funny sort of Terraform resource so that the random results can be preserved from one run to the next, in the same way as Terraform remembers the ID of an EC2 instance from one run to the next.
The random_shuffle resource can be useful for this sort of "choose any one (or N) of these options" situation.
Taking your example of randomly choosing AWS regions and availability zones, the first step would be to enumerate all of the options your random choice can choose from:
locals {
possible_regions = toset([
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
])
possible_availability_zones = tomap({
us-east-1 = toset(["a", "b", "e"])
us-east-2 = toset(["a", "c")
us-west-1 = toset(["a", "b"])
us-west-2 = toset(["b", "c"])
})
}
You can then pass these inputs into random_shuffle resources to select, for example, one region and then two availability zones from that region:
resource "random_shuffle" "region" {
input = local.possible_regions
result_count = 1
}
resource "random_shuffle" "availability_zones" {
input = local.possible_availability_zones[local.chosen_region]
result_count = 2
}
locals {
local.chosen_region = random_shuffle.region.result[0]
local.chosen_availability_zones = random_shuffle.availability_zones.result
}
You can then use local.chosen_region and local.chosen_availability_zones elsewhere in your configuration.
However, there is one important catch with randomly selecting regions in particular: the AWS provider is designed to require a region, because each AWS region is an entirely distinct set of endpoints, and so the provider won't be able to successfully configure itself if the region isn't known until the apply step, as would be the case if you wrote region = local.chosen_region in the provider configuration.
To work around this will require using the exceptional-use-only -target option to terraform apply, to direct Terraform to first focus only on generating the random region, and ignore everything else until that has succeeded:
# First apply with just the random region targeted
terraform apply -target=random_shuffle.region
# After that succeeds, run apply again normally to
# create everything else.
terraform apply

Restore an instance from snapshot without recreating using terraform

Use case: Intent is to revert an instance with already taken AWS snapshot of the volume the instance is currently running on.
For this, i thought why not use "terraform import" to bring the existing state of the instance and then modify HCL config file to replace ONLY volume. By behaviour it expect to create AMI from snapshot and then spawn instance from AMI. It works but it destroy's instance and then recreate instance.
I don't expect instance recreation, rather why not just do below by using terraform:
stop instance
detach current volume
create volume from provided snapshot to revert to
attach created volume to instance
power on instance.
How to achieve above?
Current config file which i am trying after importing an instance:
provider "aws" {
…..
…..
…..
}
resource "aws_ami" "example5554" {
name = "example5554"
virtualization_type = "hvm"
root_device_name = "/dev/sda1"
ebs_block_device {
snapshot_id = "snap-xxxxxxxxxxxxx”
device_name = "/dev/sda1"
volume_type = "gp2"
}
}
resource "aws_instance" "arstest1new" {
ami = "${aws_ami.example5554.id}"
instance_type = "m4.large"
}

terrafrom aws_instance subnet_id - Error launching source instance: Unsupported: The requested configuration is currently not supported

I am trying to build a VPC and subnet within that VPC. Thirdly I am trying to create an AWS instances within that subnet. Sounds simple, but the subnet_id parameter seems to break the terraform 'apply' (plan works just fine). Am I missing something?
Extract from main.tf
resource "aws_vpc" "poc-vpc" {
cidr_block = "10.0.0.0/16"
instance_tenancy = "dedicated"
enable_dns_hostnames = "true"
}
resource "aws_subnet" "poc-subnet" {
vpc_id = "${aws_vpc.poc-vpc.id}"
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = "true"
availability_zone = "${var.availability_zone}"
}
resource "aws_instance" "POC-Instance" {
ami = "${lookup(var.amis, var.region)}"
instance_type = "${var.instance_type}"
availability_zone = "${var.availability_zone}"
associate_public_ip_address = true
key_name = "Pipeline-POC-Key-Pair"
vpc_security_group_ids = ["${aws_security_group.poc-sec-group.id}"]
subnet_id = "${aws_subnet.poc-subnet.id}"
}
If I remove the subnet_id the 'apply' works, but the instance is created in my default VPC. This is not the aim.
Any help would be appreciated. I am a newbie to terraform so please be gentle.
I worked this out and wanted to post this up to hopefully saves others some time.
The issue is the conflict of subnet_id in the aws_instance provisioner and instance_tennancy in the aws_vpc provisioner. Remove instance tenancy and all is fixed (or set to default)
The error message is meaningless. I've asked whether this can be improved.
It's also possible that there is a conflict in your other configuration. I encountered the same Unsupported error because of different reason.
I used AMI ami-0ba5dfee72d5bb9a1 that I found from https://cloud-images.ubuntu.com/locator/ec2/
I just choose anything that is in the same region as my VPC.
Apparently that AMI can only support a* instance type and don't support t* or m* instance type.
So I think double check that:
Your AMI is compatible with your instance type.
Your AMI is in the same region as your VPC or subnet.
There is no other conflicting configuration.
The problem is terraform example that you likely copied is WRONG.
The problem is vpc instance tenancy property in most cases.
Change the VPC instance_tenancy = "dedicated" to instance_tenancy = "default".
It should work for any ec2 instance type.
The reason is that dedicated instances are supported only for m5.large or bigger instances so VPC and ec2 instance types are in conflict if you are actually creating smaller instances like t3.small or t2.m. You can micro etc. You can look at the dedicated instances here.
https://aws.amazon.com/ec2/pricing/dedicated-instances/
if you want to create your own VPC network and not use the default, then you need to also create a route table and internet gateway so you can have access to the created ec2. You will need to also add the follow config to create a full VPC network with ec2 instances that is accessible with the public IP you assigned
# Internet GW
resource "aws_internet_gateway" "main-gw" {
vpc_id = "${aws_vpc.poc-vpc.id}"
tags {
Name = "poc-vpc"
}
}
# route tables
resource "aws_route_table" "main-public" {
vpc_id = "${aws_vpc.poc-vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.main-gw.id}"
}
tags {
Name = "main route"
}
}
# route associations public
resource "aws_route_table_association" "main-public-1-a" {
subnet_id = "${aws_subnet.poc-subnet.id}"
route_table_id = "${aws_route_table.main-public.id}"
}

Resources