Unable to Launch EC2 Instances Asynchronously via Terraform - amazon-ec2

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.

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}"
}
}

Executing Powershell script on remote Windows EC2 instance in Terraform

I am starting a Windows EC2 instance in AWS. Now I want to install certain software like OpenSSH and some other tasks like creating user after the server has been created. If I have a PowerShell script, how do I execute on the remote instance?
I have a local PowerShell script - install_sft.ps1 and I want to execute on the remote EC2 instance in AWS.
I know I need to use a "provisioner" but unable to get my head around how to use it for Windows.
resource "aws_instance" "win-master" {
provider = aws.lmedba-dc
ami = data.aws_ssm_parameter.WindowsAmi.value
instance_type = var.instance-type
key_name = "RPNVirginia"
associate_public_ip_address = true
vpc_security_group_ids = [aws_security_group.windows-sg.id]
subnet_id = aws_subnet.dc1.id
tags = {
Name = "Win server"
}
depends_on = [aws_main_route_table_association.set-master-default-rt-assoc]
}
You can do this by making use of the user_data parameter of the aws_instance resource:
resource "aws_instance" "win-master" {
...
user_data_base64 = "${base64encode(file(install_sft.ps1))}"
...
}
Just ensure that install_sft.ps1 is in the same directory as your Terraform code.
An EC2 instance's User Data script executes when it starts up for the first time. See the AWS documentation here for more details.

Cannot get public IP address of spot instance with Terraform

I’m spinning up a spot instance as you can see in below config and then trying to get the IP address from the spot. It seems to work fine with a regular ec2 instance (ie. that is not spot instance).
The error that I get is:
aws_route53_record.staging: Resource
‘aws_spot_instance_request.app-ec2’ does not have attribute
‘public_ip’ for variable ‘aws_spot_instance_request.app-ec2.public_ip’
Here is the config that I’m using:
resource "aws_spot_instance_request" "app-ec2" {
ami = "ami-1c999999"
spot_price = "0.008"
instance_type = "t2.small"
tags {
Name = "${var.app_name}"
}
key_name = "mykeypair"
associate_public_ip_address = true
vpc_security_group_ids = ["sg-99999999"]
subnet_id = "subnet-99999999"
iam_instance_profile = "myInstanceRole"
user_data = <<-EOF
#!/bin/bash
echo ECS_CLUSTER=APP-STAGING >> /etc/ecs/ecs.config
EOF
}
resource "aws_route53_record" "staging" {
zone_id = "XXXXXXXX"
name = "staging.myapp.com"
type = "A"
ttl = "300"
records = ["${aws_spot_instance_request.app-ec2.public_ip}"]
The spot request is fulfilled on the AWS Console as per below:
Any help will be greatly appreciated!
So I've been trying to figure this out since last night and kept seeing the spot instance request being fulfilled via the AWS Console. Likewise, I could see the public IP for the spot and this was misleading me.
It turns out I was missing 1 line (argument) in my script:
wait_for_fulfillment = true
By default, it is set to false, and therefore when I tried to set the public_ip address it simply did not exist at that time.
Now Terraform will wait for the Spot Request to be fulfilled. According to the documentation, it will throw an error if the timeout of 10m is reached.
I tried the code snippet you provided with Terraform version 0.12.10 and got the same error. I checked the terraform.tfstate file and saw that the fields were not populated yet (for example private_ip, public_ip, and public_dns were set to null). I checked the "Spot Requests" section in the AWS Console and saw the following Status: price-too-low: Your Spot request price of 0.0075 is lower than the minimum required Spot request fulfillment price of 0.008. The request state was still open so this is why all the variables in the state file were set to null.

Create CloudWatch alarm that sets an instance to standby via SNS/Lambda

What I am looking to do is set an instance to standby mode when it hits an alarm state. I already have an alarm set up to detect when my instance hits 90% CPU for a while. The alarm currently sends a Slack and text message via SNS calling a Lambda function. I would like to add is to have the instance go into standby mode. The instances are in an autoscaling group.
I found that you can perform this through the CLI using the command :
aws autoscaling enter-standby --instance-ids i-66b4f7d5be234234234 --auto-scaling-group-name my-asg --should-decrement-desired-capacity
You can also do this with boto3 :
response = client.enter_standby(
InstanceIds=[
'string',
],
AutoScalingGroupName='string',
ShouldDecrementDesiredCapacity=True|False
)
I assume I need to write another Lambda function that will be triggered by SNS that will use the boto3 code to do this?
Is there a better/easier way before I start?
I already have the InstanceId passed into the event to the Lambda so I will have to add the ASG name in the event.
Is there a way to get the ASG name in the Lambda function when I already have the Instance ID? Then I do not have to pass it in with the event.
Thanks!
Your question has a couple sub-parts, so I'll try to answer them in order:
I assume I need to write another Lambda function that will be triggered by SNS that will use the boto3 code to do this?
You don't need to, you could overload your existing function. I could see a valid argument for either separate functions (separation of concerns) or one function (since "reacting to CPU hitting 90%" is basically "one thing").
Is there a better/easier way before I start?
I don't know of any other way you could do it, other than Cloudwatch -> SNS -> Lambda.
Is there a way to get the ASG name in the Lambda function when I already have the Instance ID?
Yes, see this question for an example. It's up to you whether it looks like doing it in the Lambda or passing an additional parameter is the cleaner option.
For anyone interested, here is what I came up with for the Lambda function (in Python) :
# Puts the instance in the standby mode which takes it off the load balancer
# and a replacement unit is spun up to take its place
#
import json
import boto3
ec2_client = boto3.client('ec2')
asg_client = boto3.client('autoscaling')
def lambda_handler(event, context):
# Get the id from the event JSON
msg = event['Records'][0]['Sns']['Message']
msg_json = json.loads(msg)
id = msg_json['Trigger']['Dimensions'][0]['value']
print("Instance id is " + str(id))
# Capture all the info about the instance so we can extract the ASG name later
response = ec2_client.describe_instances(
Filters=[
{
'Name': 'instance-id',
'Values': [str(id)]
},
],
)
# Get the ASG name from the response JSON
#autoscaling_name = response['Reservations'][0]['Instances'][0]['Tags'][1]['Value']
tags = response['Reservations'][0]['Instances'][0]['Tags']
autoscaling_name = next(t["Value"] for t in tags if t["Key"] == "aws:autoscaling:groupName")
print("Autoscaling name is - " + str(autoscaling_name))
# Put the instance in standby
response = asg_client.enter_standby(
InstanceIds=[
str(id),
],
AutoScalingGroupName=str(autoscaling_name),
ShouldDecrementDesiredCapacity=False
)

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