I love coding! I love writing! I love to learn, try, and play around with new AWS services. The problem is that I not always have access to my laptop to code on. I have tried GitHub CodeSpaces but didn't really like the experience. I needed something similar but different.
Goal
The goal was to create a setup that enables me to code from my Android tablet using VS Code. I needed access to a full fletched terminal with SSH, AWS CLI, Git and more. Therefor running it locally on the tablet is not an option, even if that was possible to install and run.
VS Code
VS Code is built from the start as a client and server part. Both the client and server part can run on the same computer, or on different virtual machines. It makes it possible to connect to a server part from basically anywhere, if that is a webclient or standalone.
This allows for remote development through different means, like SSH or a special tunnel mode.
Solution
With the understanding how VS Code is structured it should be fully possible to run the server part on an EC2 instance and connect to it from vscode.dev, using the special tunnel mode. It should the be possible to run this from my Android tablet. To run VS Code on the EC2 instance we will use the VS Code CLI
Create VPC and LaunchTemplate
First of all we need an EC2 instance running, and to have that let's start by creating a basic VPC using a basic Cloudformation template.
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: setup basic VPC
Parameters:
  ApplicationName:
    Type: String
  IPSuperSet:
    Type: String
    Description: The IP Superset to use for the VPC CIDR range, e.g 10.0
    Default: "10.0"
Resources:
  ##########################################################################
  #  VPC Base Infrastructure                                               #
  ##########################################################################
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      EnableDnsSupport: true
      EnableDnsHostnames: true
      CidrBlock: !Sub "${IPSuperSet}.0.0/16"
      Tags:
        - Key: Name
          Value: !Ref ApplicationName
  PublicSubnetOne:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: { Ref: "AWS::Region" }
      VpcId: !Ref VPC
      CidrBlock: !Sub ${IPSuperSet}.0.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${ApplicationName}-public-one
  PublicSubnetTwo:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: { Ref: "AWS::Region" }
      VpcId: !Ref VPC
      CidrBlock: !Sub ${IPSuperSet}.1.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${ApplicationName}-public-two
  ##########################################################################
  #  Gateways                                                              #
  ##########################################################################
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Ref ApplicationName
  GatewayAttachement:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
  ##########################################################################
  #  Route Tables & Routes                                                 #
  ##########################################################################
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub ${ApplicationName}-public-rt
  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: GatewayAttachement
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  PublicSubnetOneRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetOne
      RouteTableId: !Ref PublicRouteTable
  PublicSubnetTwoRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnetTwo
      RouteTableId: !Ref PublicRouteTable
Next we need to create an EC2 instance and an EBS volume. The EBS volume will be used to store all code, that way we can destroy the EC2 instance and keep all settings and code on the EBS volume. The EBS volume will be mounted during initial boot and also added to fstab so it get remounted if we reboot the instance. To make it easy to create new instances with the same configuration let's use a LaunchTemplate.
We will also install all required components during the initial boot, using a UserScript. We will run everything on Amazon Linux 2023.
We will be using Instance Connect to be able to connect to the instance. Therefor we must add the IP range for Instance Connect to our security group. To find the the IP range we can download the ip-ranges.json.
AWSTemplateFormatVersion: "2010-09-09"
Description: Base setup for VS Code EC2 resources
Parameters:
  AmiId:
    Type: String
    Default: ami-04b1c88a6bbd48f8e # AMI for Amazon Linux 2023 in eu-west-1
  InstanceType:
    Type: String
    Description: Type of instance to use for EC2 runners.
    Default: t3.large
  AvailabilityZone:
    Type: String
    Description: The availability zone to run in
    Default: eu-west-1a
  InfrastructureStackName:
    Type: String
    Description: The name of the stack with the Infrastructure resources
  ServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EC2 Security Group
      VpcId:
        Fn::ImportValue: !Sub ${InfrastructureStackName}:VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 18.202.216.48/29 # Instance Connect in eu-west-1
  SecurityGroupInboundAllowSelf:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref ServerSecurityGroup
      IpProtocol: tcp
      FromPort: "0"
      ToPort: "65535"
      SourceSecurityGroupId: !Ref ServerSecurityGroup
  InstanceRole:
    Type: AWS::IAM::Role
    Properties:
      Policies:
        - PolicyName: AllwoEC2Actions
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ec2:*
                Resource: "*"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action: sts:AssumeRole
  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref InstanceRole
  CodeEbsVolume:
    Type: AWS::EC2::Volume
    Properties:
      AvailabilityZone: !Ref AvailabilityZone
      Encrypted: false
      Size: 32
      VolumeType: gp3
  LaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        EbsOptimized: True
        IamInstanceProfile:
          Arn: !GetAtt EC2InstanceProfile.Arn
        ImageId: !Ref AmiId
        InstanceType: !Ref InstanceType
        SecurityGroupIds:
          - !GetAtt ServerSecurityGroup.GroupId
        UserData:
          Fn::Base64:
            Fn::Sub: |
              #!/bin/bash -xe
              sudo -s
              yum update -y
              yum install -y jq
              yum install git -y
              TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
              VOLUME_ID=${CodeEbsVolume}
              INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)
              REGION=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r .region)
              aws ec2 attach-volume --volume-id $VOLUME_ID --device /dev/xvdf --instance-id $INSTANCE_ID --region $REGION
              mkdir vscode-data
              chown -R ec2-user:ec2-user /vscode-data
              mount /dev/xvdf /vscode-data
              echo -e "/dev/xvdf /vscode-data xfs  defaults,nofail  0  2" >> /etc/fstab
              curl -Lk 'https://code.visualstudio.com/sha/download?build=stable&os=cli-alpine-x64' --output vscode_cli.tar.gz
              tar -xf vscode_cli.tar.gz
Start up the EC2 instance
Now let's use that LaunchTemplate to start an EC2 instance. This time let's head over to the AWS console to start it up.
Navigate to the EC2 section and select Launch Instance from template from the dropdown menu.
Start by selecting the LaunchTemplate we just created and the version to use, if there is more then one. In my case I have 6 versions and like to use the latest.
Now scroll down and ensure that the correct AMI is selected, it should be Amazon Linux 2023 AMI.
Finally select a subnet in the VPC we just created, it must the availability zone of the EBS volume we have created. In my case I have created the EBS volume in eu-west-1a so the instance need to be in a subnet in that AZ.
Leave everything else as default and the click Launch Instance
Start VSCode on the instance
With the instance running we now need to start VSCode. We have already downloaded the CLI in the user script. Let's start by connecting to the instance using Instance Connect.
Navigate to the instance and select Connect.
If connection is successful you should see a terminal like this.
Now we need to start VSCode server using the CLI, we do that with this command.
~/code  tunnel --accept-server-license-terms --name vscode-demo-tunnel
Adding --accept-server-license-terms to the command automatically accepts the license terms. We also need to give our tunnel a name, that is done with --name parameter. In the above command we give the tunnel the name vscode-demo-tunnel.
This should now give us a result like this.
What we now need to do is to grant server access, that is done by navigating to highlighted URL. This should take us to this page.
On this page we need to supply the code we were given when starting VSCode server.
After an activation we now need to authorize GitHub for the server.
If everything works we should end up at the success page.
Jumping back to the instance connect screen we should now see that the server is running, and a url to connect to it.
Now we can navigate to the highlighted URL, which is basically 'https://vscode.dev/tunnel/tunnel-name'. VSCode should load in the browser window and give us access to a full fledge VSCode editor with terminal access and everything.
In the lower left corner the connection status is fully visible.
That is what we need to do to get VS Code server running on an EC2 instance allowing us to connect to it from any browser. basically we have created our own hosted version of GitHub CodeSpaces.
But, there are still some improvements we can do.
Adding some extra automation
Connecting to the instance every time using Instance Connect to start the server is not that practical. Doing that from an Android tablet would work but I would rather try and automate that step a bit further.
What would be great is to be able to start VS Code server as a service that can run in the background. So first of all let us create a shell script, named vscodestart.sh, that will start the server.
#!/bin/sh
~/code  tunnel --accept-server-license-terms --name vscode-demo-tunnel
This is just the same command we used when we started it manually. Next we create a Linux service that we can start using systemd so let's create the service file named vscode.service.
[Unit]
After=network.target
[Service]
User=ec2-user
Group=ec2-user
ExecStart=/usr/local/bin/vscodestart.sh
[Install]
WantedBy=default.target
Now let's copy the vscode.service file to /etc/systemd/system/ and vscodestart.sh to /usr/local/bin/.
We could now start the server using command:
systemctl start vscode.service
This way VS Code server now can run in the background. OK this was now the first step in the automation. For the second step we create a AWS Systems Manager Document that we can run from the AWS CLI or the Console. The SSM Document will then run the start command on the EC2 instance starting the VS Code server service. So we add that to our CloudFormation template.
  StartVCodeServerDocument:
    Type: AWS::SSM::Document
    Properties:
      DocumentType: Command
      Content:
        schemaVersion: "2.2"
        description: Command Document for VS Code start server service
        mainSteps:
          - action: "aws:runShellScript"
            name: "startserver"
            inputs:
              runCommand:
                - sudo systemctl start vscode.service
With the SSM Document in place we can jump into the console and run the Document on our Instance. Even if this is not a fully automated solution it's a step in the right direction. I'm now able to easy start VS Code server on an EC2 instance and write code from any device as long as I have access to an browser.
Gotchas
During the project there was some gotchas and things we must consider. First of all, the very first time a server is started on an EC2 instance we must activate it using the code generated by VS Code server. This makes a fully automated solution a bit harder as we need to connect to the instance the very first time.
We must also consider that there is a limit of 5 Tunnels per GitHub account. That mean that if we start a sixth the first Tunnel will be recycled.
The service terms also only allow the use of VS Code server for personal use or within a company. You are not allowed to host and run it as SaaS solution.
Conclusion and next step
By running VS Code server on an EC2 instance I own I'm now in full control of the cost for it, what access it has, and it give me the possibility to clone all of my repos if I so like. I can now basically write code for any of my project, with very little effort and cost, from any device as long as I can run a browser and have access to internet. This now give me the possibility to code or blog from my Android tablet when I'm on the bus, or anywhere that I don't have access to my laptop.
So what are the next steps?
I'm going to create a fully automated setup where I can start and stop server instances from a webpage. This solution will of course be serverless in all parts expect for the EC2 instance it self.
As you saw I'm running the EC2 instance in a public subnet, this due to fact that I need to connect to it the first time I start the server. With the new Instance Connect Endpoint I would be able to connect to the instance even if it's in a private subnet.
Final Words
This was a really fun project. It's really great to see how much you can accomplish with very little code and effort. Stay tuned for more.
Don't forget to follow me on LinkedIn and Twitter for more content, and read rest of my Blogs
 
 
              
 
                      











 
    
Top comments (1)
Tunnels are great. Another one I use is OpenVSCode Server. It also works great using a browser from any machine.