AWS Cloud9 の代替として CFn で code-server をセットアップする

記事タイトルとURLをコピーする

こんにちは、近藤(りょう)です!

以前、プライベートサブネットに配置した AWS Cloud9 をセットアップする 記事を書きましたが、Cloud9 の新規利用が制限されたことで、今後 Cloud9 を利用する選択肢を見直す必要があるかもしれません。 blog.serverworks.co.jp

AWS Cloud9 を利用していたハンズオン手順や、日常的に使用していた方々も、代替手法を検討されているのではないでしょうか。例えば、以下のリンクでは AWS IDE Toolkits や AWS CloudShell への移行方法が紹介されています。 aws.amazon.com

AWS IDE Toolkits や AWS CloudShell を利用する方法もありますが、ローカル環境に依存せず、各チームが共通の 統合開発環境 (IDE) を簡単に初期セットアップ&カスタマイズできる仕組みを導入したい場合もあるのではないでしょうか。

そこで代替案の一つとして、今回は code-serverをDocker上にセットアップし、リモート統合開発環境 (IDE)として利用してみることを試してみます。

code-server について

Coder社が提供するOSSのリモート統合開発環境 (IDE) です。ブラウザ上でコードの記述、実行、デバッグが可能です。
VSCodeをベースにしたWebベースのIDEであるため、見た目や操作感はVisual Studio Code (VS Code) とほぼ同じです。そのため、普段からVS Codeを利用している方は、違和感なく使用できます。

code-server のドキュメント

code-serverの詳細はドキュメントを参照ください。 coder.com

Docker用のイメージを利用する場合の制約

Our official image supports amd64 and arm64. For arm32 support, you can use a community-maintained code-server alternative.

code-serverの公式イメージは amd64(一般的な64ビットIntel/AMDアーキテクチャ)およびarm64(64ビットARMアーキテクチャ)に対応しています。 coder.com

今回の構成

Public Subnet 上に EC2(Amazon Linux 2023)をデプロイし、Docker を使用して code-server をインストールします。この構成では、セキュリティグループで利用ユーザーの IP アドレスのみを許可します。また、code-server への接続は、SSL 通信を使用して 443 番ポート経由で行います。

code-serverの構成-1

環境構築してみる

東京リージョンに code-server を構築してみます。

CFn 実行

東京リージョンでCFnをぽちっとな。

本code-serverは systemdによる自動起動制御の設定をしているため、サーバーを停止してから再起動しても正常に動作します。

パラメータ は必要に応じて適宜修正してください。
(もしEC2からAWSリソースに対して何か必要な権限が必要な場合はロールの修正をお願いします。)

AWSTemplateFormatVersion: "2010-09-09"
Description: "CloudFormation template to create a code-server with specified requirements."

Parameters:
  AllowedIP:
    Type: String
    Description: "The IP address allowed to access the instance on port 443 (e.g., 203.0.113.0/32)."
    ConstraintDescription: "Must be a valid CIDR notation (e.g., 203.0.113.0/32)."
    AllowedPattern: "^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$"

  ImageId:
    Type: String
    Description: "The AMI ID for the EC2 instance (e.g., ami-12345678)."
    Default: "ami-023ff3d4ab11b2525"
    ConstraintDescription: "Must be a valid AMI ID."

Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: "10.0.0.0/16"
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: "CfnVPC"

  # Internet Gateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: "CfnInternetGateway"

  # Attach Internet Gateway to VPC
  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # Public Subnet
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: "10.0.0.0/24"
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select [ 0, !GetAZs "" ]
      Tags:
        - Key: Name
          Value: "CfnPublicSubnet"

  # Route Table
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: "CfnRouteTable"

  # Route for Internet Access
  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway

  # Associate Route Table with Subnet
  RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref RouteTable

  # Security Group
  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "Allow HTTPS access from specified IP"
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !Ref AllowedIP
      Tags:
        - Key: Name
          Value: "CfnSecurityGroup"

  # IAM Role for SSM Access
  IAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
      Path: "/"
      Tags:
        - Key: Name
          Value: "CfnSSMRole"
      Policies:
        - PolicyName: "SSMPutParameterPolicy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ssm:PutParameter
                Resource: 
                  Fn::Sub: arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/code-server/login-password

  # Instance Profile
  IAMInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref IAMRole
      InstanceProfileName: "CfnSSMInstanceProfile"

  # EC2 Instance
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t2.micro
      SubnetId: !Ref PublicSubnet
      SecurityGroupIds:
        - !Ref SecurityGroup
      IamInstanceProfile: !Ref IAMInstanceProfile
      ImageId: !Ref ImageId
      Tags:
        - Key: Name
          Value: "code-server-docker"
      UserData:
        Fn::Base64: |
          #!/bin/bash
          # ------------------------------------------------
          # 変数セット
          # ------------------------------------------------
          USER_NAME="ec2-user"
          USER_HOME="/home/ec2-user"
          CODE_SERVER_PASSWORD=$(head /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 25)
          USER_ID=1000
          GROUP_ID=1000
          DOCKER_USER=${USER_NAME}
          TEMP_DIR="/tmp"

          # ------------------------------------------------
          # インストール
          # ------------------------------------------------
          sudo dnf install -y git docker
          sudo usermod -a -G docker ${USER_NAME}
          DOCKER_COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | jq -r .tag_name)
          sudo curl -L "https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
          sudo chmod +x /usr/local/bin/docker-compose
          sudo systemctl restart docker.service

          # ------------------------------------------------
          # Docker定義
          # ------------------------------------------------
          # Dockerfileの作成
          sudo cat <<'EOF' > ${TEMP_DIR}/Dockerfile
          # ベースとなるイメージを指定(codercom/code-serverの最新バージョン)
          FROM codercom/code-server:latest

          # コンパイル時に使用する変数を定義
          # USER_ID: ユーザーのID(uid)
          # GROUP_ID: ユーザーが所属するグループのID(gid)
          # DOCKER_USER: 使用するユーザー名
          ARG USER_ID
          ARG GROUP_ID
          ARG DOCKER_USER

          # rootユーザーで実行(初期セットアップ時に必要なパーミッションを取得するため)
          USER root

          # 必要なパッケージ(libcap2-bin)をインストール
          # setcapを使用するために必要
          RUN apt-get update && apt-get install -y libcap2-bin

          # 'coder'ユーザーのIDとグループIDをARGで渡された値に変更
          # これにより、ホストとコンテナ内で一致するUID/GIDを使用
          RUN usermod -u $USER_ID coder && \
              groupmod -g $GROUP_ID coder

          # ノードのサービスバインディングに必要な権限(cap_net_bind_service)を付与
          # これにより、ポート番号が1024未満のポートにサービスをバインドする権限を付与
          RUN setcap 'cap_net_bind_service=+ep' /usr/lib/code-server/lib/node

          # 作業ディレクトリを設定
          WORKDIR /home/coder/project

          # coderユーザーに切り替え
          USER coder

          # コンテナ起動時にdumb-initとcode-serverをエントリーポイントとして設定
          ENTRYPOINT ["dumb-init", "code-server"]
          EOF

          sudo chown ${USER_ID}:${GROUP_ID} ${TEMP_DIR}/Dockerfile
          sudo mv ${TEMP_DIR}/Dockerfile ${USER_HOME}/Dockerfile

          # docker-compose.ymlの作成
          cat <<'EOF' > ${TEMP_DIR}/docker-compose.yml
          services:
            code-server:
              # code-serverサービスの設定
              build:
                # Dockerfileのビルド設定
                context: .
                args:
                  # Dockerfileに渡す引数(ユーザーID、グループID、ユーザー名)
                  USER_ID: ${USER_ID}
                  GROUP_ID: ${GROUP_ID}
                  DOCKER_USER: ${DOCKER_USER}

              # ポートの公開設定
              ports:
                - "443:443"  # ホストの443ポートをコンテナの443ポートにマッピング(HTTPSポート)

              # ボリュームのマウント設定
              volumes:
                - "$HOME/.config:/home/coder/.config"
                - "$PWD:/home/coder/project"

              # 環境変数の設定
              environment:
                - DOCKER_USER=${DOCKER_USER}

              # ユーザー設定
              user: "${USER_ID}:${GROUP_ID}"

              # コンテナの再起動設定
              restart: always
          EOF

          sudo chown ${USER_ID}:${GROUP_ID} ${TEMP_DIR}/docker-compose.yml
          sudo mv ${TEMP_DIR}/docker-compose.yml ${USER_HOME}/docker-compose.yml

          # ------------------------------------------------
          # code-server定義
          # ------------------------------------------------
          # code-serverのディレクトリ作成
          sudo mkdir -p ${USER_HOME}/.config/code-server
          sudo chown ${USER_ID}:${GROUP_ID} ${USER_HOME}/.config

          # config.yamlの作成
          cat <<EOF > ${TEMP_DIR}/config.yaml
          bind-addr: 0.0.0.0:443
          auth: password
          password: ${CODE_SERVER_PASSWORD}
          cert: true
          EOF

          sudo chown ${USER_ID}:${GROUP_ID} ${TEMP_DIR}/config.yaml
          sudo mv ${TEMP_DIR}/config.yaml ${USER_HOME}/.config/code-server/config.yaml

          # ------------------------------------------------
          # systemd定義
          # ------------------------------------------------
          # /etc/systemd/system/docker-compose-app.serviceの作成
          cat <<EOF > ${TEMP_DIR}/docker-compose-app.service
          [Unit]
          Description=Docker Compose Application
          Requires=docker.service
          After=docker.service

          [Service]
          Type=oneshot
          RemainAfterExit=yes
          WorkingDirectory=/home/ec2-user
          Environment=USER_ID=${USER_ID}
          Environment=GROUP_ID=${GROUP_ID}
          Environment=DOCKER_USER=${DOCKER_USER}
          Environment=PWD=/home/ec2-user
          Environment=HOME=${USER_HOME}
          ExecStart=/usr/local/bin/docker-compose up -d
          ExecStop=/usr/local/bin/docker-compose down

          [Install]
          WantedBy=multi-user.target
          EOF

          sudo chown ${USER_ID}:${GROUP_ID} ${TEMP_DIR}/docker-compose-app.service
          sudo mv ${TEMP_DIR}/docker-compose-app.service /etc/systemd/system/docker-compose-app.service

          sudo systemctl daemon-reload

          # ------------------------------------------------
          # code-server起動
          # ------------------------------------------------
          sudo chown -R ${USER_ID}:${GROUP_ID} ${USER_HOME}/.docker

          sudo systemctl enable docker-compose-app.service
          sudo systemctl start docker-compose-app.service ; sudo systemctl status docker-compose-app.service

          aws ssm put-parameter --name "/code-server/login-password" --value ${CODE_SERVER_PASSWORD} --type SecureString --key-id "alias/aws/ssm" --overwrite


Outputs:
  PublicIP:
    Description: "code-server URL"
    Value: !Join
      - ""
      - 
        - "https://"
        - !GetAtt EC2Instance.PublicIp 
        - "/"

  UserDataSSMParameter:
    Description: "SSM Parameter code-server login password output"
    Value: "/code-server/login-password"

CFn で指定する Parameters について

先ほどのCFnを実行する時に Parameters の設定が必要になります。

Parametersの設定

  • AllowIP
    • 接続を許可するIP アドレスを /32 指定で入力してください。
      確認くん 等を利用し、ご自身のグローバルIPの確認してください。
  • ImageId
    • 任意の Amazon Linux 2023 の AMI ID を指定してください。(本記事では ami-023ff3d4ab11b2525 を使用して検証しています。)

CFn 状態確認

作成したスタックが完了していれば、code-serverのデプロイは完了です。

CFn 実行が完了すると、スタックの「出力」セクションに以下の内容が確認できます。

Outputsの結果

  • PublicIP
    • code-serverの接続URL
  • UserDataSSMParameter
    • code-serverのログインパスワードを格納している AWS Systems Manager (SSM) パラメータストアの情報
      "/code-server/login-password" に保存されます。


AWS Systems Manager (SSM) パラメータストアの "/code-server/login-password" に code-server のログインパスワード情報が保存されています。
(復号化された値を表示にチェックを入れると文字列が参照できます。)

code-serverのパスワード情報

code-serverへの接続

code-server の接続 URL にアクセスします。(スタックの「出力」セクションのPublicIPがURLとなります。)

証明書を設定していないため、「この接続ではプライバシーが保護されません」と表示されますがそのまま接続を進めてください。
(「詳細設定」-「xxxxxにアクセスする(安全ではありません)」を押下する。)

code-serverのログイン-プライバシー保護画面

SSM パラメータストアに保存されたパスワードを入力し、「SUBMIT」を押下します。

code-serverのログイン-1

code-server にログインできたら、必要な設定を行い、利用を開始してください。

code-serverのログイン-2

エラーメッセージについて

An SSL certificate error occurred when fetching the script.

code-serverのログイン-エラーメッセージ-1

サーバー証明書を設定していないため出力されていますので現在の設定の場合は問題ありません。 以下、参考となりますが適切なサーバー証明書を導入することで回避できます。

coder.com

code-serverの削除

作成したCFnのスタックを削除してください。
パラメータストアの情報は自動で削除されないため、"/code-server/login-password" の削除もお願いします。

まとめ

個人的ですがCoder社はRe: invent 2024で「The enterprise development environment (sponsored by Coder)」(訳:エンタープライズ開発環境 (Coder 提供))でセッションをしているようなので気になっています。
参考:Join Coder at AWS re:Invent 2024

私は VS Code を利用しているため、違和感なく使用できております。 ハンズオン等のAWS Cloud9 の代替として code-server を試すのはいかがでしょうか。

近藤 諒都

(記事一覧)

カスタマーサクセス部CS5課

夜行性ではありません。朝活派です。

趣味:お酒、旅行、バスケ、掃除、家庭用パン作り(ピザも)など