Table of Contents
- Why Automate Bastion Host Deployment?
- Prerequisites and Setup
- Terraform Implementation
- CloudFormation Templates
- Ansible Playbooks
- Conclusion
A Bastion host is a special purpose server used to provide secure access to private resources within a network, such as virtual machines in a private subnet. In cloud environments like AWS, Azure, or GCP, Bastion hosts act as controlled entry points through which administrators can SSH or RDP into instances that are not directly accessible from the internet. These hosts are typically placed in a public subnet with strict security group rules and hardened configurations to minimize attack surfaces. By funneling all administrative access through a single, monitored, and auditable point, Bastion hosts help enforce the principle of least privilege, reduce exposure of internal systems, and improve overall security posture in cloud architectures.
Why Automate Bastion Host Deployment?
Manual bastion host deployment is error-prone, time-consuming, and difficult to replicate across environments. Infrastructure as Code (IaC) solves these challenges by providing:
- Consistency: Identical configurations across dev, staging, and production
- Version Control: Track changes and roll back when needed
- Speed: Deploy in minutes, not hours
- Compliance: Enforce security standards automatically
- Documentation: Code serves as living documentation
The Hidden Cost of Manual Deployment

Choosing the Right IaC Tool
- Terraform: Best for multi-cloud environments and complex infrastructures
- CloudFormation: Ideal for AWS-native deployments with deep service integration
- Ansible: Perfect for configuration management and hybrid deployments
Or better yet, for “ClickOps” admins, you can get our Bastion AMI straight from the AWS MarketPlace below.

Prerequisites and Setup
Before we begin, ensure you have:
Required Tools
# Check versions
terraform --version # 1.5+ recommended
aws --version # 2.x required
ansible --version # 2.14+ recommended
Project Structure
If you would like to get access to the code described below, you can access it on github.
#https://github.com/solvedevops/aws-bastion-automation.git
.
├── ansible
│ └── playbook.yml
├── cloudformation
│ └── bastion-stack.yaml
└── terraform
├── bastion.tf
└── vars.tf
4 directories, 4 files
Terraform Implementation
bastion.tf
# Provider configuration
provider "aws" {
region = var.aws_region
}
# Data source for latest Bastion AMI
data "aws_ami" "bastion" {
most_recent = true
owners = ["679593333241"] # AWS MarketPlace ID
filter {
name = "name"
values = ["SolveDevOps-Bastion-Host-Ubuntu24.04*"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
}
# Elastic IP
resource "aws_eip" "bastion" {
domain = "vpc"
tags = {
Name = "Bastion-EIP"
}
}
# EC2 Instance
resource "aws_instance" "bastion" {
ami = data.aws_ami.bastion.id
instance_type = var.instance_type
key_name = var.key_name
#subnet_id = aws_subnet.bastion_public.id
vpc_security_group_ids = var.security_groups_ids
#associate_public_ip_address = true
root_block_device {
volume_type = "gp3"
volume_size = 60
encrypted = true
}
tags = {
Name = "Bastion-Server"
}
}
# EIP Association
resource "aws_eip_association" "bastion" {
instance_id = aws_instance.bastion.id
allocation_id = aws_eip.bastion.id
}
# Data source for availability zones
##data "aws_availability_zones" "available" {
# state = "available"
#}
# Outputs
output "bastion_public_ip" {
value = aws_eip.bastion.public_ip
}
output "bastion_admin_url" {
value = "http://${aws_eip.bastion.public_ip}/admin"
}
output "ssh_connection_string" {
value = "ssh -i ${var.key_name}.pem ubuntu@${aws_eip.bastion.public_ip}"
}
vars.tf
# Variables
variable "aws_region" {
description = "AWS region for FreePBX deployment"
default = "us-east-1"
}
variable "instance_type" {
description = "EC2 instance type for FreePBX"
default = "t3.medium"
}
variable "key_name" {
description = "AWS key pair name for SSH access"
type = string
}
variable "security_groups_ids" {
description = "List of Subnet Group Ids"
type = list(string)
default = []
}
Deployment Commands
# Initialize Terraform
cd terraform/
terraform init
# Create a terraform plan
terraform plan
# Apply configuration
terraform apply
CloudFormation Templates
Complete CloudFormation Stack
bastion-stack.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Bastion Host deployment using AWS MarketPlace Image'
Parameters:
KeyName:
Description: Name of an existing EC2 KeyPair to enable SSH access
Type: AWS::EC2::KeyPair::KeyName
ConstraintDescription: Must be the name of an existing EC2 KeyPair
InstanceType:
Description: EC2 instance type for Bastion
Type: String
Default: t3.large
AllowedValues:
- t3.large
- t3.xlarge
ConstraintDescription: Must be a valid EC2 instance type
Mappings:
RegionMap:
ap-south-1:
AMI: ami-0612aad4569163896
eu-north-1:
AMI: ami-01ce554ddd66861f8
eu-west-3:
AMI: ami-0db7186514a13af34
eu-west-2:
AMI: ami-02363a79364adb260
eu-west-1:
AMI: ami-04655665a14615370
ap-northeast-3:
AMI: ami-0dbd54a2497d40ff3
ap-northeast-2:
AMI: ami-03ad6d2ea6683e5c4
ap-northeast-1:
AMI: ami-0e18c9237d66e2c6e
ca-central-1:
AMI: ami-0e31524ec38891006
sa-east-1:
AMI: ami-0437cda56731c4c8a
ap-southeast-1:
AMI: ami-014b8d7ca9ad783ba
ap-southeast-2:
AMI: ami-0080b9a2a51e2d894
eu-central-1:
AMI: ami-0af0b4a9dad36131a
us-east-1:
AMI: ami-07873de158f43fe0e
us-east-2:
AMI: ami-02c4f6020f83bc8e2
us-west-1:
AMI: ami-0e7b363658a343952
us-west-2:
AMI: ami-0149473c34e05b8bf
Resources:
# Elastic IP
BastionEIP:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Name
Value: Bastion-EIP
# EC2 Instance
BastionInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: !FindInMap [RegionMap, !Ref 'AWS::Region', AMI]
InstanceType: !Ref InstanceType
KeyName: !Ref KeyName
SecurityGroupIds:
- sg-0343512454
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeType: gp3
VolumeSize: 60
Encrypted: true
Tags:
- Key: Name
Value: Bastion-Server
# EIP Association
EIPAssociation:
Type: AWS::EC2::EIPAssociation
Properties:
InstanceId: !Ref BastionInstance
EIP: !Ref BastionEIP
Outputs:
PublicIP:
Description: Public IP address of Bastion instance
Value: !Ref BastionEIP
AdminURL:
Description: Bastion Admin URL
Value: !Sub 'http://${BastionEIP}/admin'
SSHConnection:
Description: SSH connection string
Value: !Sub 'ssh -i ${KeyName}.pem admin@${BastionEIP}'
Deploying with CloudFormation
aws cloudformation create-stack \
--stack-name bastion-host \
--template-body file://bastion-stack.yaml \
--parameters \
ParameterKey=KeyName,ParameterValue=my_key \
ParameterKey=InstanceType,ParameterValue=t3.large
Check stack system
# Check stack status
aws cloudformation describe-stacks --stack-name bastion-host
Ansible Playbooks
Complete Ansible Automation
playbook.yml
---
- name: Deploy Bastion host on AWS
hosts: localhost
connection: local
gather_facts: false
vars:
aws_region: us-east-1
instance_type: t3.large
key_name: "solve-useast1"
ami_id: "ami-07873de158f43fe0e" # marketplace image for Bastion Host
tasks:
- name: Allocate Elastic IP
amazon.aws.ec2_eip:
region: "{{ aws_region }}"
in_vpc: yes
register: eip
- name: Launch Bastion Host Instance
amazon.aws.ec2_instance:
key_name: "{{ key_name }}"
instance_type: "{{ instance_type }}"
image_id: "{{ ami_id }}"
wait: yes
region: "{{ aws_region }}"
security_group: "sg-012345654"
volumes:
- device_name: /dev/xvda
ebs:
volume_type: gp3
volume_size: 60
encrypted: yes
tags:
Name: Bastion Host-Server
register: ec2
- name: Associate Elastic IP
amazon.aws.ec2_eip:
device_id: "{{ ec2.instances[0].instance_id }}"
ip: "{{ eip.public_ip }}"
region: "{{ aws_region }}"
- name: Add host to inventory
add_host:
hostname: "{{ eip.public_ip }}"
groups: Bastion
ansible_user: admin
ansible_ssh_private_key_file: "{{ key_name }}.pem"
Running the Ansible Playbook
# Install required collections
ansible-galaxy collection install amazon.aws
# Run the playbook
ansible-playbook playbook.yml
Conclusion
Infrastructure as Code transforms bastion host deployment from a manual, error-prone process into a reliable, repeatable, and secure operation. Whether you choose Terraform’s flexibility, CloudFormation’s AWS integration, or Ansible’s configuration power, the key is to start automating today.
Key Takeaways:
- Automate Everything: Manual processes don’t scale and introduce errors
- Version Control: Treat infrastructure like code with proper versioning
- Test Thoroughly: Validate changes before production deployment
- Monitor Continuously: Automate monitoring and alerting
- Document as Code: Let your IaC serve as living documentation
Ready to Automate?
While these IaC patterns will get you started, for Proof of Concept installations, you might prefer to use “ClickOps” and get the Bastion Host deployed straight from the AWS MarketPlace. If this is the case, please click on the image below to get redirected to AWS MarketPlace.

Deploy production-ready bastion hosts with a single Terraform resource or CloudFormation stack.