In the previous post we went through a whole lot of trouble to maintain an EBS Volume and to make sure it automatically mounts on boot for our stateful service.

Accompaining that with an AWS Autoscaling Group, we now have a “Mostly Available” service that automatically recovers, with a static data disk.

The last bit of the equation however is making sure that we can keep on accessing the service with the same DNS name, even if the underlying instance changes IP address.

While we could potentially use a ALB or NLB for this purpose, that automatically registers the new instances launched by the Autoscaling Group, there is a much cheaper solution, that only involves a few lines in our userdata script.

Example: a bastion host

Let’s say you use a bastion server to protect your private instances from direct SSH access.

This is a tiny t4g.nano instance, costing $3.40 every month that barely stays alive. Placing a $25/month load balancer in front of it just to get a stable DNS name feels like a waste.

You want the instance to be reachable as bastion.example.com.

First of all, define the example.com DNS zone in Route53. If you already have set this up elsewhere, you can import it into your state file with a data block rather than a resource:

data "aws_route53_zone" "root" {
  name = "example.com"
}

Allow your bastion instance to ChangeResourceRecordSets on this Zone with this policy:

data "aws_iam_policy_document" "bastion" {
  statement {
    sid    = "SetDomainNames"
    effect = "Allow"

    actions = [
      "route53:ChangeResourceRecordSets"
    ]

    resources = [
      "arn:aws:route53:::hostedzone/${data.aws_route53_zone.root.zone_id}"
    ]
  }
}

Add to the bastion userdata (checkout the previous post where we discuss the EBS Volume automounting) these few lines:

PUBLIC_IP=$(curl http://169.254.169.254/latest/meta-data/public-ipv4)

aws route53 change-resource-record-sets --hosted-zone-id ${zone_id} --change-batch '
{
    "Changes": [
        {
            "Action": "UPSERT",
            "ResourceRecordSet": {
                "Type": "A",
                "Name": "bastion.example.com",
                "TTL": 60,
                "ResourceRecords": [{"Value": "'$PUBLIC_IP'"}]
            }
        }
    ]
}
'

Wherever you defined the userdata resource in terraform, pass in the zone_id variable:

data "template_file" "bastion_userdata" {
  template = file("${path.module}/userdata.sh")

  vars = {
    zone_id = data.aws_route53_zone.root.zone_id
  }
}

And you are all set!

Congratulations, your server can now die peacefully, knowing that another one will be soon spun up by the autoscaling group, replacing its place in the DNS namespace and even mounting a stateful EBS Volume if needed.