Combine two simple objects to a list - terraform

I was wondering if anyone could suggest a trick to do this. Imagine I have a terraform code that retrieves the latest version of multiple AMI.
data "aws_ami" "amzn" {
most_recent = true
owners = ["amazon"]
filters...
}
data "aws_ami" "centos" {
most_recent = true
owners = ["12345678"]
filters...
}
What I would like to have is a list with both AWS AMi. The purpose is to choose between the two when I create an EC2 instance.
resource "aws_instance" "EC2" {
count = 100
ami = choose from list
instance_type = "t2.micro"
...
}
In this example, I have provided only two AMI. But in reality I will have about 20.

How a list will help you? You need to choose from it somehow. It's better to use a map for it, so you can pick a specific ami based on a key.
After you load the data resources, you can define a map using locals:
locals {
amis = {
amzn = data.aws_ami.amzn
centos = data.aws_ami.centos
}
}
Then to access it, you simply address it as follows
resource "aws_instance" "EC2" {
count = 100
ami = local.amis[amzn].id
instance_type = "t2.micro"
...
}

Related

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

Conditionals in for_each loop around elastic IPs

I have the following code and I would like to have a conditional that will only create an elastic IP if the instance is part of a public subnet (or based off of a boolean value if needed). This is the code that I currently have that works, but I want it to not create elastic IPs for resources on the private subnets:
locals {
instances_beta = {
my-ec2 = {
name = "myec2",
ami = "ami-029e27fb2fc8ce9d8",
instancetype = "t3.xlarge"
environment = "Beta",
securitygroups = [var.mysg],
subnetid = var.public-a,
elasticip = true
}
}
}
resource "aws_instance" "beta-instance" {
for_each = local.instances_beta
ami = each.value.ami
instance_type = each.value.instancetype
subnet_id = each.value.subnetid
key_name = "mykey"
vpc_security_group_ids = each.value.securitygroups
tags = {
Name = each.value.name
Environment = each.value.environment
}
}
resource "aws_eip" "beta-eip" {
for_each = local.instances_beta
instance = aws_instance.beta-instance[each.key].id
vpc = true
}
It sounds like count is the best way to do something like that, but I cannot do that as I am already using a for_each to achieve the resource creation. I was trying to do this with a nested for loop, but I cannot quite figure out how to get the syntax correct or if this is the best way to do it. For reference , the best resource I found on it was here around for_each conditionals: https://blog.gruntwork.io/terraform-tips-tricks-loops-if-statements-and-gotchas-f739bbae55f9
You can use for loop to create filtered map, for example:
for_each = {
for key, value in local.instances_beta: key => value if value.subnetid == var.public-a
}
It will filter local.instances_beta and leave items where subnetid equals var.public-a. You can adjust condition according to your needs.
More details in terraform documentation.

Will "aws_ami" with most_recent=true impact future updates?

Given the config below, what happens if I run apply command against the infrastructure if Amazon rolled out a new version of the AMI?
Will the test instance is going to be destroyed and recreated?
so scenario
terraform init
terraform apply
wait N months
terraform plan (or apply)
AM I going to see "forced" recreation of the ec2 instance that was created N months ago using the older version of the AMI which was "recent" back then?
data "aws_ami" "amazon-linux-2" {
most_recent = true
filter {
name = "owner-alias"
values = ["amazon"]
}
filter {
name = "name"
values = ["amzn2-ami-hvm*"]
}
}
resource "aws_instance" "test" {
depends_on = ["aws_internet_gateway.test"]
ami = "${data.aws_ami.amazon-linux-2.id}"
associate_public_ip_address = true
iam_instance_profile = "${aws_iam_instance_profile.test.id}"
instance_type = "t2.micro"
key_name = "bflad-20180605"
vpc_security_group_ids = ["${aws_security_group.test.id}"]
subnet_id = "${aws_subnet.test.id}"
}
Will "aws_ami" with most_recent=true impact future updates?
#ydeatskoR and #sogyals429 have the right answer. To be more concrete:
resource "aws_instance" "test" {
# ... (all the stuff at the top)
lifecycle {
ignore_changes = [
ami,
]
}
}
note: docs moved to: https://www.terraform.io/docs/language/meta-arguments/lifecycle.html#ignore_changes
Yes as per what #ydaetskcoR said you can have a look at the ignore_changes lifecycle and then it would not recreate the instances. https://www.terraform.io/docs/configuration/resources.html#ignore_changes

terraform setting hostname on multiple aws instances

I'm wanting to understand how user data can be used to set hostnames for 2 or more ec2 instances that terrafrom creates. Below is my instance.tf which creates 2 instances.
resource "aws_instance" "example" {
count = 2
ami = "${lookup(var.AMIS, var.aws_region)}"
instance_type = "t2.micro"
tags = {Name = "rb-${count.index}"}
# the VPC subnet
subnet_id = "${aws_subnet.dee-main-public-1.id}"
# the security group
vpc_security_group_ids = ["${aws_security_group.allow-ssh.id}"]
# the public SSH key
key_name = "${aws_key_pair.mykeypair.key_name}"
}
resource "aws_key_pair" "mykeypair" {
key_name = "mykeypair"
public_key = "${file("${var.PATH_TO_PUBLIC_KEY}")}"
}
How do I set the hostnames for those 2 instances. i.e web1.example.com, web2.example.com
I understand cloudinit or remote-exec can be used for this but struggling to come up with the code as I'm still a beginner. Really appreciate If I can get some help to come up to speed. Many thanks in advance.
-B
You can do it with data resource for template file which allows you to use count function. The below syntax will allow you use the hostname variable with the count function.
data "template_file" "init" {
count = 2
template = file("$path")
vars = {
hostname = "web${count.index}.example.com"
}
}
resource "aws_instance" "master" {
ami = "$ami"
count = 2
instance_type = t2.medium
user_data = data.template_file.init[count.index].rendered
}

Terraform provisioner for multiple instances

I am trying to accomplish adding 2 instances to a file called aws_worker_nodes_IP. This is the code below...again...I do not just need one worker IP listed I need both or all if my variables were to change.
I was told to use self.public_IP but that just list one. I need it for both.
#-----key pair for Workernodes-----
resource "aws_key_pair" "k8s-node_auth" {
key_name = "${var.key_name2}"
public_key = "${file(var.public_key_path2)}"
}
#-----Workernodes-----
resource "aws_instance" "nodes-opt-us1-k8s" {
instance_type = "${var.k8s-node_instance_type}"
ami = "${var.k8s-node_ami}"
count = "${var.NodeCount}"
tags {
Name = "nodes-opt-us1-k8s"
}
key_name = "${aws_key_pair.k8s-node_auth.id}"
vpc_security_group_ids = ["${aws_security_group.opt-us1-k8s_sg.id}"]
subnet_id = "${aws_subnet.opt-us1-k8s.id}"
#-----Link Terraform worker nodes to Ansible playbooks-----
provisioner "local-exec" {
command = <<EOD
cat <<EOF > aws_worker_nodes_IP
[workers]
${self.public_ip} <------need both here not just one
EOF
EOD
}
}
#----this has two nodes----- "count = "${var.NodeCount}"
Sorry if I did not explain correctly in my first question, and I appreciate the help. I have only been working with terraform for a few months. Also, I am a Network Engineer learning to write code.

Resources