globart
Posted on January 3, 2024
This is basically a slighty modified version of a tutorial by Anton Putra, which I've changed to use modules (with pinned versions for future compatibility) instead of plain resources, so it is easier to read. I've also added the code for creating and assigning appropriate IAM Role and Instance Profile to ASG instances, so you can manage them with SSM.
General settings
First of all, you'll have to set the region, which you would like the resources to be deployed in:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.31.0"
}
}
}
provider "aws" {
region = "eu-west-1"
}
VPC
Then, you'll create the VPC and all of its resources (security groups are dependant on eachother, so they are created as resources first and then rules are created as modules)
locals {
...
vpc_cidr = "10.0.0.0/16"
azs = slice(data.aws_availability_zones.available.names, 0, 2)
tags = {
ManagedBy = "Terraform"
}
...
}
data "aws_availability_zones" "available" {}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.4.0"
name = "alb-vpc"
cidr = local.vpc_cidr
azs = local.azs
private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k)]
public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 4)]
enable_nat_gateway = true
single_nat_gateway = true
tags = local.tags
}
resource "aws_security_group" "ec2" {
name = "ec2-sg"
vpc_id = module.vpc.vpc_id
tags = local.tags
}
resource "aws_security_group" "alb" {
name = "alb-sg"
vpc_id = module.vpc.vpc_id
tags = local.tags
}
module "alb-sg-rules" {
source = "terraform-aws-modules/security-group/aws"
version = "5.1.0"
create_sg = false
security_group_id = aws_security_group.alb.id
ingress_cidr_blocks = ["0.0.0.0/0"]
ingress_rules = ["http-80-tcp", "https-443-tcp"]
egress_with_source_security_group_id = [
{
from_port = 8080
to_port = 8080
protocol = "tcp"
description = "App port"
source_security_group_id = aws_security_group.ec2.id
},
{
from_port = 8081
to_port = 8081
protocol = "tcp"
description = "Full healthcheck"
source_security_group_id = aws_security_group.ec2.id
}
]
}
module "ec2-sg-rules" {
source = "terraform-aws-modules/security-group/aws"
version = "5.1.0"
create_sg = false
security_group_id = aws_security_group.ec2.id
ingress_with_source_security_group_id = [
{
from_port = 8080
to_port = 8080
protocol = "tcp"
description = "App port"
source_security_group_id = aws_security_group.alb.id
},
{
from_port = 8081
to_port = 8081
protocol = "tcp"
description = "Full healthcheck"
source_security_group_id = aws_security_group.alb.id
}
]
egress_cidr_blocks = ["0.0.0.0/0"] // these are needed for instance to be able to initiate a connection to SSM
egress_rules = ["https-443-tcp"]
}
AMI
After this, you'll have to create launch template for your ASG. I'll use Packer to create the AMI, as provided in Anton's post. You'll have to create the following files:
files/my-app.service
[Unit]
Description=My App
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
ExecStart=/home/ubuntu/go/bin/my-app
User=ubuntu
Environment=GIN_MODE=release
Restart=always
RestartSec=1
[Install]
WantedBy=multi-user.target
scripts/bootstrap.sh
#!/bin/bash
set -e
sudo add-apt-repository ppa:longsleep/golang-backports
sudo apt-get update
sudo apt-get install -y golang-go
go install github.com/antonputra/tutorials/lessons/127/my-app@main
my-app.pkr.hcl
packer {
required_plugins {
amazon = {
version = "v1.2.9"
source = "github.com/hashicorp/amazon"
}
}
}
source "amazon-ebs" "my-app" {
ami_name = "my-app-{{ timestamp }}"
instance_type = "t3a.small" // this isn't the instance type of ASG, it's the type of temp instance Packer will use to build an AMI off of
region = "eu-west-1" // change this to the region of your resources
subnet_id = "subnet-074a32b171778af28" // change this to any public subnet in the specified region, for example, you can use the one from default VPC
source_ami_filter {
filters = {
name = "ubuntu/images/*ubuntu-jammy-22.04-amd64-server-*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["099720109477"]
}
ssh_username = "ubuntu"
tags = {
Name = "My-App",
ManagedBy = "Packer"
}
}
build {
sources = ["source.amazon-ebs.my-app"]
provisioner "file" {
destination = "/tmp"
source = "files"
}
provisioner "shell" {
script = "scripts/bootstrap.sh"
}
provisioner "shell" {
inline = [
"sudo mv /tmp/files/my-app.service /etc/systemd/system/my-app.service",
"sudo systemctl start my-app",
"sudo systemctl enable my-app"
]
}
}
Then, run these commands to initialize and build your AMI using Packer:
packer init my-app.pkr.hcl
packer build my-app.pkr.hcl
Additional parameters
After this, you'll have to create/import keypair and decide on the instance type, you'd like to use for your ASG. You'll also have to create Route53 public hosted zone and update nameservers for your domain with the ones provided in NS record, which will be automatically created in the Route53 zone. Then, you'll add these values to locals:
locals {
...
domain_name = "https-alb.pp.ua"
keypair_name = "devops"
instance_type = "t3a.micro"
ami_id = "ami-06f69317847054bb5"
...
}
ALB with HTTPS
You can now also create ALB with support for HTTPS and a target group, which will be attached to ASG:
module "acm" {
source = "terraform-aws-modules/acm/aws"
version = "5.0.0"
domain_name = local.domain_name
zone_id = data.aws_route53_zone.public.zone_id
validation_method = "DNS"
tags = local.tags
}
module "alb" {
source = "terraform-aws-modules/alb/aws"
version = "9.4.0"
name = "alb"
vpc_id = module.vpc.vpc_id
subnets = module.vpc.public_subnets
security_groups = [aws_security_group.alb.id]
enable_deletion_protection = false
target_groups = {
asg = {
name = "asg-tg"
port = 8080
protocol = "HTTP"
vpc_id = module.vpc.vpc_id
create_attachment = false
health_check = {
enabled = true
port = 8081
interval = 30
protocol = "HTTP"
path = "/health"
matcher = "200"
healthy_threshold = 3
unhealthy_threshold = 3
}
}
}
listeners = {
http-https-redirect = {
port = 80
protocol = "HTTP"
redirect = {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
},
https = {
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08" // set to allow most clients, should be change to newer one
certificate_arn = module.acm.acm_certificate_arn
forward = {
target_group_key = "asg"
}
}
}
tags = local.tags
}
resource "aws_route53_record" "alb" {
name = local.domain_name
type = "A"
zone_id = data.aws_route53_zone.public.zone_id
alias {
name = module.alb.dns_name
zone_id = module.alb.zone_id
evaluate_target_health = false
}
}
ASG
And, finally, you'll create the ASG itself:
resource "aws_iam_service_linked_role" "autoscaling" {
aws_service_name = "autoscaling.amazonaws.com"
description = "A service linked role for autoscaling"
custom_suffix = "ssm"
provisioner "local-exec" {
command = "sleep 10"
}
tags = local.tags
}
module "asg" {
source = "terraform-aws-modules/autoscaling/aws"
version = "7.3.1"
name = "asg"
use_name_prefix = false
vpc_zone_identifier = module.vpc.private_subnets
min_size = 1 // automatically set as desired
max_size = 3
launch_template_name = "my-app"
launch_template_use_name_prefix = false
update_default_version = true
image_id = local.ami_id
instance_type = local.instance_type
key_name = local.keypair_name
security_groups = [aws_security_group.ec2.id]
create_traffic_source_attachment = true
traffic_source_identifier = module.alb.target_groups["asg"].arn
service_linked_role_arn = aws_iam_service_linked_role.autoscaling.arn
service_linked_role_arn = aws_iam_service_linked_role.autoscaling.arn
create_iam_instance_profile = true
iam_instance_profile_name. = "ssm-instance-profile"
iam_role_name = "ssm-role"
iam_role_path = "/ec2/"
iam_role_description = "SSM role example"
iam_role_tags = local.tags
iam_role_policies = {
AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
block_device_mappings = [
{
# Root volume
device_name = "/dev/xvda"
no_device = 0
ebs = {
delete_on_termination = true
encrypted = true
volume_size = 1
volume_type = "gp3"
}
}
]
scaling_policies = {
avg-cpu-policy-greater-than-80 = {
policy_type = "TargetTrackingScaling"
estimated_instance_warmup = 300
target_tracking_configuration = {
predefined_metric_specification = {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 80.0
}
}
}
tags = local.tags
}
You can optionally add output, to see complete healthcheck URL after all of the resources are created:
output "custom_domain" {
value = "https://${module.acm.distinct_domain_names[0]}/ping" // will output only first domain supplied
}
Posted on January 3, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.