Establishing a Secure Remote Development Environment with AWS EC2 and Terraform
Tags: terraform, aws, development, tutorial
Published: 2023-11-17 13:52:08

### Introduction

**Remote development environments (RDEs)** allow software engineers to develop and deploy software remotely rather than on their local machine. RDEs are popular among software engineers for several reasons, including company security policies, requirements for specific resources, access to internal resources, and the ability to develop from different devices.

This article provides a complete guide on setting up an RDE using an Amazon EC2 instance. We will cover everything from choosing the right instance type to troubleshooting common problems. By the end of this article, you will have a fully configured RDE that you can use to develop and deploy software from anywhere in the world.

### Setting up AWS EC2 Instance

We will use Terraform to create an Amazon EC2 instance with EBS volume.  [Terraform](https://www.terraform.io/) is an [infrastructure as code](https://en.wikipedia.org/wiki/Infrastructure_as_code) (IaC) tool used for building, changing, and versioning infrastructure through code. This can also be accomplished using the AWS Console or AWS SDK.

#### Setup Network Infrastructure 

Before provisioning an Amazon EC2 instance, we must establish the necessary network infrastructure. This process involves creating a VPC, subnet, and route table and configuring an internet gateway. If you already have it, you can skip this step. 


```javascript
### VPC  
resource "aws_vpc" "vpc-dev" {  
  cidr_block            = "10.10.0.0/21"  
  enable_dns_support   = true  
  enable_dns_hostnames = true  
  tags                 = merge(tomap({  
    Name = "dev-vpc",  
  }), local.common_tags)  
}  
  
### Public subnet  
resource "aws_subnet" "public-subnet" {  
  cidr_block              = "10.10.1.0/24"  
  vpc_id                  = aws_vpc.vpc-dev.id  
  availability_zone       = "eu-west-2a"  
  map_public_ip_on_launch = true  
  
  tags = merge(tomap({  
    Name = "dev-public"  
  }), local.common_tags)  
}  
  
### Route tables for the subnet
resource "aws_route_table" "public-route-table-dev" {  
  vpc_id = aws_vpc.vpc-dev.id  
  tags   = merge(tomap({  
    Name = "public-route-table",  
  }), local.common_tags)  
}  
  
### Associate the newly created route tables to the subnet
resource "aws_route_table_association" "public-route-association" {  
  route_table_id = aws_route_table.public-route-table-dev.id  
  subnet_id      = aws_subnet.public-subnet.id  
}  

### Internet Gateway for the public subnet  
resource "aws_internet_gateway" "igw-dev" {  
  vpc_id = aws_vpc.vpc-dev.id  
  tags   = merge(tomap({  
    Name = "publick-igw",  
  }), local.common_tags)  
}  
  
### Route the public subnet traffic through the Internet Gateway  
resource "aws_route" "public-internet-igw-route-dev" {  
  route_table_id         = aws_route_table.public-route-table-dev.id  
  gateway_id             = aws_internet_gateway.igw-dev.id  
  destination_cidr_block = "0.0.0.0/0"  
}
```

#### Create Security Group

We'll now create an AWS Security Group to ensure secure network access to an AWS EC2 instance.

```javascript
### Dev Environment Security group (traffic Local ssh -> EC2) 
resource "aws_security_group" "dev_environment" {  
  name        = "Dev Environment Security Group"  
  description = "Allows inbound access from the SSH only"  
  vpc_id      = aws_vpc.vpc-dev.id  
  
  # For connecting to your EC2 from your local machine, you need to add your IP address  
  ingress {  
    from_port   = 22  
    to_port     = 22  
    protocol    = "tcp"  
    cidr_blocks = [  
      "192.168.0.1/32"  
    ]  
  }  
  
  egress {  
    from_port   = 0  
    to_port     = 0  
    protocol    = "-1"  
    cidr_blocks = [  
      "0.0.0.0/0"  
    ]  
  }  
  tags = merge(tomap({  
    Name = "Dev Environment Security Group"  
  }), local.common_tags)  
}  
```

This security group restricts SSH connections to only allow access from IP 192.168.0.1. To enable access from your public IP, replace 192.168.0.1 with your public IP address. If you want to allow SSH connections from any IP, replace 192.168.0.1 with 0.0.0.0.

#### Create EC2 Instance

With the network infrastructure in place, we're ready to create an AWS EC2 instance for our remote development environment.

Ubuntu 22.04 LTS is used in this article because it is a popular and well-supported operating system for remote development environments. However, you can use any operating system that meets your needs.

Suppose you use IntelliJ IDEA for remote development. To achieve optimal performance and low latency in your remote development environment (RDE), I recommend running an Amazon EC2 instance of type t3.large or r5.large. However, if you are not planning to use IntelliJ IDEA, you can choose a type that suits your needs. Additionally, we will be using a persistent EBS volume for data storage.

```javascript
resource "aws_key_pair" "ssh_key_dev_environment" {  
  key_name   = "ssh_key_dev_environment"  
  public_key = "ssh-rsa AAAAB3Nza"  
}  
  
# instance data  
data "aws_ami" "ubuntu_22_04" {  
  most_recent = true  
  
  filter {  
    name   = "name"  
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]  
  }  
  
  filter {  
    name   = "architecture"  
    values = ["x86_64"]  
  }  
  owners = ["099720109477"] # Canonical  
}  
  
locals {  
  development_environment_user_data = <<EOT  
#!/bin/bash  
# on_cloud_init.sh  
# ----------------------------------------------------------------------------  
# This script is passed as user data during EC2 launch and executed  
# when the EC2 instance is booted for the first time.  
# Since all user data scripts are executed as root there's not need for sudo  
# ----------------------------------------------------------------------------  
set -eu  
  
installPackages() {  
  echo '*** Installing packages ***'  
  sudo apt-get update  
  sudo apt-get install awscli net-tools mc -y  
}  
  
installPackages  
  
export AWS_REGION=eu-central-1  
export PROJECT_DATA_VOLUME_MARKER=/data/.data_volume  
  
mountDataVolume() {  
  
  DATA_BLOCK_DEVICE=/dev/xvdf  
  echo '*** Mounting data volume ***'  
  
  echo "Wait for data volume to be attached"  
  while ! lsblk $DATA_BLOCK_DEVICE ; do  
    echo "/dev/xvdf is not attached yet. Waiting 5 seconds"  
    sleep 5  
  done  
  
  
  echo "Check if filesystem ext is on data volume"  
  if [ "$(lsblk -f $DATA_BLOCK_DEVICE -o FSTYPE -n)" == "ext4" ]  
  then  
    echo "filesystem ext4 is already on data volume"  
  else  
    echo "creating filesystem ext4 on data volume"  
    sudo mkfs -t ext4 $DATA_BLOCK_DEVICE  
  fi  
  
  echo "mount data volume at /data"  
  sudo mkdir -p /data  
  echo "UUID=$(blkid -s UUID -o value $DATA_BLOCK_DEVICE)  /data  ext4  defaults,nofail  0  2" | sudo tee /etc/fstab -a  
  mount -a  
  sudo mount | grep '/data'  
}  
  
echo "Check if data volume is already mounted"  
if [ -e "$PROJECT_DATA_VOLUME_MARKER" ]  
then  
  echo '*** project data volume already mounted ***'  
else  
  mountDataVolume  
  echo "Marking data volumes as mounted"  
  echo "DO NOT DELETE OR RENAME THIS FILE\!" > $PROJECT_DATA_VOLUME_MARKER  
fi  
  
EOT  
}  
  
resource "aws_instance" "development_environment" {  
  ami           = data.aws_ami.ubuntu_22_04.id  
  instance_type = "t2.micro"  
  
  
  subnet_id = aws_subnet.public-subnet.id  
  
  vpc_security_group_ids = [  
    aws_security_group.dev_environment.id,  
  ]  
  
  
  root_block_device {  
    volume_size = 10  
    volume_type = "gp3"  
  }  
  
  user_data            = local.development_environment_user_data  
  
  key_name   = aws_key_pair.ssh_key_dev_environment.key_name  
  monitoring = false  
  
  tags = merge(tomap({  
    Name = "Development Environment"  
  }), local.common_tags)  
}  
  
  
# Create and attache EBS  
  
resource aws_ebs_volume application_data {  
  availability_zone = "eu-west-2a"  
  encrypted         = true  
  size              = 100  
  type              = "gp3"  
  tags              = merge({  
    Name    = "vol-remote-dev-data"  
  }, local.common_tags)  
}  
  
resource aws_volume_attachment env_data {  
  device_name = "/dev/sdf"  
  instance_id = aws_instance.development_environment.id  
  volume_id   = aws_ebs_volume.application_data.id  
}
```

Please replace the provided SSH public key with your own. You can apply the desired changes and complete the EC2 instance provisioning process.

```bash
$ terraform plan
$ terraform apply
```

Congratulations! You have successfully provisioned an EC2 instance suitable for remote development. To connect to the VM, utilise its public IP address.

### Connecting to a Remote Environment using an IDE:

IntelliJ IDEA and Visual Studio Code are popular Integrated Development Environments (IDEs) supporting remote development. In this article, we will demonstrate using IntelliJ IDEA. Nevertheless, setting up Visual Studio Code is straightforward and should pose no issues.

IntelliJ IDEA supports two workflows for remote development: server-to-client and client-to-server.

The client-to-server workflow is straightforward:

_Choose the "Remote Development" option from the IntelliJ IDEA menu._
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vs6axi33honp1u3shqlx.png)

_Select a connection via SSH and specify the SSH connection details to establish a connection with your remote instance._
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3bm47jg391789hy7glj6.png)

IntelliJ IDEA will automatically install the necessary IDE components on your remote instance, and you will be ready to start developing. Configuring the server-to-client can be found in the [official documentation](https://www.jetbrains.com/help/idea/remote-development-overview.html#workflow).

### Tips

#### Easy SSH Access 

EC2 instances are assigned dynamic IP addresses by default, meaning the IP address changes each time the instance is stopped and restarted. Consistent instance access can be challenging, especially when working remotely for development purposes. You can address this issue using Elastic IP or a VPN tunnel.

Elastic IP provides a static IP address that can be associated with your EC2 instance. It ensures the instance has the same IP address, even if it's stopped and restarted. At the same time, Elastic IP incurs a monthly fee.

If you prefer to avoid the additional cost of Elastic IP, you can create a VPN tunnel between your local machine and the EC2 instance. This tunnel establishes a secure private network connection, allowing you to access the instance as if it were on your local network. For this scenario, I recommend using Tailscale, a user-friendly and free option for up to 100 devices.

#### Port Forwarding

With IntelliJ IDEA, you can effortlessly establish port forwarding from your EC2 instance to your local machine. Alternatively, if you prefer to configure a port forwarding tunnel manually, utilise the following command:

```
ssh -N -L <LOCAL PORT>:localhost:<REMOTE PORT> ubuntu@<IP ADDRESS>
```


### Conclusion

This guide covered the process of establishing a remote development environment using AWS EC2 instances and Terraform. The remote development environment offers the flexibility to work from anywhere, eliminates hardware limitations, and enables collaboration among developers. The possibilities are endless, and the power of remote development lies in its adaptability and flexibility.