こんにちは、広野です。
AWS Cloud9 亡き後の IDE 環境をいろいろと考えてまして。結局のところシンプルな構成に落ち着きました。作成した構成を紹介します。自分の PC に VSCode をインストールしている人には全く役に立たない記事ですのでご放念ください。
ざっくり要件
- AWS Cloud9 の代わりとなる、クラウド上の IDE を構築したい。
- リモートアクセスはブラウザで (HTTPS で) で通信できるようにしたい。(特殊なポートは NG)
- HTTPS 通信で自己証明書を使用するのは NG。
- OS は Windows 以外にしたい。ライセンス面や価格面など、いろいろと。
アーキテクチャ
- VSCode をインストールした Ubuntu 24.04 arm64 にブラウザから GUI アクセスする環境です。
- EC2 には Elastic IP アドレスを割り当て、それをあらかじめ用意してある Amazon Route 53 パブリックホストゾーンに A レコード登録します。そのドメインおよびサブドメインに合わせて、Let’s Encrypt で SSL 証明書を作成します。証明書の有効期限は 90 日なので、更新するためのスクリプトを EC2 内に作成します。(自動更新ではなく、必要になったら実行)
- リモートアクセス機能は Amazon DCV が提供します。ただし 8443 ポートを使用するので、一応そのポートはそのまま利用できるようにしておき、8443 にアクセスできない環境のために 443 ポートでもアクセスできるよう、nginx でリバースプロキシを構築します。
- SSL 証明書は、Amazon DCV および nginx にカスタム証明書として組み込みます。
- 誰でもアクセスできるのはまずいので、とりあえず指定したソース IP アドレスでないと許可しないようセキュリティグループを設定します。セキュリティ面は引き続き強化を検討します。研修用途であれば現状で問題ないと思います。
要件から考えたこと
- AWS Cloud9 の代わりに、クラウド上の IDE を使用したい。
やはり VSCode もしくは VSCode 準拠のツールにしたいです。まずは純正 VSCode 使用で試行錯誤しています。そのため Amazon EC2 上で動く環境を作りました。今後は code-server も検証したいです。
- リモートアクセスはブラウザで (HTTPS で) で通信できるようにしたい。(特殊なポートは NG)
RDP や VNC ではなく、ブラウザでリモートのデスクトップを操作できる Amazon DCV を採用しました。会社のネットワーク通信の制約で、特殊ポートの通信ができない場合も 443 であれば通過できます。
- HTTPS 通信で自己証明書を使用するのは NG。
会社のネットワークセキュリティ等で、自己証明書の Web サイトにはアクセスできないことがあり、正規の SSL 証明書を用意する必要がありました。EC2 に ALB や Amazon CloudFront をかぶせることも考えましたが、コスト面を気にしたのと、CloudFront だと自己証明書のオリジンをサポートしていないために結局オリジン (EC2) に正規の証明書を実装せねばならず、採用をやめました。
- OS は Windows 以外にしたい。
OS が Windows であれば AWS Systems Manager Fleet Manager によるリモートアクセスはできたのですが、Windows だと高額だったり、会社のルールでセキュリティ対策などしないといけないので、Linux にしました。Linux の中でも、AWS で馴染み深い Amazon Linux 2023 を使用したかったのですが、本来サーバー用途での利用を想定された OS なのでデスクトップが使いにくく、次に馴染みのある Ubuntu にしました。Mint も考えましたが、純正 AMI が無かったのでやめました。インスタンスタイプは Graviton (arm64) を使用した t4g の方が t3 (x86_64) よりも若干安かったので、t4g を採用しています。幸い、使用するツールも arm64 に対応してました。
AWS CloudFormation テンプレート
補足はインラインコメントします。
EC2 ユーザーデータの中で以下のツールをインストールしているのですが、必要に応じて追加、削除しましょう。
- VSCode
- Amazon DCV
- nginx
- AWS CLI
- Amazon Q Developer CLI (サインインが必要なのでインストーラダウンロードのみ)
- Let’s Encrypt 証明書作成用モジュール
- NVM (Node.js バージョン管理ツール)
AWSTemplateFormatVersion: "2010-09-09"
Description: The CloudFormation template that creates Ubuntu 24.04 ARM64 EC2 instance with GNOME 3, VSCode, and Amazon DCV.
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
SystemName:
Type: String
Description: System name. use lower case only. (e.g. example)
Default: example
MaxLength: 10
MinLength: 1
SubName:
Type: String
Description: System sub name. use lower case only. (e.g. prod or dev)
Default: dev
MaxLength: 10
MinLength: 1
DomainName:
Type: String
Description: Domain name for URL. xxxxx.xxx
Default: example.com
MaxLength: 40
MinLength: 5
# VPC は既存にあるものを使用する想定です。
VpcId:
Type: AWS::EC2::VPC::Id
Description: Choose a existing VPC ID you deploy the EC2 instance in.
InstanceSubnet:
Type: AWS::EC2::Subnet::Id
Description: Choose an existing Public Subnet ID you deploy the EC2 instance in.
InstanceType:
Type: String
Description: EC2 instance type (ARM/Graviton)
Default: t4g.large
AllowedValues:
- t4g.large
- t4g.xlarge
InstanceVolumeSize:
Type: Number
Description: The volume size in GB
Default: 20
# アクセスを許可するソースIPアドレス(サブネット)です。
AllowedSubnet:
Type: String
Description: Allowed source IPv4 subnet and subnet mask. (e.g. xxx.xxx.xxx.xxx/32)
Default: xxx.xxx.xxx.xxx/32
MaxLength: 18
MinLength: 9
TimeZone:
Type: String
Description: The specified time zone.
Default: Asia/Tokyo
MaxLength: 30
MinLength: 1
# ubuntu ユーザーの初期パスワードです。Amazon DCV のパスワードにもなります。リソース作成後は変更しましょう。
InitialPassword:
Type: String
Description: The initial OS password. You must change it after your first login.
Default: Gx8jeTLc
MaxLength: 128
MinLength: 8
# Route 53 パブリックホストゾーンは既存で持っている想定です。
Route53HostZoneId:
Type: String
Description: Route 53 public host zone ID for the SSL certificate.
Default: XXXXXXXXXXXXXXXXXXXXX
MaxLength: 30
MinLength: 1
# Let's Encrypt は証明書作成時にメールアドレスが必要になります。
YourEmail:
Type: String
Description: Your email address for SSL certificate application.
Default: xxx@xxx.xxx
MaxLength: 30
MinLength: 1
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "General Configuration"
Parameters:
- SystemName
- SubName
- Label:
default: "Domain Configuration"
Parameters:
- DomainName
- Route53HostZoneId
- YourEmail
- Label:
default: "Network Configuration"
Parameters:
- VpcId
- InstanceSubnet
- AllowedSubnet
- Label:
default: "EC2 Configuration"
Parameters:
- InstanceType
- InstanceVolumeSize
- Label:
default: "OS Configuration"
Parameters:
- TimeZone
- InitialPassword
Resources:
# ------------------------------------------------------------#
# EC2 Launch template
# ------------------------------------------------------------#
EC2LaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateName: !Sub vscode-${SystemName}-${SubName}
LaunchTemplateData:
InstanceType: !Ref InstanceType
ImageId: >-
{{resolve:ssm:/aws/service/canonical/ubuntu/server/24.04/stable/current/arm64/hvm/ebs-gp3/ami-id}}
BlockDeviceMappings:
- Ebs:
VolumeSize: !Ref InstanceVolumeSize
VolumeType: gp3
DeleteOnTermination: true
Encrypted: true
DeviceName: /dev/sda1
NetworkInterfaces:
- SubnetId: !Ref InstanceSubnet
Groups:
- !Ref Ec2SecurityGroup
DeviceIndex: 0
AssociatePublicIpAddress: false
MetadataOptions:
HttpTokens: required
Monitoring:
Enabled: true
TagSpecifications:
- ResourceType: volume
Tags:
- Key: Cost
Value: !Sub ${SystemName}-${SubName}
DependsOn:
- Ec2SecurityGroup
# ------------------------------------------------------------#
# EC2 Security Group
# ------------------------------------------------------------#
Ec2SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
VpcId: !Ref VpcId
GroupDescription: Allow dcv-server access from specified subnets
SecurityGroupIngress:
- Description: Allow HTTPS (8443) from the specified client
FromPort: 8443
IpProtocol: tcp
ToPort: 8443
CidrIp: !Ref AllowedSubnet
- Description: Allow QUIC from the specified client
FromPort: 8443
IpProtocol: udp
ToPort: 8443
CidrIp: !Ref AllowedSubnet
- Description: Allow HTTPS from the specified client
FromPort: 443
IpProtocol: tcp
ToPort: 443
CidrIp: !Ref AllowedSubnet
SecurityGroupEgress:
- Description: Allow all outbound traffic
IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Cost
Value: !Sub ${SystemName}-${SubName}
- Key: Name
Value: !Sub SG-vscode-${SystemName}-${SubName}
# ------------------------------------------------------------#
# EC2 Role / Instance Profile (IAM)
# ------------------------------------------------------------#
Ec2Role:
Type: AWS::IAM::Role
Metadata:
cfn_nag:
rules_to_suppress:
- id: W11
reason: CodeWhisperer requires '*' as a resource, reference https://docs.aws.amazon.com/codewhisperer/latest/userguide/cloud9-setup.html#codewhisperer-IAM-policies
Properties:
RoleName: !Sub Ec2Role-vscode-${SystemName}-${SubName}
Description: This role allows EC2 instance to invoke S3 and SSM.
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
Path: /
# EC2 インスタンスには、とりあえず私が必要だった権限しか付与していません。用途に応じて追加が必要です。
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- arn:aws:iam::aws:policy/AWSCodeCommitPowerUser
- arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
Policies:
- PolicyName: !Sub Ec2Policy-vscode-${SystemName}-${SubName}
PolicyDocument:
Version: 2012-10-17
Statement:
# Amazon DCV のライセンスを読み取るために必要な権限です。
- Effect: Allow
Action: s3:GetObject
Resource: !Sub arn:aws:s3:::dcv-license.${AWS::Region}/*
# Amazon Q Developer 関連
- Effect: Allow
Action:
- codewhisperer:GenerateRecommendations
Resource: "*"
# 以下は Let's Encrypt への証明書申請のために必要です。DNSによるドメイン所有者確認をします。
- Effect: Allow
Action:
- route53:ListHostedZones
- route53:GetChange
Resource:
- "*"
- Effect: Allow
Action:
- route53:ChangeResourceRecordSets
Resource:
- !Sub arn:aws:route53:::hostedzone/${Route53HostZoneId}
Ec2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Ref Ec2Role
Path: /
Roles:
- !Ref Ec2Role
DependsOn:
- Ec2Role
# ------------------------------------------------------------#
# Elastic IP address for EC2 instance
# ------------------------------------------------------------#
EipEc2:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
Tags:
- Key: Cost
Value: !Sub ${SystemName}-${SubName}
- Key: Name
Value: !Sub eip-vscode-${SystemName}-${SubName}
# ------------------------------------------------------------#
# EIP Association
# ------------------------------------------------------------#
EIPAssociation:
Type: AWS::EC2::EIPAssociation
Properties:
AllocationId: !GetAtt EipEc2.AllocationId
InstanceId: !Ref Ec2Instance
DependsOn:
- Ec2Instance
- EipEc2
# ------------------------------------------------------------#
# EC2
# ------------------------------------------------------------#
Ec2Instance:
Type: AWS::EC2::Instance
Properties:
IamInstanceProfile: !Ref Ec2InstanceProfile
LaunchTemplate:
LaunchTemplateId: !Ref EC2LaunchTemplate
Version: !GetAtt EC2LaunchTemplate.LatestVersionNumber
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -euxo pipefail
export DEBIAN_FRONTEND=noninteractive
cd /root
# User password
echo "ubuntu:${InitialPassword}" | chpasswd
# Locale & timezone
timedatectl set-timezone ${TimeZone}
apt update -y
apt upgrade -y
apt install -y language-pack-ja-base language-pack-ja ibus-mozc unzip
update-locale LANG=ja_JP.UTF-8
sed -i 's/^XKBLAYOUT=".*"/XKBLAYOUT="jp"/' /etc/default/keyboard
dpkg-reconfigure -phigh console-setup
systemctl restart console-setup
# AWS CLI
curl https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip -o awscliv2.zip
unzip awscliv2.zip
./aws/install
rm awscliv2.zip
rm -fr aws
# ubuntu desktop
apt install -y ubuntu-desktop gdm3 xserver-xorg-video-dummy mesa-utils
sed -i -e "s/^#WaylandEnable=false/WaylandEnable=false/g" /etc/gdm3/custom.conf
systemctl restart gdm3
dpkg-reconfigure gdm3
systemctl set-default graphical.target
systemctl isolate multi-user.target
systemctl isolate graphical.target
tee /etc/X11/xorg.conf << EOF Section "Device" Identifier "DummyDevice" Driver "dummy" Option "UseEDID" "false" VideoRam 512000 EndSection Section "Monitor" Identifier "DummyMonitor" HorizSync 5.0 - 1000.0 VertRefresh 5.0 - 200.0 Option "ReducedBlanking" EndSection Section "Screen" Identifier "DummyScreen" Device "DummyDevice" Monitor "DummyMonitor" DefaultDepth 24 SubSection "Display" Viewport 0 0 Depth 24 Virtual 4096 2160 EndSubSection EndSection EOF systemctl isolate multi-user.target systemctl isolate graphical.target # VSCode apt install -y gpg apt-transport-https software-properties-common wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/
sh -c 'echo "deb [arch=arm64] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'
apt update -y
apt install -y code
# Amazon DCV
wget https://d1uj6qtbmh3dt5.cloudfront.net/NICE-GPG-KEY
gpg --import NICE-GPG-KEY
wget https://d1uj6qtbmh3dt5.cloudfront.net/nice-dcv-ubuntu2404-aarch64.tgz
tar -xvzf nice-dcv-ubuntu2404-aarch64.tgz
cd nice-dcv-2024.0-19030-ubuntu2404-aarch64
apt install -y ./nice-dcv-server_2024.0.19030-1_arm64.ubuntu2404.deb ./nice-dcv-web-viewer_2024.0.19030-1_arm64.ubuntu2404.deb ./nice-xdcv_2024.0.654-1_arm64.ubuntu2404.deb
usermod -aG video dcv
systemctl enable dcvserver
cd /root
rm nice-dcv-ubuntu2404-aarch64.tgz
rm -fr nice-dcv-2024.0-19030-ubuntu2404-aarch64
sed -i "/^\[session-management\/automatic-console-session/a owner=\"ubuntu\"\nstorage-root=\"%home%\"" /etc/dcv/dcv.conf
sed -i "s/^#create-session/create-session/g" /etc/dcv/dcv.conf
# Create SSL cert
apt install -y software-properties-common certbot python3-certbot-dns-route53
certbot certonly --dns-route53 -d vscode-${SystemName}-${SubName}.${DomainName} --agree-tos -m ${YourEmail} --non-interactive --preferred-challenges dns-01
cp /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/fullchain.pem /etc/dcv/dcv.pem
cp /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/privkey.pem /etc/dcv/dcv.key
chown dcv:dcv /etc/dcv/dcv.pem /etc/dcv/dcv.key
chmod 600 /etc/dcv/dcv.pem /etc/dcv/dcv.key
# Install nginx
apt install -y nginx
systemctl enable --now nginx
mkdir -p /etc/nginx/ssl
chmod 700 /etc/nginx/ssl
cp /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/fullchain.pem /etc/nginx/ssl/dcv-chain.crt
cp /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/privkey.pem /etc/nginx/ssl/dcv.key
chmod 600 /etc/nginx/ssl/*.key
chmod 644 /etc/nginx/ssl/*.crt
tee /etc/nginx/conf.d/dcv.conf << EOF
server {
listen 443 ssl;
server_name vscode-${SystemName}-${SubName}.${DomainName};
ssl_certificate_key /etc/nginx/ssl/dcv.key;
ssl_certificate /etc/nginx/ssl/dcv-chain.crt;
location / {
proxy_pass https://127.0.0.1:8443;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_ssl_verify_depth 2;
proxy_ssl_name vscode-${SystemName}-${SubName}.${DomainName};
proxy_ssl_server_name on;
}
}
EOF
systemctl restart nginx
# Create cert renew script
cat << 'EOF' > /home/ubuntu/renew-cert.sh
#!/bin/bash
set -e
sudo certbot renew --cert-name "vscode-${SystemName}-${SubName}.${DomainName}"
sudo cp -f /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/fullchain.pem /etc/dcv/dcv.pem
sudo cp -f /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/privkey.pem /etc/dcv/dcv.key
sudo chown dcv:dcv /etc/dcv/dcv.pem /etc/dcv/dcv.key
sudo chmod 600 /etc/dcv/dcv.pem /etc/dcv/dcv.key
sudo cp -f /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/fullchain.pem /etc/nginx/ssl/dcv-chain.crt
sudo cp -f /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/privkey.pem /etc/nginx/ssl/dcv.key
sudo chmod 600 /etc/nginx/ssl/dcv.key
sudo chmod 644 /etc/nginx/ssl/dcv-chain.crt
sudo systemctl restart nginx
echo "Certificate renewed."
EOF
chmod +x /home/ubuntu/renew-cert.sh
chown ubuntu:ubuntu /home/ubuntu/renew-cert.sh
# Install nvm
sudo -u ubuntu bash -c "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash"
# Download Amazon Q Developer CLI
sudo -i -u ubuntu bash << EOF
curl --proto '=https' --tlsv1.2 -sSf "https://desktop-release.q.us-east-1.amazonaws.com/latest/q-aarch64-linux.zip" -o "q.zip"
unzip q.zip
EOF
# Finally, reboot
reboot
Tags:
- Key: Cost
Value: !Sub ${SystemName}-${SubName}
- Key: Name
Value: !Sub vscode-${SystemName}-${SubName}
DependsOn:
- Ec2InstanceProfile
- Route53RecordA
- S3BucketMyDocuments
# ------------------------------------------------------------#
# Route 53
# ------------------------------------------------------------#
Route53RecordA:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneName: !Sub ${DomainName}.
Name: !Sub vscode-${SystemName}-${SubName}.${DomainName}.
Type: A
TTL: 300
ResourceRecords:
- !GetAtt EipEc2.PublicIp
DependsOn: EipEc2
# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
# EIP
DcvServerUrl8443:
Value: !Sub "https://vscode-${SystemName}-${SubName}.${DomainName}:8443"
DcvServerUrl:
Value: !Sub "https://vscode-${SystemName}-${SubName}.${DomainName}"
リソース作成後の動作
出来上がりはこんな感じです。
AWS CloudFormation テンプレートの出力に、ブラウザアクセス用の URL があります。ubuntu ユーザーでログインします。ユーザーデータの実行に時間がかかるので、ブラウザアクセスはスタックの完了後 10 分程度待った方がいいです。気になる方は /var/log/cloud-init-output.log で状況を確認してください。

再度 ubuntu のログイン画面が表示されるので、ログインします。
VSCode も起動できました。クリップボードの共有もできます。
passwd コマンドで忘れずにパスワード変更しておきましょう。

ubuntu ユーザーのホームディレクトリに、Let’s Encrypt の証明書更新用スクリプト renew-cert.sh を作ってあります。これを実行すると証明書を更新してくれますが、有効期限が 30 日を切らないと更新できないようです。オプションで強制更新する設定もありましたが、そこまでするのはやめました。
まとめ
いかがでしたでしょうか?
要件によってカスタマイズすることで、さらにいろいろなバリエーションを作れそうです。今後この環境を使って Kiro を試したいと思っています。
AWS Cloud9 は軽い点が良かったのですが、VSCode は重いですね・・・。インスタンスタイプが large 以上じゃないときついです。
本記事が皆様のお役に立てれば幸いです。




