AWS Cloud9 を新規作成する際の CloudFormation テンプレートを眺めてみた自分メモ

最近、AWS を学んでいるのですが、個人的に最高なのが [JAWS-UG CLI専門支部](https://jawsug-cli.connpass.com/) です。[波田野さん](https://twitter.com/tcsh) の説明聴きながら、Cloud9 コンソール上で AWS CLI 叩くの楽しいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/cb3b28eb-e22d-25b4-314b-49bc2374542f.png)
個人的に [AWS Cloud9](https://aws.amazon.com/jp/cloud9/) に興味を持ちましたので、最初のセットアップの手順を自分なりに追ってみました。これはその自分メモです。

# 今回の対象

[JAWS-UG CLI専門支部](https://jawsug-cli.connpass.com/) イベントのどれか、例えば [VPCの回](https://jawsug-cli.connpass.com/event/175444/) の「AWS CLIハンズオンの環境構築手順」欄で紹介されている以下の手順資料が元ネタです。

* [AWS CLI利用環境(Cloud9)の構築・利用・破棄 (簡易版) — AWS CLI利用環境(Cloud9)の構築・利用・破棄(簡易版)](http://prototype-handson-cli.s3-website-ap-northeast-1.amazonaws.com/handson_light_web-aws_prepare/handson_light_web-aws_prepare-cloud9/index.html)

この手順自体はすごく判り易いので、このなかでメインの部分である以下のページを主に調べます。

[1.1. Cloud9環境の構築 (handson-cloud9-env)](http://prototype-handson-cli.s3-website-ap-northeast-1.amazonaws.com/handson_light_web-aws_prepare/handson_light_web-aws_prepare-cloud9/cloud9_web-environment-create-case-handson-cloud9-env.html)

この手順で何がどう準備されているのか、それを理解するための自分メモです。

# AWS Cloud9 環境の構築

手順のなかで Cloud9 を作成する際、入力した値は以下の3つだけでした。他はデフォルト値のまま。

```
Name: handson-cloud9-env
Network (VPC): (Cloud9用に作成したVPCを選択する)
Subnet: (Cloud9用に作成したVPCのサブネットを選択する)
```

こんな簡単にあの便利な環境が作成され、EC2 への SSH 接続や AWS CLI のログインまで自動実行されるなんて、素晴らしいです。内部ではどんな作業が実施されているのでしょうか？

# CloudFormation を使っていた

AWS マネージメントコンソールを眺めていたところ、CloudFormation に見慣れないスタックが作成されているのを見つけました。名前からして、これ Cloud9 環境のセットアップ用のスタックではないでしょうか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/e96b3690-9579-e789-1311-c8ce15cef034.png)

さっそくテンプレートを眺めてみます。JSON 形式だと読みにくいので、「デザイナーで表示」して YAML 形式に変換したものが以下です。

```yaml
Resources:
  Instance:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: ami-0884ffec9680b66a1
      InstanceType: t2.micro
      UserData: >-
        XXXX 内容省略 XXXX
      Tags:
        - Key: Name
          Value: aws-cloud9-handson-cloud9-env-84xxxxd2
      NetworkInterfaces:
        - AssociatePublicIpAddress: true
          DeviceIndex: 0
          SubnetId: subnet-0fxxxx5d
          GroupSet:
            - !Ref InstanceSecurityGroup
  InstanceSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: >-
        Security group for AWS Cloud9 environment
        aws-cloud9-handson-cloud9-env-84xxxxd2
      VpcId: vpc-03xxxx14
      SecurityGroupIngress:
        - FromPort: 22
          ToPort: 22
          IpProtocol: tcp
          CidrIp: 18.179.48.128/27
        - FromPort: 22
          ToPort: 22
          IpProtocol: tcp
          CidrIp: 18.179.48.96/27
      Tags:
        - Key: Name
          Value: aws-cloud9-handson-cloud9-env-84xxxxd2
```

EC2 インスタンスとセキュリティグループを作成しているのがわかります。EC2 で使用している AMI の ami-0884ffec9680b66a1 は、「Cloud9Default-2020-06-02T07-52」というAMI名のようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/6c2fadd5-49e3-a6c8-4c86-2add0e290072.png)

セキュリティグループで追加している2つの IP Address が謎です。EC2 の Public IP とも違うようだし…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/c88de1ac-81d3-5f42-74e0-a6e3e4739662.png)
ググってみたら [AWS IP Address Ranges](https://anto.online/tools/aws-ip-ranges/) というサイトがあって、どうやら東京リージョンの Cloud9 用に決まっているアドレスみたいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/a11dc48d-26fb-12d3-29a9-99b79a58ec1b.png)
関係ないですが、コンソール上で aws コマンドを使ったら、何故かエラーになったので aws config コマンドでリージョンを ap-northeast-1 を設定しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/00633d94-f37a-e348-f374-701a14b1abc9.png)

# ユーザーデータの bash スクリプト

EC2 インスタンスを作成するうえで大事なのは AMI イメージの選択、そしてそれをカスタマイズするためのユーザーデータですよね。上記の CloudFormation テンプレートでは UserData 欄が [Base64](https://ja.wikipedia.org/wiki/Base64) エンコードされていて読めなかったので、デコードして内容を確認してみます。

まず最初のほうはこんな感じ。

```bash
#!/bin/bash

UNIX_USER="ec2-user"
UNIX_USER_HOME="/home/ec2-user"
ENVIRONMENT_PATH="/home/ec2-user/environment"
UNIX_GROUP=$(id -g -n "$UNIX_USER")

# add SSH key
install -g "$UNIX_GROUP" -o "$UNIX_USER" -m 755 -d "$UNIX_USER_HOME"/.ssh
cat <<'EOF' >> "$UNIX_USER_HOME"/.ssh/authorized_keys
# Important
# ---------
# The following public key is required by Cloud9 IDE
# Removing this key will make this EC2 instance inaccessible by the IDE
#

cert-authority ssh-rsa AAXXXX 内容省略 XXXXbw== 84xxxxd2@cloud9.amazon.com

#
# Add any additional keys below this line
#
EOF
```

最初は基本的な環境変数の設定をして、それから SSH 用のパブリックキーを格納したファイルを追加していますね。作成されたファイルをコンソール上で確認しておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/40e94dba-dcff-3201-2db3-4129e7d9caa8.png)
その先は特に変わったところは無いようにおもわれます。起動関係のコマンドは sudo パスワード不要にしているあたりがポイントですかね。

```bash
# allow automatic shutdown
echo "$UNIX_USER    ALL=(ALL) NOPASSWD: /sbin/poweroff, /sbin/reboot, /sbin/shutdown" >> /etc/sudoers

ln -s /opt/c9 "$UNIX_USER_HOME"/.c9
chown -R "$UNIX_USER":"$UNIX_GROUP" "$UNIX_USER_HOME"/.c9 /opt/c9
install -g "$UNIX_GROUP" -o "$UNIX_USER" -m 755 -d "$ENVIRONMENT_PATH"
```

そして ~/.bashrc の定義部分は長いですねー。ただヒアドキュメント形式なので、そのまま内容を読めるのが救いではあります。

```bash
if [ "$ENVIRONMENT_PATH" == "/home/ec2-user/environment" ] && grep "alias python=python27" "$UNIX_USER_HOME"/.bashrc; then

    cat <<'EOF' > "$UNIX_USER_HOME"/.bashrc
# .bashrc

export PATH=$PATH:$HOME/.local/bin:$HOME/bin

# load nvm
export NVM_DIR="$HOME/.nvm"
[ "$BASH_VERSION" ] && npm() {
    # hack: avoid slow npm sanity check in nvm
    if [ "$*" == "config get prefix" ]; then which node | sed "s/bin\/node//";
    else $(which npm) "$@"; fi
}
# [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"  # This loads nvm
rvm_silence_path_mismatch_check_flag=1 # prevent rvm complaints that nvm is first in PATH
unset npm # end hack

# User specific aliases and functions
alias python=python27

# modifications needed only in interactive mode
if [ "$PS1" != "" ]; then
    # Set default editor for git
    git config --global core.editor /usr/bin/nano

    # Turn on checkwinsize
    shopt -s checkwinsize

    # keep more history
    shopt -s histappend
    export HISTSIZE=100000
    export HISTFILESIZE=100000
    export PROMPT_COMMAND="history -a;"

    # Source for Git PS1 function
    if ! type -t __git_ps1 && [ -e "/usr/share/git-core/contrib/completion/git-prompt.sh" ]; then
        . /usr/share/git-core/contrib/completion/git-prompt.sh
    fi

    # Cloud9 default prompt
    _cloud9_prompt_user() {
        if [ "$C9_USER" = root ]; then
            echo "$USER"
        else
            echo "$C9_USER"
        fi
    }

    PS1='\[\033[01;32m\]$(_cloud9_prompt_user)\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$(__git_ps1 " (%s)" 2>/dev/null) $ '
fi

EOF

    chown "$UNIX_USER":"$UNIX_GROUP" "$UNIX_USER_HOME"/.bashrc
fi
```
~/.local/bin と ~/bin を PATH に含めているのがポイントでしょうか。

その後の load nvm ハックのあたりは [NVM getting very slow on startup in Bash](https://github.com/nvm-sh/nvm/issues/1277) あたりとか、ネットにいろいろ情報があるので、有名な問題っぽいですが、今回はスルーで。

その後はプロンプト設定してくれたり、環境整えてくれているようですね。Python がまだ ver2 なんだー、とか思ってみたり。

続いて、同じくヒアドキュメントによる ~/environment/README.md ファイルの作成ですね。シェル起動時に表示されるアレ？ですかね。

```bash
if [ "$ENVIRONMENT_PATH" == "/home/ec2-user/environment" ] && [ ! -f "$ENVIRONMENT_PATH"/README.md ]; then
    cat <<'EOF' >> "$ENVIRONMENT_PATH"/README.md
         ___        ______     ____ _                 _  ___
        / \ \      / / ___|   / ___| | ___  _   _  __| |/ _ \
       / _ \ \ /\ / /\___ \  | |   | |/ _ \| | | |/ _` | (_) |
      / ___ \ V  V /  ___) | | |___| | (_) | |_| | (_| |\__, |
     /_/   \_\_/\_/  |____/   \____|_|\___/ \__,_|\__,_|  /_/
 -----------------------------------------------------------------


Hi there! Welcome to AWS Cloud9!

To get started, create some files, play with the terminal,
or visit https://docs.aws.amazon.com/console/cloud9/ for our documentation.

Happy coding!

EOF

    chown "$UNIX_USER":"$UNIX_GROUP" "$UNIX_USER_HOME"/environment/README.md
fi
```

そして最後は、Cloud9 が自動終了される際に実行されるスクリプトを作成して登録しているようです。

```bash
# Fix for permission error when trying to call `gem install`
chown "$UNIX_USER" -R /usr/local/rvm/gems

#This script is appended to another bash script, so it does not need a bash script file header.

UNIX_USER_HOME="/home/ec2-user"

C9_DIR=$UNIX_USER_HOME/.c9
CONFIG_FILE_PATH="$C9_DIR"/autoshutdown-configuration
VFS_CHECK_FILE_PATH="$C9_DIR"/stop-if-inactive.sh

echo "SHUTDOWN_TIMEOUT=30" > "$CONFIG_FILE_PATH"
chmod a+w "$CONFIG_FILE_PATH"

echo -e '#!/bin/bash
set -euo pipefail
CONFIG=$(cat '$CONFIG_FILE_PATH')
SHUTDOWN_TIMEOUT=${CONFIG#*=}
if ! [[ $SHUTDOWN_TIMEOUT =~ ^[0-9]*$ ]]; then
    echo "shutdown timeout is invalid"
    exit 1
fi
is_shutting_down() {
    is_shutting_down_system_d &> /dev/null || is_shutting_down_init_d &> /dev/null
}
is_shutting_down_system_d() {
    local TIMEOUT
    TIMEOUT=$(busctl get-property org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager ScheduledShutdown)
    if [ "$?" -ne "0" ]; then
        return 1
    fi
    if [ "$(echo $TIMEOUT | awk "{print \$3}")" == "0" ]; then
        return 1
    else
        return 0
    fi
}
is_shutting_down_init_d() {
    pgrep shutdown
}
is_vfs_connected() {
    pgrep vfs-worker >/dev/null
}

if is_shutting_down; then
    if [[ ! $SHUTDOWN_TIMEOUT =~ ^[0-9]+$ ]] || is_vfs_connected; then
        sudo shutdown -c
    fi
else
    if [[ $SHUTDOWN_TIMEOUT =~ ^[0-9]+$ ]] && ! is_vfs_connected; then
        sudo shutdown -h $SHUTDOWN_TIMEOUT
    fi
fi' > "$VFS_CHECK_FILE_PATH"

chmod +x "$VFS_CHECK_FILE_PATH"

echo "* * * * * root $VFS_CHECK_FILE_PATH" > /etc/cron.d/c9-automatic-shutdown
```

いやぁ、思ったより長かったですね。

README.md は更新が多そうだからユーザーデータで生成・更新するのはわかるのですが、最後のスクリプトはどうしてここで作成しているのか謎ですね… いろいろ改善中なのですかねー。

# というわけで

今回は AWS Cloud9 サービス作成時の CloudFormation テンプレートと、その中に設定されていたユーザーデータ、bash スクリプトを眺めてみました。

本当は AMI イメージのほうも眺めて、インストール済みのサービスや起動設定など眺めたいところですが。それはまた時間のあるときに。

というわけで、本当に自分しか見ないメモになった気もしますが… 一応公開しておきますね。いつか誰かのお役に立てば幸いです。

それではまた！