AWS CloudFormationで学生の演習用サーバー80台を立てる


# 要件

* 学生がSSHログインできてroot権限を渡せるサーバーを人数分作りたい。
* Ubuntu で http サーバーを立ててページを公開できるようにしたい。
  * sudo 権限を渡す
* なるべく渡す情報を減らしたいので、学籍番号でアクセスできるようにしたい
  * `ssh student_id@student_id.example.com` のようなホスト名でアクセスできるとよい
* SSH鍵ペアは事前に作っておく
  * セキュアに渡せる経路は別途確保してある。
* AWS それ自体の使い方を学習させるにはちょっと重い気がする
  * そういう時間を割けるならば AWS Education を使うのも手。無料だし。
* 環境は演習が終わったらすぐ捨てるが、鍵は来週再利用する

# CloudFormation

Infrastructure as Code に基づき、AWS の EC2 やセキュリティ設定を含むいろんなリソースを設定ファイル(テンプレート)一つで与えられる仕組みのようだ。
略称は cfn。


# 方法

cfn の Nested Stack で学生1人に割り当てるスタックを人数分コピペでインクルードする。

## EC2 の起動イメージ (AMI) の準備

* AMI は Ubuntu 18 LTS の公式を以下のように弄ったもの

  ```sh
apt-get update -y
apt-get install -y python-pip
apt-get install -y python-setuptools
mkdir -p /opt/aws/bin
python /usr/lib/python2.7/dist-packages/easy_install.py --script-dir /opt/aws/bin https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz
```


## EC2 インスタンス等の設定ファイル

* 学生に渡す EC2 と Route53 とポート設定をまとめたファイル。　https://gist.github.com/mmasko/66d34b651642525c63cd39251e0c2a8b を元に色々と細かく修正した。
    * cfn-init がエラーを吐くのだが原因はよくわからない。とりあえず動いているのでヨシ！
* Elastic IP で固定IPを割り当てている。
    * 80人となると　`The maximum number of addresses has been reached.`　のエラーが出る。 [上限値の変更申請を日本語で出せる。](https://qiita.com/msyk_tym/items/e3bbed8d382d06a6a329)
* example.com は Route53 で確保したドメイン名に置き換える。 例えば `.name` は最も安い TLD の一つのようだった (Route53 の何かの PDF ドキュメントに載っていた価格一覧を見る限り)。
* kisojikken というのは講義の名前



## 初期ファイルとスクリプト

3つのスクリプトをスタック作成時においている。

* `/etc/kisojikken/mkusers.sh (学籍番号)`
    * ユーザを作る
* `/etc/kisojikken/pubkey.sh`
    * 公開鍵をダウンロードしてきて .ssh/authorized_key に加える
    * 公開鍵の tar.gz を置いてある URL をハードコードしている:-p
* `/etc/kisojikken/init.sh (学籍番号)`
    * スタック作成時に上記2つのスクリプトを呼び出す 

その他の `/etc/cfn/cfn-hup.conf` などは参考元のスクリプトからそのまま持ってきたもので、 `cfn-init` が使っている (はず)

## テンプレート (ネストされる側)

```yaml
Parameters:
  EnvironmentSize:
    Type: String
    Default: t3a.nano
    AllowedValues:
      - t2.nano
      - t3a.nano
      - t3a.small
    Description: Select instance size
  KeyPair:
    Default: somekey
    Description: Existing keypair
    Type: AWS::EC2::KeyPair::KeyName
  StudentID:
    Description: Student ID to create
    Type: String
  SecurityGroupName:
    Default: kisojikken2020-securitygroup
    Description: Security Group's Name
    Type: String
  ImageId:
    Default: ami-02fdb41f362247f2e
    Description: AMI Image Id
    Type: String
Resources:
  DNSRecordSet:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName: example.com.
      Name: {"Fn::Join": [ ".", [ !Ref StudentID, "example.com" ]]}
      Type: A
      TTL: '300'
      ResourceRecords:
      - !Ref PublicIP
    DependsOn: PublicIP  
  PublicIP:
    Type: AWS::EC2::EIP
    Properties: 
      Domain: vpc
      InstanceId: !Ref NestedInstance
  NestedInstance:
    Type: AWS::EC2::Instance
    CreationPolicy:
      ResourceSignal:
        Timeout: PT5M
    Properties:
      ImageId: !Ref ImageId
      InstanceType: !Ref EnvironmentSize
      SecurityGroups:
        - !Ref SecurityGroupName
      KeyName: !Ref KeyPair
      Tags:
        - Key: Name
          Value: !Ref StudentID
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource NestedInstance --configsets setup --region ${AWS::Region}
          /etc/kisojikken/init.sh ${StudentID}
          /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource NestedInstance --region ${AWS::Region}
    Metadata:
      AWS::CloudFormation::Init:
        configSets:
          setup:
            - "configure_cfn"
        configure_cfn:
          files:
            /etc/kisojikken/init.sh:
              content: !Sub |
                #!/bin/bash
                set -e
                
                USERS=$*
                /etc/kisojikken/mkusers.sh $USERS
                /etc/kisojikken/pubkey.sh
              mode: "000555"
              owner: root
              group: root
            /etc/kisojikken/mkusers.sh:
              content: !Sub |
                #!/bin/bash
                set -e
                
                USERS=$*
                
                for U in $USERS
                do
                    adduser --disabled-password --gecos "" $U
                    usermod -G sudo $U
                done
              mode: "000555"
              owner: root
              group: root
            /etc/kisojikken/pubkey.sh:
              content: !Sub |
                #!/bin/bash
                set -e
                PUBKEYDIR=/etc/kisojikken2020/pubkeys
                
                mkdir -p $PUBKEYDIR
                pushd $PUBKEYDIR > /dev/null
                curl -s (鍵が置いてあるURL) |tar zxf - --overwrite
                popd > /dev/null
                
                for u in `ls /home`
                do
                    if [ -d $PUBKEYDIR/$u ];
                    then
                	SSHDIR=/home/$u/.ssh
                	mkdir -p $SSHDIR
                	cat $PUBKEYDIR/$u/id_rsa.pub >> $SSHDIR/authorized_keys
                	chown -R $u $SSHDIR
                	chmod -R go-rwx $SSHDIR
                	echo ssh key added for user $u
                    fi
                done
              mode: "000555"
              owner: root
              group: root
            /etc/cfn/cfn-hup.conf:
              content: !Sub |
                [main]
                stack=${AWS::StackId}
                region=${AWS::Region}
                verbose=true
                interval=5
              mode: "000400"
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Sub |
                [cfn-auto-reloader-hook]
                triggers=post.update
                path=Resources.NestedInstance.Metadata.AWS::CloudFormation::Init
                action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource NestedInstance --configsets setup --region ${AWS::Region}
              mode: "000400"
              owner: root
              group: root
            /lib/systemd/system/cfn-hup.service:
              content: !Sub |
                [Unit]
                Description=cfn-hup daemon
                
                [Service]
                Type=simple
                ExecStart=/opt/aws/bin/cfn-hup
                Restart=always
                
                [Install]
                WantedBy=multi-user.target
              mode: "000400"
              owner: root
              group: root
          commands:
            01_enable_cfn-hup:
              command: "systemctl enable cfn-hup.service"
            02_start_cfn-hup:
              command: "systemctl start cfn-hup.service"
```

# テンプレート (ネストする側)

```yaml
Parameters:
  EnvironmentSize:
    Type: String
    Default: t3a.nano
    AllowedValues:
      - t2.nano
      - t3a.nano
      - t3a.small
    Description: Select instance size
  KeyPair:
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instance.
    ConstraintDescription: must be the name of an existing EC2 KeyPair.
    Type: AWS::EC2::KeyPair::KeyName
  SecurityGroupName:
    Description: Security Group's Name
    Default: kisojikken2020-securitygroup
    Type: String
  ImageId:
    Default: ami-02fdb41f362247f2e
    Type: String
  InstanceTemplateURL:
    Default: (上記 kinstance.template のURL)
    Description: template url 
    Type: String

Resources:
  y3033111:
    Type: 'AWS::CloudFormation::Stack'
    Properties:
      TemplateURL: !Ref InstanceTemplateURL
      Parameters:
        EnvironmentSize: !Ref EnvironmentSize
        SecurityGroupName: !Ref SecurityGroupName
        ImageId: !Ref ImageId
        KeyPair: !Ref KeyPair
        StudentID: *****
  z3033003:
    Type: 'AWS::CloudFormation::Stack'
    Properties:
      TemplateURL: !Ref InstanceTemplateURL
      Parameters:
        EnvironmentSize: !Ref EnvironmentSize
        SecurityGroupName: !Ref SecurityGroupName
        ImageId: !Ref ImageId
        KeyPair: !Ref KeyPair
        StudentID: *****
.... (これが人数分つづく)
```