How to iterate over all aws_instances in terraform? - amazon-ec2

I'm relatively new to terraform and I'm trying to iterate over all aws_instances to apply a null_resource. Can you use multiple splats to access all instances, regardless of their names?
The EC2 instances are broken down by three types:
aws_instance.web.* (3 instances)
aws_instance.app.* (3 instances)
aws_instance.db.* (2 instances)
Here's my attempt to apply a null_resource to all eight aws_instances:
resource "null_resource" "install_security_package" {
#count = "${length(aws_instance)}" #terraform error: resource count can't reference variable: aws_instance
#count = "${length(aws_instance.*)}" #terraform error: resource variables must be three parts: TYPE.NAME.ATTR
count = "${length(aws_instance.*.*)}" #terraform error: unknown resource 'aws_instance.*'
connection {
type = "ssh"
host = "${element(aws_instance.*.private_ip, count.index)}"
user = "${lookup(var.user, var.platform)}"
private_key = "${file("${var.private_key_path}")}"
timeout = "2m"
}
provisioner "remote-exec" {
inline = [
"sudo rpm -Uvh http://www.example.com/security/repo/security_baseline.rpm",
]
}
}

It is not currently possible to match all resources of a given type. The "splat" syntax, as you've seen, only allows selecting all of the instances created from a particular resource block.
The closest you can get to this with Terraform today is to concatenate together the different resources:
concat(aws_instance.web.*.private_ip, aws_instance.app.*.private_ip, aws_instance.db.*.private_ip)
In the current version of Terraform as of this answer it is necessary to use some of the workarounds shared in github issue #4084 in order to avoid duplicating that complex expression in multiple places. A forthcoming feature called Local Values will make this simpler in the near future, allowing the list to be given an name to be re-used in multiple places:
# Won't work until Terraform PR#15449 is merged and released
locals {
aws_instance_addrs = "${concat(aws_instance.web.*.private_ip, aws_instance.app.*.private_ip, aws_instance.db.*.private_ip)}"
}
resource "null_resource" "install_security_package" {
count = "${length(local.aws_instance_addrs)}"
connection {
type = "ssh"
host = "${local.aws_instance_addrs[count.index]}"
user = "${lookup(var.user, var.platform)}"
private_key = "${file("${var.private_key_path}")}"
timeout = "2m"
}
provisioner "remote-exec" {
inline = [
"sudo rpm -Uvh http://www.example.com/security/repo/security_baseline.rpm",
]
}
}

Related

Creating EC2 instances in Terraform with worksapces

I'm trying to figure out how to create different number of EC2 instances between two different Terraform workspaces. My approach is to have all Terraform code in one Github branch. I would like to have one aws_instance block that creates a different number of instances that are also different sized EC2 instances. I plan on using a a TFVARS file for separate environments in this case to specify what size instances are to be used. Any advice on how to best approach this scenario would be helpful. I am using Terraform version 0.12.26
You can simply do similar like this: (3 instances for staging and 1 for other workspaces)
resource "aws_instance" "cluster_nodes" {
count = terraform.workspace == "staging" ? 3 : 1
ami = var.cluster_aws_ami
instance_type = var.cluster_aws_instance_type
# subnet_id = aws_subnet.cluster_subnet[var.azs[count.index]].id
subnet_id = var.public_subnet_ids[count.index]
vpc_security_group_ids = [aws_security_group.cluster_sg.id]
key_name = aws_key_pair.cluster_ssh_key.key_name
iam_instance_profile = "${aws_iam_instance_profile.cluster_ec2_instance_profile.name}"
associate_public_ip_address = true
tags = {
Name = "Cluster ${terraform.workspace} node-${count.index}"
}
}

Unable to Launch EC2 Instances Asynchronously via Terraform

I am willing to launch two instances via Terraform. First one will generate some certificate files, push to S3 bucket. The second instance will pull those certificates from particular S3 bucket. Both operations will be handled by user data. The problem here is pull commands (aws cli) in user data of second instance are not working. (It is working when I try from shell) I think the issue is about terraform is launching both instances synchronously so that second instance is getting launched before first instance pushes the certificates to S3.
I also tried to handle this by adding "depends_on" to my code but it did not work. I am looking for a way to launch the instances asynchronously. Like second instance will be launched after 30 seconds then first instance is launched. Here I am pasting the related part of the code.
data "template_file" "first_executor" {
template = file("some_path/first_executor.sh")
}
resource "aws_instance" "first_instance" {
ami = data.aws_ami.amazon-linux-2.id
instance_type = "t2.micro"
user_data = data.template_file.first_executor.rendered
network_interface {
device_index = 0
network_interface_id = aws_network_interface.first_instance-network-interface.id
}
}
###
data "template_file" "second_executor" {
template = file("some_path/second_executor.sh")
}
resource "aws_instance" "second_instance" {
depends_on = [aws_instance.first_instance]
ami = data.aws_ami.amazon-linux-2.id
instance_type = "t2.micro"
user_data = data.template_file.second_executor.rendered
network_interface {
device_index = 0
network_interface_id = aws_network_interface.second-network-interface.id
}
}
Answer is no. "depends_on" in Terraform means it will wait for a resource to be available. This means, your second EC2 will be created as soon as first EC2 is triggered.
Terraform will not wait till your first EC2 is in "running" state or if user data is executed.
I would suggest go with depdens_on and then, in your second EC2 user data script, add some logic to have a loop which will look up S3 and will wait and repeat till the resources are found.

Terraform creating lambda before zip is ready

I'm trying to create a Terraform module that will build my JS lambdas, zip them and deploy them. This however proves to be problematic
resource "null_resource" "build_lambda" {
count = length(var.lambdas)
provisioner "local-exec" {
command = "mkdir tmp"
working_dir = path.root
}
provisioner "local-exec" {
command = var.lambdas[count.index].code.build_command
working_dir = var.lambdas[count.index].code.working_dir
}
}
data "archive_file" "lambda_zip" {
count = length(var.lambdas)
type = "zip"
source_dir = var.lambdas[count.index].code.working_dir
output_path = "${path.root}/tmp/${count.index}.zip"
depends_on = [
null_resource.build_lambda
]
}
/*******************************************************
* Lambda definition
*******************************************************/
resource "aws_lambda_function" "lambda" {
count = length(var.lambdas)
filename = data.archive_file.lambda_zip[count.index].output_path
source_code_hash = filebase64sha256(data.archive_file.lambda_zip[count.index].output_path)
function_name = "${var.application_name}-${var.lambdas[count.index].name}"
description = var.lambdas[count.index].description
handler = var.lambdas[count.index].handler
runtime = var.lambdas[count.index].runtime
role = aws_iam_role.iam_for_lambda.arn
memory_size = var.lambdas[count.index].memory_size
depends_on = [aws_iam_role_policy_attachment.lambda_logs, aws_cloudwatch_log_group.log_group, data.archive_file.lambda_zip]
}
The property source_code_hash = filebase64sha256(data.archive_file.lambda_zip[count.index].output_path) , although technically not obligatory, is necessary, or the existing lambda will never get overriden as Terraform will think that it is still the same version of lambda and will skip the deployment altogether. Unfortunately it looks like the method filebase64sha256 is evaluated before the creation of any resource. This means that there is no zip for the hash calculation and so I get the error
Error: Error in function call
on modules\api-gateway-lambda\main.tf line 35, in resource "aws_lambda_function" "lambda":
35: source_code_hash = filebase64sha256(data.archive_file.lambda_zip[count.index].output_path)
|----------------
| count.index is 0
| data.archive_file.lambda_zip is tuple with 1 element
Call to function "filebase64sha256" failed: no file exists at tmp\0.zip.
If i manually place a zip in the right location, I can see that the whole thing starts working and the zip eventually gets overriden by a new one, but the hash in this case must come from the previous zip.
What is the right way to execute the whole thing in the right order?
The archive_file data source has its own output_base64sha256 attribute which can give you that same result without asking Terraform to read a file that doesn't exist yet:
source_code_hash = data.archive_file.lambda_zip[count.index].output_base64sha256
The data source will populate this at the same time it creates the file, and because your lambda function depends on the data source it will therefore always be available before the lambda function configuration is evaluated.

Creating an icinga2 Apply For rule across multiple variables

Our team is just getting started with icinga2. We have an existing homegrown tool that checks a lot of different things depending on an argument so we have a series of CheckCommand objects that vary by only one argument. These checks need to be applied to multiple database instances across multiple hosts. Creating separate CheckCommands is working fine, like this:
object Host "some.oracle.host" {
import "oracle-host"
address = "172.1.1.1"
vars.db_instances = {
"EnvName" = {
"username" = "username"
"password" = "EQo1OF95G8Gs"
"dbname" = "FOO"
"connstr" = "jdbc:oracle:thin:#(FOO)"
}
}
apply Service for (env => instance in host.vars.db_instances) {
import "oracle-db-svc"
check_command = "check_oracle_foocheck"
display_name = env + "-foocheck"
vars.svc_dbname = instance["dbname"]
}
apply Service for (env => instance in host.vars.db_instances) {
import "oracle-db-svc"
check_command = "check_oracle_barcheck"
display_name = env + "-barcheck"
vars.svc_dbname = instance["dbname"]
}
Here we are creating new "check_oracle_foocheck" and "check_oracle_barcheck" services for each instance of each host that has a db_instances variable. It works well but the problem is, we have a couple dozen of those checks to implement. I'm looking for something simpler, something like:
const OracleChecks = ["foocheck", "barcheck"]
apply Service for (check in OracleChecks) {
import "oracle-db-svc"
for(env => instance in host.vars.db_instances){
check_command = "$check$"
display_name = env + "-$check$"
}
}
I assume this wouldn't work because the apply for rule isn't actually returning and maybe you can't iterate on hosts within an apply for. Here I'm just trying to convey the objective.
What I'm after is an approach to make sort of a matrix assignment of a set of services to sets of instances which live on multiple hosts. Any tips?

When provisioning with Terraform, how does code obtain a reference to machine IDs (e.g. database machine address)

Let's say I'm using Terraform to provision two machines inside AWS:
An EC2 Machine running NodeJS
An RDS instance
How does the NodeJS code obtain the address of the RDS instance?
You've got a couple of options here. The simplest one is to create a CNAME record in Route53 for the database and then always point to that CNAME in your application.
A basic example would look something like this:
resource "aws_db_instance" "mydb" {
allocated_storage = 10
engine = "mysql"
engine_version = "5.6.17"
instance_class = "db.t2.micro"
name = "mydb"
username = "foo"
password = "bar"
db_subnet_group_name = "my_database_subnet_group"
parameter_group_name = "default.mysql5.6"
}
resource "aws_route53_record" "database" {
zone_id = "${aws_route53_zone.primary.zone_id}"
name = "database.example.com"
type = "CNAME"
ttl = "300"
records = ["${aws_db_instance.default.endpoint}"]
}
Alternative options include taking the endpoint output from the aws_db_instance and passing that into a user data script when creating the instance or passing it to Consul and using Consul Template to control the config that your application uses.
You may try Sparrowform - a lightweight provision tool for Terraform based instances, it's capable to make an inventory of Terraform resources and provision related hosts, passing all the necessary data:
$ terrafrom apply # bootstrap infrastructure
$ cat sparrowfile # this scenario
# fetches DB address from terraform cache
# and populate configuration file
# at server with node js code:
#!/usr/bin/env perl6
use Sparrowform;
$ sparrowfrom --ssh_private_key=~/.ssh/aws.pem --ssh_user=ec2 # run provision tool
my $rdb-adress;
for tf-resources() -> $r {
my $r-id = $r[0]; # resource id
if ( $r-id 'aws_db_instance.mydb') {
my $r-data = $r[1];
$rdb-address = $r-data<address>;
last;
}
}
# For instance, we can
# Install configuration file
# Next chunk of code will be applied to
# The server with node-js code:
template-create '/path/to/config/app.conf', %(
source => ( slurp 'app.conf.tmpl' ),
variables => %(
rdb-address => $rdb-address
),
);
# sparrowform --ssh_private_key=~/.ssh/aws.pem --ssh_user=ec2 # run provisioning
PS. disclosure - I am the tool author

Resources