Some learning paths may require one or more server nodes to complete. The Terraform files shown here can be used as a platform to work on those learning paths. The intent is for you to modify these as needed to support other learning path activities.
A Jump Server (also known as a bastion host) is an intermediary device responsible for funneling traffic through firewalls using a supervised secure channel. By creating a barrier between networks, jump servers create an added layer of security against outsiders wanting to maliciously access sensitive company data. Only those with the right credentials can log into a jump server and obtain authorization to proceed to a different security zone.
An alternative to setting up a Jump server like below is to use Linux Bastion Hosts or AWS Systems Manager service .
For deploying Arm instances on AWS and providing access via Jump Server, the Terraform configuration can be broken down into six files: variables.tf
, provider.tf
, VPC_subnet_IG_RT.tf
, security_groups.tf
, ec2.tf
and outputs.tf
.
Once configured, it creates an instance with OS Login configured to use as a bastion host and a private instance to use alongside the bastion host.
Start by creating these files in your desired directory.
touch variables.tf provider.tf VPC_subnet_IG_RT.tf security_groups.tf ec2.tf outputs.tf
variables.tf
configures the region, ami, instance type, disk size, and IP range for both the private server and jump server.
Add the code below to the variables.tf
file:
variable "region" {
description = "The AWS region to create resources in."
default = "us-east-1"
}
variable "ubuntu-ami" {
description = "Ubuntu 22.04 AMI image"
default = "ami-0f69dd1d0d03ad669" // Ubuntu ARM64 Image
nullable = false
}
// Bastion Server Credentials
variable "bastionhost-instance-type" {
description = "Instance Type of bastion/Jump server"
default = "t4g.small"
}
variable "bastionhost-disk-size" {
description = "Root disk size of bastion/Jump server"
default = 8
}
variable "jump-server-IP-Range" {
default = "0.0.0.0/0"
}
// Private Server Credentials
variable "instance-type-1" {
description = "Instance Type of private server"
default = "t4g.small"
}
variable "disk-size-1" {
description = "Root disk size of private server"
default = 40
}
provider.tf
contains a region
variable that controls where to deploy the instances.
Add the code below to the provider.tf
file:
provider "aws" {
region = var.region
}
VPC_subnet_IG_RT.tf
contains the following several resources. For more information about these resources, see the
Amazon VPC Documentation
.
Specifically, we will have a Virtual Private Cloud (VPC) with a CIDR block size of /16. In that VPC, we will have the following two subnets with a CIDR block size of /24 each:
Then, we will have a Route Table for the Internet Gateway so that instance can connect to the outside world, update it, and associate it with the private subnet.
Finally, create a Network Address Translation gateway to connect your VPC/Network to the internet and attach this gateway to your VPC in the public network.
Add the code below to the VPC_subnet_IG_RT.tf
file:
// VPC creation
resource "aws_vpc" "Demo_VPC" {
cidr_block = "10.1.0.0/16"
instance_tenancy = "default"
enable_dns_support = "true"
enable_dns_hostnames = "true"
tags = {
Name = "VPC"
}
}
// Subnet creation
resource "aws_subnet" "Public_Subnet" {
vpc_id = aws_vpc.Demo_VPC.id
cidr_block = "10.1.1.0/24"
map_public_ip_on_launch = "true"
availability_zone = "us-east-1a"
tags = {
Name = "Jump Public Subnet"
}
}
resource "aws_subnet" "Private_Subnet" {
vpc_id = aws_vpc.Demo_VPC.id
map_public_ip_on_launch = "true"
cidr_block = "10.1.2.0/24"
availability_zone = "us-east-1b"
tags = {
Name = "Private Subnet"
}
}
resource "aws_internet_gateway" "IGW" {
vpc_id = aws_vpc.Demo_VPC.id
tags = {
Name = "VPC Internet Gateway"
}
}
resource "aws_route_table" "Route_table" {
vpc_id = aws_vpc.Demo_VPC.id
tags = {
Name = "VPC Route Table"
}
}
resource "aws_route" "VPC_internet_access" {
route_table_id = aws_route_table.Route_table.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.IGW.id
}
resource "aws_route_table_association" "VPC_association" {
subnet_id = aws_subnet.Public_Subnet.id
route_table_id = aws_route_table.Route_table.id
}
resource "aws_eip" "jump-eip" {
vpc = true
public_ipv4_pool = "amazon"
}
resource "aws_nat_gateway" "eip" {
depends_on = [aws_eip.jump-eip]
allocation_id = aws_eip.jump-eip.id
subnet_id = aws_subnet.Public_Subnet.id
tags = {
Name = "NAT_Gateway"
}
}
// Route table for SNAT in private subnet
resource "aws_route_table" "private_subnet_route_table" {
depends_on = [aws_nat_gateway.eip]
vpc_id = aws_vpc.Demo_VPC.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_nat_gateway.eip.id
}
tags = {
Name = "private_subnet_route_table"
}
}
resource "aws_route_table_association" "private_subnet_route_table_association" {
depends_on = [aws_route_table.private_subnet_route_table]
subnet_id = aws_subnet.Private_Subnet.id
route_table_id = aws_route_table.private_subnet_route_table.id
}
security_groups.tf
creates two security groups, one for the Bastion Host and one for the Private Instance, in order to allow SSH access from this Bastion Host.
Add the code below to the security_groups.tf
file:
resource "aws_security_group" "only_ssh_bastion" {
depends_on = [aws_subnet.Public_Subnet]
name = "only_ssh_bastion"
vpc_id = aws_vpc.Demo_VPC.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["${var.jump-server-IP-Range}"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "only_ssh_bastion"
}
}
resource "aws_security_group" "only_ssh_private_instance" {
depends_on = [aws_subnet.Public_Subnet]
name = "only_ssh_private_instance"
description = "allow ssh bastion inbound traffic"
vpc_id = aws_vpc.Demo_VPC.id
ingress {
description = "Only ssh_sql_bastion in public subnet"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.only_ssh_bastion.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
tags = {
Name = "only_ssh_sql_bastion"
}
}
ec2.tf
creates a Bastion/Jump server and a Private Instance.
Add the code below to the ec2.tf
file:
// Bastion/Jump server
resource "aws_instance" "BASTION" {
ami = var.ubuntu-ami
instance_type = var.bastionhost-instance-type
subnet_id = aws_subnet.Public_Subnet.id
vpc_security_group_ids = [aws_security_group.only_ssh_bastion.id]
key_name = aws_key_pair.deployer.key_name
root_block_device {
volume_size = var.bastionhost-disk-size
volume_type = "gp2"
encrypted = true
delete_on_termination = true
}
tags = {
Name = "bastion-host"
}
}
// Private instance
resource "aws_instance" "ec2" {
ami = var.ubuntu-ami
instance_type = var.instance-type-1
subnet_id = aws_subnet.Private_Subnet.id
vpc_security_group_ids = [aws_security_group.only_ssh_private_instance.id]
key_name = aws_key_pair.deployer.key_name
root_block_device {
volume_size = var.disk-size-1
volume_type = "gp2"
encrypted = true
delete_on_termination = true
}
tags = {
Name = "terraform"
}
}
resource "aws_key_pair" "deployer" {
key_name = "id_rsa"
public_key = file("~/.ssh/id_rsa.pub")
}
outputs.tf
defines the output values for this configuration.
Add the code below to the outputs.tf
file:
output "EC2-public_ip" {
value = aws_instance.ec2.public_ip
}
output "EC2-private_ip" {
value = aws_instance.ec2.private_ip
}
output "bastionhost-public-ip" {
value = aws_instance.BASTION.public_ip
}
Run terraform init
to initialize the Terraform deployment. This command is responsible for downloading all the dependencies which are required for the provider AWS.
terraform init
The output should be similar to what is shown below:
Initializing the backend...
Initializing provider plugins...
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Run terraform plan
to create an execution plan.
terraform plan -out main.tfplan
A long output of resources to be created will be printed. The bottom of the output should be similar to:
Plan: 19 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ EC2-private_ip = (known after apply)
+ EC2-public_ip = (known after apply)
+ bastionhost-public-ip = (known after apply)
────────────────────────────────────────────────────────────────────────────────────────────────
Saved the plan to: main.tfplan
Run terraform apply
to apply the execution plan to your cloud infrastructure. The command below creates all required infrastructure.
terraform apply main.tfplan
If prompted to confirm if you want to create AWS resources, answer yes
.
The bottom of the output should be similar to what is shown below:
Apply complete! Resources: 19 added, 0 changed, 0 destroyed
outputs:
EC2-private_ip = "10.1.2.76"
EC2-public_ip = "54.221.51.159"
bastionhost-public_ip = "54.152.41.75"
Make note of the outputs to identify your instances. This is particularly useful when having multiple instances.
Verify the instances setup on the AWS Console.
Go to EC2 -> instances you should see the following two instances running:
EC2-public_ip
and EC2-private_ip
outputs above.bastionhost-public_ip
output above.Click on the Instance IDs to display the Instance Summary view which includes more details about your instances.
Connect to a target server via a Jump Host using the -J
flag from the command line. This tells SSH to make a connection to the jump host and then establish a TCP forwarding to the target server from there. For example, if using a ubuntu
AMI:
ssh -J ubuntu@<jump-host-IP> ubuntu@<target-server-IP>
Replace <jump-host-IP>
with the public IP of the bastion VM and <target-server-IP>
with the private IP of the target VM.
Terminal applications such as PuTTY , MobaXterm and similar can be used.
Different Linux distributions have different default usernames you can use to connect.
Default usernames for AMIs are listed in a table. Find your operating system and see the default username you should use to connect.
The output is shown below. Once connected, you are now ready to use your instance.
ubuntu@ip-172-31-46-24:~/ $ ssh -J ubuntu@54.205.132.186 ubuntu@10.1.2.76
The authenticity of host '54.205.132.186 (54.205.132.186)' can't be established.
ED25519 key fingerprint is SHA256:LEP11QPanpvagrBEfz71C5111gUQjAUTtzIF8ovAdzT.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '54.205.132.186' (ED25519) to the list of known hosts.
The authenticity of host '10.1.2.76 (<no hostip for proxy command>)' can't be established.
ED25519 key fingerprint is SHA256:4FsZ5txilwvvrbaIkvdxJuznb6dKQiN2FSyd7/I/EtQ.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.1.2.76' (ED25519) to the list of known hosts.
Welcome to Ubuntu 22.04.1 LTS (GNU/Linux 5.15.0-1019-aws aarch64)
Run terraform destroy
to delete all resources created through Terraform, including resource groups, virtual networks, and all other resources.
terraform destroy
A long output of resources to destroy will be printed. If prompted to confirm if you want to destroy all resources, answer yes
.
The bottom of the output should be similar to:
Destroy complete! Resources: 19 destroyed.
Use the uname utility to verify that you are using an Arm-based server. For example:
uname -m
will identify the host machine as aarch64
.
Install the gcc
compiler. If you are using Ubuntu
, use the following commands. If not, refer to the
GNU compiler install guide
:
sudo apt-get update
sudo apt install -y gcc
Using a text editor of your choice, create a file named hello.c
with the contents below:
#include <stdio.h>
int main(){
printf("hello world\n");
return 0;
}
Build and run the application:
gcc hello.c -o hello
./hello
The output is shown below:
hello world