AWS 上で稼働する VSCode Server に自動停止を導入してみた

## はじめに

2024/07/25 の AWS 社の発表により、AWS 上で Cloud9 の新規利用が不可能になりました。ハンズオンやちょっと試す際にも最適な環境であったため、非常に残念です。  
その後、代替案として SageMaker の CodeEditor が話題になりましたが、AWS の workshop studio を見ると、[VSCode Server の利用が手順化されていました。](<https://catalog.workshops.aws/typescript-and-cdk-for-beginner/ja-JP/10-getting-started/20-self-hosted/10-vscode-server>)
軽く使ってみたところほぼ VSCode のため非常に使いやすく、テンプレートから簡単に導入ができるので導入も簡単でした。

ただ、テンプレートのままでは自動停止には対応せず、Cloud9 のように自動停止に頼った使い方をすると消し忘れが多発し、コスト増に繋がる可能性があります。  
そこで今回、テンプレートに変更を加え、自動停止に対応させたので方法をご紹介致します。

## CloudFormation テンプレート

workshop studioの[Visual Studio Code Server のセットアップ](https://catalog.workshops.aws/typescript-and-cdk-for-beginner/ja-JP/10-getting-started/20-self-hosted/10-vscode-server)において、東京リージョンの Launch ボタンの情報を見ると、下記のテンプレートが使われていることが確認できます。  

<https://ws-assets-prod-iad-r-nrt-2cb4b4649d0e0f94.s3.ap-northeast-1.amazonaws.com/4704be05-fbe3-49cc-94d4-c9e3c6e17881/vscode-server.yaml>

Infrastructure Composer (最近Application Composerから改名) に投入してみましょう。  

![infrastructure_composer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989049/a9f89830-ee0c-e29f-39cc-37ad37f89f29.png)


- 下部のほうで VPC 関連リソースが定義されています。
- VSCode Server は中央の EC2 インスタンスで稼働します。
- アクセスには CloudFront が用いられ、ログイン時に入力するパスワードは Secrets Manager で管理されます。
- SSM にて EC2 の初期セットアップのコマンドが実行されます。

## 自動停止の実現方法

EC2 のアイドル時の停止ということで、当初は CPU 使用率等を考えましたが、これだとインスタンスタイプに依存するし、アイドル時の CPU 使用率のしきい値を一定の値に決めるのは困難でした。  
VSCode Server にブラウザ上で繋いでいるときは使用中、ブラウザが閉じられたらアイドル状態という形にしたいと考えました。  
そこで、本構成で利用されているWeb サーバー (nginx) のログを調べたところ、**Active connections** という値が取得できることが分かり、これを利用することにしました。

### テンプレートの修正 1：Active connections の取得

**VSCodeInstanceSSMDoc** リソースの mainSteps に  
下記の **UpdateNginxConf**、**ReloadNginx** および、**ConfigureMetrics** の 3 ステップを追加します。

```yaml
          - action: aws:runShellScript
            name: UpdateNginxConf
            inputs:
              runCommand:
                - |
                  cat << 'EOF' > /etc/nginx/nginx.conf
                  user www-data;
                  worker_processes auto;
                  pid /run/nginx.pid;
                  include /etc/nginx/modules-enabled/*.conf;

                  events {
                          worker_connections 768;
                          # multi_accept on;
                  }

                  http {

                          ##
                          # Basic Settings
                          ##

                          sendfile on;
                          tcp_nopush on;
                          types_hash_max_size 2048;
                          # server_tokens off;

                          # server_names_hash_bucket_size 64;
                          # server_name_in_redirect off;

                          include /etc/nginx/mime.types;
                          default_type application/octet-stream;

                          ##
                          # SSL Settings
                          ##

                          ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
                          ssl_prefer_server_ciphers on;

                          ##
                          # Logging Settings
                          ##

                          access_log /var/log/nginx/access.log;
                          error_log /var/log/nginx/error.log;

                          ##
                          # Gzip Settings
                          ##

                          gzip on;

                          # gzip_vary on;
                          # gzip_proxied any;
                          # gzip_comp_level 6;
                          # gzip_buffers 16 8k;
                          # gzip_http_version 1.1;
                          # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

                          ##
                          # Virtual Host Configs
                          ##

                          include /etc/nginx/conf.d/*.conf;
                          include /etc/nginx/sites-enabled/*;

                          server {
                                  listen 80;
                                  server_name localhost;
                                  location /nginx_status {
                                          stub_status on;
                                          allow 127.0.0.1;  # ローカルホストからのみアクセスを許可
                                          deny all;         # 他のすべてのアクセスを拒否
                                  }
                          }
                  }

                  #mail {
                  #       # See sample authentication script at:
                  #       # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
                  #
                  #       # auth_http localhost/auth.php;
                  #       # pop3_capabilities "TOP" "USER";
                  #       # imap_capabilities "IMAP4rev1" "UIDPLUS";
                  #
                  #       server {
                  #               listen     localhost:110;
                  #               protocol   pop3;
                  #               proxy      on;
                  #       }
                  #
                  #       server {
                  #               listen     localhost:143;
                  #               protocol   imap;
                  #               proxy      on;
                  #       }
                  #}
          - action: aws:runShellScript
            name: ReloadNginx
            inputs:
              runCommand:
                - sudo nginx -s reload
          - action: aws:runShellScript
            name: ConfigureMetrics
            inputs:
              runCommand:
                - mkdir /home/codeserver
                - | # スクリプトの作成
                  echo '#!/bin/bash
                  instance_id=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
                  active_connections=$(curl -s http://localhost/nginx_status | grep "Active connections" | awk '\''{print $3}'\'')
                  aws cloudwatch put-metric-data \
                      --metric-name ActiveSession \
                      --namespace "AWS/EC2" \
                      --value $active_connections \
                      --dimensions InstanceId="$instance_id" \
                      --region "ap-northeast-1"' > /home/codeserver/monitor.sh
                - chmod +x /home/codeserver/monitor.sh

                - | # cronジョブを設定
                  (crontab -l 2>/dev/null; echo "* * * * * /home/codeserver/monitor.sh") | crontab -

```

#### UpdateNginxConf

中身の大半は元々配置されている/etc/nginx/nginx.conf のままです。  
終盤の数行、下記の内容を足したいために追加しています。  
こちらの設定により、localhost/nginx_status にて nginx のステータス情報を取得できるようになります。  

```yaml
                          server {
                                  listen 80;
                                  server_name localhost;
                                  location /nginx_status {
                                          stub_status on;
                                          allow 127.0.0.1;  # ローカルホストからのみアクセスを許可
                                          deny all;         # 他のすべてのアクセスを拒否
                                  }
                          }
```

#### ReloadNginx

UpdateNginxConf の編集結果を反映させるために、nginx をリロードします。  

```shell
sudo nginx -s reload
```

#### ConfigureMetrics

こちらでは CloudWatch のメトリクスを登録する処理を実施しています。  
以下の作業を cron で 1 分ごとに実施します。  

- インスタンス ID をインスタンスメタデータから取得
- nginx_status より、Active connections の値を取得（grep等で加工）
- put-metric-data により CloudWatch に登録（EC2 のインスタンス別メトリクスに、ActiveSession というカスタムメトリクスを登録）

```yaml
                - mkdir /home/codeserver
                - | # スクリプトの作成
                  echo '#!/bin/bash
                  instance_id=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
                  active_connections=$(curl -s http://localhost/nginx_status | grep "Active connections" | awk '\''{print $3}'\'')
                  aws cloudwatch put-metric-data \
                      --metric-name ActiveSession \
                      --namespace "AWS/EC2" \
                      --value $active_connections \
                      --dimensions InstanceId="$instance_id" \
                      --region "ap-northeast-1"' > /home/codeserver/monitor.sh
                - chmod +x /home/codeserver/monitor.sh

                - | # cronジョブを設定
                  (crontab -l 2>/dev/null; echo "* * * * * /home/codeserver/monitor.sh") | crontab -
```

## Active connections の仕様

しきい値を決めるために Active connections の値を検証してみました。  

- EC2 を起動しただけの状態 →1
- ブラウザで VSCode Server に接続 →3 以上

となることがわかったので、一定時間 1 だったら止めるという方針にしました。  
3より大きくなる条件はよくわからないのですが、VSCode内で行っている処理によるのかもしれません。  
アプリケーションやウェブサイトのプレビュー機能を使うと増えるのかも。  

![ActiveConnection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989049/9ad1ca57-753a-e8e5-7aee-659a317dbf15.png)


## テンプレートの修正 2：CloudWatch Alarm の追加

パラメータの AutoStopIdleTimeInMinutes に記載の分数、アイドル状態だった場合に停止するアラームを追加します。  
30 分間、データポイントが ActiveSession <= 1 の状態が続くと発動します。  
AlarmActions には、arn:aws:automate:${AWS::Region}:ec2:stop を活用し、Lambda 等は不要で EC2 を止めることができます。  

```yaml
Parameters:

・・・中略・・・

  EC2Idenifier:
    Type: String
    Description: The Idenifier for EC2
  AutoStopIdleTimeInMinutes:
    Type: Number
    Description: Idle time in minutes before auto-stopping the instance.
    Default: 30


・・・中略・・・

  IdleEC2Alarm:
    Type: 'AWS::CloudWatch::Alarm'
    Properties:
      AlarmName: !Join ["", [!Ref EC2Idenifier ,"-EC2IdleAlarm" ]]
      AlarmDescription: 'Monitor EC2 instance idle time'
      MetricName: 'ActiveSession'
      Namespace: 'AWS/EC2'
      Statistic: 'Maximum'
      Period: 60
      EvaluationPeriods: !Ref AutoStopIdleTimeInMinutes
      Threshold: 1
      ComparisonOperator: 'LessThanOrEqualToThreshold'
      Dimensions:
        - Name: 'InstanceId'
          Value: !Ref VSCodeInstanceEC2Instance
      AlarmActions:
        - !Sub arn:aws:automate:${AWS::Region}:ec2:stop
```

※EC2Idenifier は利用者区別のためにパラメータを入れています。個人名などが入る想定。

## アラーム挙動確認

17:43 頃に VSCode Server のタブを閉じて接続を切った後の挙動です。  
約 30 分後の 18:13 にアラーム状態となり、ec2:stop のアクションが動いたことを確認できました。  

![alarm1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989049/7ded4b95-67d7-aa80-0848-d7c2d6ab5405.png)
![alarm2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989049/4f07ce1f-db39-5266-65a7-df22e0b7b7e7.png)


## おわりに

VSCode Server の起動しっぱなしによるコスト上昇を抑えるための自動停止手段をご紹介いたしました。  
EC2 のアイドル状態の定義が難しかったので、最初は実現が難しいと思っていましたが、nginx のステータスからブラウザ接続状態を確認できる確実な値を取得できました。  
さらに、cron からの CloudWatch メトリクスを利用して、アラームを起動する方法も活用できたため、複数サービスを組み合わせた非常に有意義な取り組みになったと感じています。  
VSCode Server の利用時にぜひ活用してみてください。