こんにちは、広野です。
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 に説明させると細かい説明を聞けると思います。すみません。。。
本記事が皆様のお役に立てれば幸いです。