AWS CLIを使ってクラスター環境を準備する方法（あるいはCouchbaseインストール手順 AWS編）

# はじめに

分散処理環境を扱うエンジニアにとって、時々の検証の必要性に応じたクラスターを構築するために、クラウドを活用することができます。

本来の目的である検証自体に集中するためにも、クラスター環境の構築は、再現性があり、かつ容易に構成変更できるものとしたいところです。

世の中には、様々な構成管理ツールが存在しており、それらは本番環境運用のように、構成管理そのものの信頼性や運用の標準化を求める場合には非常に有用なものですが、ここでの、検証環境の構築という目的のためには、最低限必ず必要になる技術要素のみで、目的を達成したいと思います。

ここでは、クラウドとしてAWSを利用します。スクリプトの実行環境としては、Macを用い、CLIのインストールなどは完了しているものとします。スクリプト化に際して、SSHを利用していますが、SSHについての説明も割愛します。

ここで紹介する手順は、Hadoopなど、様々な分散処理環境の準備に応用できるものですが、ここではCouchbase Serverのインストールを実例として説明します。

## EC2インスタンス作成

VMを構築するスクリプトの本体（`create_aws_instances.sh`）は以下のようなものです。

```shell
#!/bin/bash

. ./my_aws.config

echo "KEY_NAME:" $KEY_NAME
echo "TAG_PROJECT:" $TAG_PROJECT
echo "TAG_OWNER:" $TAG_OWNER
echo "INSTANCE_NAME:" $INSTANCE_NAME
echo "OS_USER:" $OS_USER

LANG=c date +%Y%m%d

CLI_INPUT_JSON=file://my_cli_input.json

TAG_DATE=`LANG=c date +%Y%m%d`
TAG_DATE_TIME=`LANG=c date +%Y%m%d_%H%M`
TAG_ENDDATE=`LANG=c date -v +7d +%m%d%Y`

INSTANCE_FULLNAME="${INSTANCE_NAME}_${TAG_DATE_TIME}"

TAG_SPECS="ResourceType=instance,Tags=[\
{Key=Name,Value=${INSTANCE_FULLNAME}},\
{Key=owner,Value=${TAG_OWNER}},\
{Key=enddate,Value=${TAG_ENDDATE}},\
{Key=project,Value=${TAG_PROJECT}}]"

MSG_DRYRUN_EXPECTED="Request would have succeeded, but DryRun flag is set"

MSG_DRYRUN=$(aws ec2 run-instances --dry-run --region $REGION \
  --instance-type $INSTANCE_TYPE \
  --tag-specifications $TAG_SPECS \
  --cli-input-json $CLI_INPUT_JSON 2>&1 )

echo $MSG_DRYRUN

if [[ $MSG_DRYRUN = *$MSG_DRYRUN_EXPECTED* ]]; then
   echo "Dry Run Succeeded."
else
   echo "Dry Run Failed."
   exit
fi

FILENAME_PASSWORDLESS_SHELL="passwordless_${INSTANCE_NAME}_${TAG_DATE_TIME}.sh"

function create_instance() {

if [ $# -ne 0 ]; then 
  NODE_TYPE=$1
  INSTANCE_FULLNAME="${INSTANCE_NAME}_${TAG_DATE_TIME}_${NODE_TYPE}"
fi

echo $INSTANCE_FULLNAME

TAG_SPECS="ResourceType=instance,Tags=[\
{Key=Name,Value=${INSTANCE_FULLNAME}},\
{Key=owner,Value=${TAG_OWNER}},\
{Key=enddate,Value=${TAG_ENDDATE}},\
{Key=project,Value=${TAG_PROJECT}}]"

echo $TAG_SPECS

INSTANCE_ID=$(aws ec2 run-instances --region $REGION \
  --instance-type $INSTANCE_TYPE \
  --tag-specifications $TAG_SPECS \
  --query 'Instances[].InstanceId' \
  --output text \
  --user-data=file://"init.sh" \
  --cli-input-json $CLI_INPUT_JSON )

echo $?
echo Instance ID: $INSTANCE_ID

aws ec2 wait instance-running --instance-ids $INSTANCE_ID; echo 'Instance is prepared.'
aws ec2 modify-instance-attribute \
   --instance-id $INSTANCE_ID \
   --no-disable-api-termination


PUBLIC_IP=$(aws ec2 describe-instances --instance-id $INSTANCE_ID  --query 'Reservations[].Instances[].PublicIpAddress' --output text)

echo Public IP: $PUBLIC_IP

cat <<EOF >> $FILENAME_PASSWORDLESS_SHELL
cat ~/.ssh/id_rsa.pub | ssh  -o "StrictHostKeyChecking=no" -i $KEY_NAME.pem $OS_USER@$PUBLIC_IP 'cat >> .ssh/authorized_keys'
ssh  -o "StrictHostKeyChecking=no" -i $KEY_NAME.pem $OS_USER@$PUBLIC_IP 'chmod 700 .ssh; chmod 640 .ssh/authorized_keys'

EOF
}


for node in ${NODES[@]}
do
  create_instance $node
done
chmod +x $FILENAME_PASSWORDLESS_SHELL
```

### 設定の定義

上記スクリプトの初めの部分で、読みこまれている`my_aws.config`は、以下のようなものです。

`<YOUR_...>`の部分や`INSTANCE_TYPE`、`OS_USER`の値は、適宜置き換えてください。

```shell
KEY_NAME=~/<YOUR_KEY_NAME>


INSTANCE_NAME=<YOUR_INSTANCE_NAME>
REGION=<YOUR_REGION>

TAG_PROJECT=<YOUR_TAG_PROJECT>
TAG_OWNER=<YOUR_TAG_OWNER>

INSTANCE_TYPE=t2.medium
OS_USER=ec2-user

NODES=(CB01 CB02 CB03 CB04 CB05 CB06)
```

最後の行は、６ノードからなるCouchbase Serverクラスターを構築するために、６つのインスタンスを作成することを意味します。それぞれのインスタンスには、指定した名前が付されます。
Hadoopの場合は、`(MASTER1 MASTER2 WORKER1 WORKER2 WORKER3)`のようにしても良いでしょう。

上記スクリプト実行により、ドライラン実行後、`NODES`変数に指定した数のEC2インスタンスを作成します。

また、`passwordless_${INSTANCE_NAME}_${TAG_DATE_TIME}.sh`という名前のシェルスクリプトファイルを生成します。

注：上記のスクリプト中に、EC2インスタンスにタグを付与している箇所があります。多くは（`Key=Name`以外は）任意のものですが、今回記事にするに当たって、あえて取り除かず残してあります。特に`Key=enddate`は、混乱を招くかもしれないため注釈すると、ここで指定している日付（作成時点の７日後）も、あくまで任意のものです。このようなスクリプトによるタグ内容の生成は、組織から提供されたクラウドのアカウントを使っている際に、特定のタグが付与されていない場合には自動的にインスタンスが削除される運用が行われている場合に、活用することができます。

### 初期実行スクリプト

上記スクリプトの下記抜粋にあるように、`aws ec2 run-instances`コマンドに`--user-data=file://"init.sh`というオプションを指定しています。

```
INSTANCE_ID=$(aws ec2 run-instances --region $REGION \
  --instance-type $INSTANCE_TYPE \
  --tag-specifications $TAG_SPECS \
  --query 'Instances[].InstanceId' \
  --output text \
  --user-data=file://"init.sh" \
  --cli-input-json $CLI_INPUT_JSON )
```

`init.sh`というファイルが実行しているディレクトリに配置されていることを想定しています。このスクリプトは、インスタンス作成後にインスタンス上で実行されます。

内容は任意ですが、例として、下記のように設定した時刻に自動的にシャットダウンする設定を加えることなどができます。

```
#! /bin/bash

echo "0 22 * * * root /sbin/shutdown -h now" >> /etc/crontab
```

### インスタンス定義

上記の抜粋の`--cli-input-json`オプションに指定されている変数(`$CLI_INPUT_JSON`)は、下記のように定義されています。このファイル（`my_cli_input.json`）も、同じディレクトリに配置されていることを前提としています。

```
CLI_INPUT_JSON=file://my_cli_input.json
```

内容は、下記のようなものです。
`<YOUR_...>`の部分や`InstanceType`、その他の値は、適宜置き換えてください。

```json
{
    "ImageId": "<YOUR_IMAGE_ID>",
    "KeyName": "<YOUR_KEY_NAME>",
    "SecurityGroupIds": [
         "<YOUR_SECURITY_GROUP_IP>"
    ],
    "InstanceType": "t2.medium",
    "BlockDeviceMappings": [
        {
            "DeviceName": "/dev/sda1",
            "Ebs": {
                "VolumeSize": 20,
                "DeleteOnTermination": true,
                "VolumeType": "standard"
            },
            "NoDevice": ""
        }
    ],
    "Monitoring": {
        "Enabled": false
    },
    "SubnetId": "<YOUR_SUBNET_ID>",
    "DisableApiTermination": true,
    "NetworkInterfaces": [
        {
            "DeviceIndex": 0,
            "AssociatePublicIpAddress": true
        }
    ]
}

```


## リモートログイン設定

パスワードを用いずに、EC2へのリモートログインを実行できるようにするために、生成されたシェルスクリプトを実行します。
このスクリプトの中身は、上記スクリプトの以下の部分が対応します。

```
cat ~/.ssh/id_rsa.pub | ssh  -o "StrictHostKeyChecking=no" -i $KEY_NAME.pem $OS_USER@$PUBLIC_IP 'cat >> .ssh/authorized_keys'
ssh  -o "StrictHostKeyChecking=no" -i $KEY_NAME.pem $OS_USER@$PUBLIC_IP 'chmod 700 .ssh; chmod 640 .ssh/authorized_keys'
```

さらに、下記のスクリプト（`renew_ssh_config.sh`)を実行します。

このスクリプトは、現在起動されているEC2インスタンスを対象として、SSH設定ファイルを生成します。

上掲のインスタンス作成用スクリプトでは、何度かインスタンスを作成した場合にも、EC2インスタンスの名前が重複しないよう年月日時分を使ってフルネームを生成していますが、ここでは`NODES`配列に利用した名前を使うため、同じ名前（上記フルネーム中の最後に`_`区切りを用いサフィックスとして追加）が付されたインスタンスが同時に起動していないことが前提となります。先に作ったインスタンスを停止せずに、付加的に、インスタンス作成用スクリプトを複数回実行する場合には、都度`NODES`配列に含まれる名前を変更する必要があります。

```shell
#!/bin/bash +x
 
DATE=`LANG=c date +%y%m%d_%H%M`

PATH_CONFIG=~/.ssh/
FILE_CONFIG=config


mv ${PATH_CONFIG}${FILE_CONFIG}{,.$DATE.bak}

aws ec2 describe-instances --output=text --query 'Reservations[].Instances[].{InstanceId: InstanceId, GlobalIP: join(`, `, NetworkInterfaces[].Association.PublicIp), State: State.Name, Name: Tags[?Key==`Name`].Value|[0]}' | grep running | awk '{printf "Host %s\n  HostName %s\n  User ec2-user\n  Port 22\n  ServerAliveInterval 60\n", substr($3,match($3,"_[^_]*$")+1), $1}' > ${PATH_CONFIG}${FILE_CONFIG}
```

注：実はこのSSH設定ファイル作成スクリプトには、OSのユーザ (`ec2-user`)を、ハードコードしています（最後のコマンド）。
このファイルも、初めのスクリプトで生成するよう修正することが考えられますが、差し当たり、このスクリプトを利用する場合には、適宜利用するOSに合わせて、編集してください。

ここまで実行すると、`my_aws.config`の定義を用いて、下記のようにVMにログインすることができるようになります。

```shell-session
$ ssh ec2-user@CB1
```

## リモートシェル実行（Couchbase Serverインストール)

上記のリモートログイン設定により、下記のスクリプト(`exec_on_hosts.sh`)を用いて、ローカル端末からVM上でのシェルのリモート実行が可能になります。

```shell
#!/bin/sh +x

. ./my_aws.config

echo "OS_USER:" $OS_USER

script=$1
LOG_BASE=$(basename ${1%.*})_`LANG=c date +%y%m%d_%H%M`
echo $LOG_BASE


for node in ${NODES[@]}
do
  ssh $OS_USER@$node 'LC_ALL=C sudo bash -s -x  2>&1' < $script > ${LOG_BASE}_${node}.log
done
```
上記のスクリプトは、下記のように、リモートで実行したいスクリプトを引数にとります。

```shell
$ ./exec_on_hosts.sh cb_install.sh
```

Couchbase Serverインストール用のスクリプト(`cb_install.sh`)は、以下のようなものです。

実際に、リモートOS上で実行できるスクリプトです（ローカルから実行するための特別な要素ははありません）。

```shell
#!/bin/bash -x

if [ ! -e ./couchbase-release-1.0-x86_64.rpm ]; then
    curl -O https://packages.couchbase.com/releases/couchbase-release/couchbase-release-1.0-x86_64.rpm
fi

sudo rpm -i ./couchbase-release-1.0-x86_64.rpm
sudo yum -y install couchbase-server

echo 'PATH="$HOME/.local/bin:$HOME/bin:$PATH:/opt/couchbase/bin"' >> .bashrc 
echo "export PATH" >> .bashrc 
echo 'MANPATH=$MANPATH:/opt/Couchbase/share/man' >> .bashrc 
echo "export MANPATH" >> .bashrc 

sudo systemctl status  couchbase-server
```

上記スクリプト実行によるログが、ローカルのファイルに出力されます。

ここまでが、一般的にクラスター環境準備のために活用できる部分です。

## Couchbase Serverクラスターの構成

ここからは、Couchbase Serverをクラスターとして構成する方法を説明します。

`http://(Public IP/DNS):8091`にアクセスします。

インストール直後の状態では、下記のような画面が表示されます。「Setup New Cluster」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/76f9d6f6-bf61-3d5f-5ad1-828037a44067.png)

「Host Name / IP Address」として、アクセスしているホストの情報を入力します。「Save & Finish」を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/eb19195e-9993-5a96-ba56-1943137d674e.png)

さらに必要な設定を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/dde67e51-ae3d-80c0-98cf-40db82b55d02.png)

１ノードからなるクラスターの構築は、以上で完了です。適宜ノードの追加を行います。

# 最後に

今回、目的を達成するために必要な手順の概観を伝えることを旨とし、内容の解説は最低限に留めました。引用したスクリプトは、必要な修正を加えた上で実行できるよう、ファイル全体を掲載しています。具体的な詳細は、スクリプト自体を参照してください。