こんにちは、広野です。
AWS Cloud9 は研修用途では非常に使い勝手が良かったのですが、AWS が新規アカウントへの提供を終了してしまいました。今回は私が試みた代替ソリューションの実装編です。本記事は Application Load Balancer に対してリスナールールとターゲットグループを作成し、Amazon EC2 インスタンスを関連付けます。



アーキテクチャ
アーキテクチャ概要編で、以下の図を紹介しておりました。
- code-server サーバを配置する VPC です。何の変哲もない一般的なサブネット構成にしています。
- NAT Gateway は課金節約のため、1 つのパブリックサブネットにしか配置していません。
- code-server サーバは 1 ユーザーあたり 1 台を割り当てます。図では 1 つしかありませんが、人数分作成される想定です。ALB はユーザー全体で共用します。
- code-server のログインパスワードはインストール時に設定しますが、AWS Secrets Manager で生成したものを使用します。
- ALB には HTTPS アクセスさせるため、図には書いていませんが独自ドメインを使用します。そのための SSL 証明書はそのリージョンの AWS Certificate Manager で作成されていることが前提になります。
- ALB から EC2 インスタンスへの通信は 80 番ポート (HTTP) を使用します。
- ALB はインターネットに公開されますが、研修用途を想定して会社のソース IP アドレスからのみアクセスできるようにセキュリティグループを設定しています。
前提
前回記事を参考に、ALB や Amazon EC2 起動テンプレート等を作成してください。
AWS CloudFormation テンプレートの構成
テンプレートは 3 つに分けています。
- VPC
- Application Load Balancer
- Amazon EC2 インスタンス ← 今回はここ
テンプレートの範囲を図にすると、以下のようになります。ここに書かれているリソースが作成されます。
- 作成済みの ALB に、ターゲットグループごとにリスナールールを作成します。このリスナールールは、/ide1/ というパスであれば User 1 用のターゲットグループ (= User 1 用の code-server サーバ) にトラフィックをルーティングします。上限を 100 としているのは、ALB のクォータで 1 台の ALB に対してリスナールールを 100 個までしか作成できないためです。
- 今回のテンプレートで、Amazon EC2 インスタンスを作成します。起動テンプレートやセキュリティグループは共用しているので、前回記事のテンプレート内で作成しています。
- AWS Secrets Manager は code-server へのログインパスワードを生成、Amazon EC2 インスタンスから取得するために使用しています。
Amazon EC2 インスタンスの中の構成
アーキテクチャ概要編でも説明しましたが、以下の図のように Amazon EC2 インスタンスには nginx と code-server をインストールします。インストールには userdata スクリプトを使用します。userdata スクリプトも後述する AWS CloudFormation テンプレートの中に含まれていますので、その中で細かい補足をします。
パスの変換について
上の図のように例えば /ide2/ で渡されたパスを / に変換するようにしています。/ide2/ は ALB のパスベースルーティングのためにこちらが勝手に追加したパスなので、code-server が知らないパスだからです。
nginx の設定ファイルは userdata スクリプトの中で作成しており、その設定ファイルの中で以下の記述があります。
location /ide${InstanceId}/
${InstanceId} はテンプレートのパラメータとして入力する、ユーザーを一意に扱うための 1 – 100 の数字になります。つまり、パスベースルーティングの判断条件となる ide1 や ide2 などのパス名になります。
nginx が、これを受けて code-server に / としてトラフィックをパスするときには、location の後に /ide1/ のようにパス名を / で囲むように書くと機能します。
Amazon EC2 インスタンスの IAM ロールについて
テンプレートでは、以下の権限を付与しています。必要に応じて追加が必要です。
- AWS Systems Manager と連携する権限。セッションマネージャ経由でログインできます。
- AWS CodeCommit と連携する権限。
- Amazon CloudWatch と連携する権限。
- Amazon S3 バケット (前回記事で作成したログ保存用バケット) に書き込む権限。
- AWS CDK と連携する権限
- Amazon Q Developer と連携する権限
- AWS Secrets Manager で、自分が作成したシークレットにアクセスする権限。
AWS Secrets Manager のリソースベースポリシーについて
AWS Secrets Manager で作成したシークレットには、リソースベースポリシーを作成しています。自分のシークレットを他人に見られないようにするのが目的です。以下の設定が入っています。
- 自分の IAM ユーザであればアクセスできます。テンプレートのパラメータで、自分の IAM ユーザー名を入力する仕様にしています。
- 自分の IDE (EC2 インスタンス) であればアクセスできます。code-server の自動インストール時に必要なため。
ALB のヘルスチェックについて
ALB は仕様上、ヘルスチェックに失敗している状態のターゲットグループにトラフィックをルーティングしてくれません。今回の構成では、ヘルスチェックをなるべく成功させるため、nginx が持っている empty_gif という画像ファイルをチェックするようターゲットグループに設定しています。また、ヘルスチェックであればアクセスログを残さないよう設定しています。
ヘルスチェック用パス: /healthcheck.html
その実体は nginx の設定にある empty_gif
AWS CloudFormation テンプレート
AWS CloudFormation テンプレートです。
細かいことはインラインのコメントで補足します。
AWSTemplateFormatVersion: "2010-09-09"
Description: The CloudFormation template that creates an EC2 instance with an ALB target group for code-server.
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
SystemName:
Type: String
Description: The system name. use lower case only. (e.g. example)
Default: example
MaxLength: 10
MinLength: 1
SubName:
Type: String
Description: The system sub name. use lower case only. (e.g. prod or dev)
Default: dev
MaxLength: 10
MinLength: 1
InstanceId:
Type: Number
Description: The individual instance id (number) specified from the administrator. The valid range is 1 to 100.
Default: 1
MinValue: 1
MaxValue: 100
YourIamUserName:
Type: String
Description: Your IAM user name. This is used for the permission of your code-server credential.
Default: youriamusername
MaxLength: 30
MinLength: 1
DomainName:
Type: String
Description: The domain name for the internet-facing URL.
Default: example.com
MaxLength: 40
MinLength: 5
VpcId:
Type: AWS::EC2::VPC::Id
Description: Choose a existing VPC ID you deploy the EC2 instance in.
Resources:
# ------------------------------------------------------------#
# EC2
# ------------------------------------------------------------#
Ec2Instance:
Type: AWS::EC2::Instance
Properties:
IamInstanceProfile: !Ref Ec2InstanceProfile
LaunchTemplate:
LaunchTemplateId:
Fn::ImportValue:
!Sub ec2LtId-code-server-${SystemName}-${SubName}
Version:
Fn::ImportValue:
!Sub ec2LtVersion-code-server-${SystemName}-${SubName}
UserData:
Fn::Base64: !Sub |
#!/bin/bash
# ec2-user を使用するつくりにしています。
export HOME=/home/ec2-user
# userdata スクリプトのログを見やすくするための設定です。
set -euxo pipefail
# Install packages - nginx, git, Node.js だけをインストールする構成にしています。
dnf update -y
dnf install -y nginx git nodejs
# Install code-server
curl -fsSL https://code-server.dev/install.sh | bash
mkdir -p /home/ec2-user/.config/code-server
mkdir -p /home/ec2-user/.local/share/code-server/User
mkdir -p /home/ec2-user/environment
chown -R ec2-user:ec2-user /home/ec2-user/.config /home/ec2-user/.local /home/ec2-user/environment
# Get credential from Secrets Manager
# ここで、AWS Secrets Manager で生成されたパスワードを取得して code-server に設定しています。
PASSWORD=$(aws secretsmanager get-secret-value --secret-id "${SecretCodeServer.Id}" --region "${AWS::Region}" --query SecretString --output text)
# Update code-server config
cat <<EOF > /home/ec2-user/.config/code-server/config.yaml
bind-addr: 127.0.0.1:8008
auth: password
password: ${!PASSWORD}
cert: false
EOF
chown ec2-user:ec2-user /home/ec2-user/.config/code-server/config.yaml
tee home/ec2-user/.local/share/code-server/User/settings.json <<EOF
{
"extensions.autoUpdate": false,
"extensions.autoCheckUpdates": false,
"terminal.integrated.cwd": "/home/ec2-user/environment",
"telemetry.telemetryLevel": "off",
"security.workspace.trust.startupPrompt": "never",
"security.workspace.trust.enabled": false,
"security.workspace.trust.banner": "never",
"security.workspace.trust.emptyWindow": false,
"editor.indentSize": "tabSize",
"editor.tabSize": 2,
"python.testing.pytestEnabled": true,
"auto-run-command.rules": [
{
"command": "workbench.action.terminal.new"
}
]
}
EOF
chown ec2-user:ec2-user /home/ec2-user/.local/share/code-server/User/settings.json
# Service start
systemctl enable --now code-server@ec2-user
# Configure nginx
tee /etc/nginx/nginx.conf <<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '\$remote_addr - \$remote_user [\$time_local] "\$request" '
'\$status \$body_bytes_sent "\$http_referer" '
'"\$http_user_agent" "\$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/conf.d/*.conf;
server {
listen 80;
server_name code-server-${SystemName}-${SubName}.${DomainName};
location = /healthcheck.html {
empty_gif;
access_log off;
break;
}
location /ide${InstanceId}/ {
proxy_pass http://localhost:8008/;
proxy_set_header Host \$host;
proxy_http_version 1.1;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection upgrade;
proxy_set_header Accept-Encoding gzip;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
}
}
EOF
# Service start
systemctl enable --now nginx
Tags:
- Key: Cost
Value: !Sub ${SystemName}-${SubName}
- Key: Name
Value: !Sub code-server-ide${InstanceId}-${SystemName}-${SubName}
DependsOn:
- Ec2InstanceProfile
# ------------------------------------------------------------#
# 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-code-server-ide${InstanceId}-${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: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
- arn:aws:iam::aws:policy/AWSCodeCommitPowerUser
- arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
Policies:
- PolicyName: !Sub CDKAssumeRolePolicy-ide${InstanceId}-${SystemName}-${SubName}
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Resource:
- !Sub arn:${AWS::Partition}:iam::*:role/cdk-*
- PolicyName: !Sub QDeveloperPolicy-ide${InstanceId}-${SystemName}-${SubName}
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "codewhisperer:GenerateRecommendations"
Resource: "*"
Effect: Allow
- PolicyName: !Sub Ec2S3Policy-ide${InstanceId}-${SystemName}-${SubName}
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- "s3:PutObject"
- "s3:ListBucket"
Resource:
- Fn::ImportValue:
!Sub S3BucketLogsArn-code-server-${SystemName}-${SubName}
- Fn::Join:
- ""
- - Fn::ImportValue:
!Sub S3BucketLogsArn-code-server-${SystemName}-${SubName}
- "/*"
Effect: Allow
- PolicyName: !Sub Ec2SecretsPolicy-ide${InstanceId}-${SystemName}-${SubName}
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- secretsmanager:GetResourcePolicy
- secretsmanager:GetSecretValue
- secretsmanager:DescribeSecret
- secretsmanager:ListSecretVersionIds
Resource:
- !GetAtt SecretCodeServer.Id
DependsOn:
- SecretCodeServer
Ec2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Ref Ec2Role
Path: /
Roles:
- !Ref Ec2Role
DependsOn:
- Ec2Role
# ------------------------------------------------------------#
# ALB Target Group / Listener rule
# ------------------------------------------------------------#
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
VpcId: !Ref VpcId
Port: 80
Protocol: HTTP
TargetType: instance
HealthCheckEnabled: true
HealthCheckPath: /healthcheck.html
HealthCheckIntervalSeconds: 60
HealthCheckPort: traffic-port
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 10
HealthyThresholdCount: 5
UnhealthyThresholdCount: 10
Name: !Sub tg-ide${InstanceId}
IpAddressType: ipv4
ProtocolVersion: HTTP1
Targets:
- Id: !GetAtt Ec2Instance.InstanceId
Port: 80
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: 30
- Key: stickiness.enabled
Value: false
- Key: load_balancing.algorithm.type
Value: round_robin
- Key: slow_start.duration_seconds
Value: 0
- Key: stickiness.app_cookie.cookie_name
Value: APPCOOKIE
- Key: stickiness.app_cookie.duration_seconds
Value: 86400
- Key: stickiness.lb_cookie.duration_seconds
Value: 86400
Tags:
- Key: Cost
Value: !Sub ${SystemName}-${SubName}
DependsOn:
- Ec2Instance
ListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn:
Fn::ImportValue:
!Sub AlbListenerArn-${SystemName}-${SubName}
Priority: !Ref InstanceId
Conditions:
- Field: path-pattern
Values:
- !Sub /ide${InstanceId}/*
Actions:
- Type: forward
TargetGroupArn: !Ref TargetGroup
DependsOn:
- TargetGroup
# ------------------------------------------------------------#
# Secrets Manager
# ------------------------------------------------------------#
SecretCodeServer:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub code-server-ide${InstanceId}-${SystemName}-${SubName}
Description: code-server credential
GenerateSecretString:
PasswordLength: 8
ExcludePunctuation: true
IncludeSpace: false
RequireEachIncludedType: true
Tags:
- Key: Cost
Value: !Sub ${SystemName}-${SubName}
SecretCodeServerResourcePolicy:
Type: AWS::SecretsManager::ResourcePolicy
Properties:
BlockPublicPolicy: true
SecretId: !Ref SecretCodeServer
ResourcePolicy:
Version: "2012-10-17"
Statement:
- Effect: Deny
Principal: "*"
Action: secretsmanager:GetSecretValue
Resource: "*"
Condition:
StringNotEquals:
aws:PrincipalArn:
- !Sub "arn:aws:iam::${AWS::AccountId}:user/${YourIamUserName}"
- !GetAtt Ec2Role.Arn
# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
CodeServerUrl:
Value:
Fn::Join:
- ""
- - Fn::ImportValue:
!Sub AlbUrl-${SystemName}-${SubName}
- !Sub /ide${InstanceId}/?folder=/home/ec2-user/environment
テンプレート実行後の作業
テンプレートを実行すると、スタックの出力欄に作成した自分用 IDE の URL が表示されます。それにアクセスすると、以下のようにログイン画面が表示されます。

このパスワードは前述の通り AWS Secrets Manager で生成されているので、お手数ですが AWS マネジメントコンソールで自分のシークレットを確認しに行きます。以下のスクリーンショットのようにシークレットの値を表示して確認します。※モザイクかけています。
自分以外の人が作成したシークレットは権限で見ることができなくなっています。
無事ログインが成功すると、以下のように code-server の UI が表示されます。もちろん git が使用できます。npm などで必要なモジュールをインストールして開発していきます。aws cli もインストール済みなので使用できます。
まとめ
いかがでしたでしょうか。
3つ目の記事は内容が多く説明を細かく書くときりがなくなってしまい、若干省略してしまいました。大変お手数ですが AWS CloudFormation テンプレートを生成 AI に説明させると細かい説明を聞けると思います。すみません。。。
本記事が皆様のお役に立てれば幸いです。





