AWS Batchで最新の(optimized)AMIをコンピューティング環境に適用するジョブ

2018年7月、東京リージョンにFargateがやってきます。

Fargate導入に向け、既存のECS向けに作った資産は極力Fargateで動かす方針です。
テンポラリなジョブはすべてAWS Batchで実行する予定です。
AWS Batchの土台となるEC2には定期的に最新のAMIを自動で適用して、その流れでproxy設定もAMIに仕込む。という作業もAWS Batchでさせるつもりなので、そのためのBatchを紹介します。

## Dockerfile
元となるジョブ定義に使用するコンテナイメージを作成するためのDockerfileです。

```Docker:Dockerfile
FROM alpine:3.6

RUN apk update \
 && apk upgrade \
 && apk add --no-cache \
        python \
        bash \
        curl \
 && apk add --no-cache --virtual .build-deps \
        wget \
        curl \
        py-pip \
        unzip \
 && wget --no-check-certificate -O /tmp/packer.zip https://releases.hashicorp.com/packer/1.2.4/packer_1.2.4_linux_amd64.zip \
 && unzip /tmp/packer.zip -d /usr/local/bin \
 && curl -o /bin/jq -L https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 \
 && chmod +x /bin/jq \
 && pip install awscli \
 && apk add tzdata \
 && cp -p /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
 && apk del tzdata \
 && apk del .build-deps \
 && rm -rf /var/cache/apk/* \
 && rm -rf /tmp/* /var/tmp/*

COPY packer.sh /bin/
RUN chmod +x /bin/packer.sh

COPY packerfile.org /

CMD packer.sh
```

## packerテンプレート

~~現時点(18/7/12)において、[HTTPプロキシ設定](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/http_proxy_config.html)では/etc/sysconfig/dockerに対するNO_PROXY 設定は 169.254.169.254だけとなっておりますが、169.254.170.2,/var/run/docker.sockが描き漏れています。(/var/run/docker.sockは不要かもしれません)~~
私の地道な布教の甲斐あって、現時点19/8/12では修正されてなされておられますご様子です。


```json:packerfile.org
{
    "builders":
    [
        {
            "type": "amazon-ebs",
            "ami_name": "{{%ENV%}}-{{%SOURCE_AMI%}}-{{%AMI_NAME}}",
            "region": "ap-northeast-1",
            "source_ami": "{{%SOURCE_AMI_ID%}}",
            "instance_type": "m4.large",
            "security_group_id": "{{%SECURITY_GROUP_ID%}}",
            "subnet_id": "{{%SUBNET_ID%}}",
            "ssh_timeout": "1m",
            "ssh_username": "ec2-user",
            "tags": {
                "base-ami": "{{%SOURCE_AMI_ID%}}"
            }

        }
    ],
    "provisioners": [
        {
            "type": "shell",
            "execute_command": "{{ .Vars }} sudo -E sh '{{ .Path }}'",
            "inline": [
                "sudo echo 'http_proxy={{%HTTP_PROXY%}}' >> /etc/ecs/ecs.config ",
                "sudo echo 'https_proxy={{%HTTPS_PROXY%}}' >> /etc/ecs/ecs.config ",
                "sudo echo 'no_proxy=169.254.169.254,169.254.170.2,/var/run/docker.sock' >> /etc/ecs/ecs.config ",
                "sudo echo 'env http_proxy={{%HTTP_PROXY%}}' >> /etc/init/ecs.override ",
                "sudo echo 'env https_proxy={{%HTTPS_PROXY%}}' >> /etc/init/ecs.override ",
                "sudo echo 'env no_proxy=169.254.169.254,169.254.170.2,/var/run/docker.sock' >> /etc/init/ecs.override ",
                "sudo echo 'export http_proxy={{%HTTP_PROXY%}}' >> /etc/sysconfig/docker ",
                "sudo echo 'export https_proxy={{%HTTPS_PROXY%}}' >> /etc/sysconfig/docker ",
                "sudo echo 'export no_proxy=169.254.169.254,169.254.170.2' >> /etc/sysconfig/docker "
                      ]
        }
    ]
}
```

## AMI入れ替えスクリプト

コンテナ内で実行する、AMI入れ替え作業をする元となるシェルスクリプトです。
一般的には、AWS Batchで実行するジョブの本体はs3等に置いておいて、Batch実行時にs3に取得しに行く流れみたいですが、コードの管理はgitLabでしっかりして、デプロイまでの流れは自動化するので大丈夫です。

```bash:packer.sh
#!/bin/bash

#####################
# ジョブ定義側で以下環境変数を設定する。
AMI_NAME=
BASE_COMPUTE_ENVIRONMENT=
HTTP_PROXY=
HTTPS_PROXY=
SECURITY_GROUP_IDS=
SUBNET_ID=
ENV=

YYYYMMDD=`date '+%Y%m%d%H%M'`

delete_batch_compute_environment()
{
        COMPUTE_ENVIRONMENT_ARN=$1
        COUNT=0
        LOOP_COUNT=10

        while [ ${COUNT} -lt ${LOOP_COUNT} -a "${STATE}" != "DISABLED" ]; do
                # 既存のコンピューティング環境をDISABLE
                aws batch update-compute-environment --compute-environment ${COMPUTE_ENVIRONMENT_ARN} --state DISABLED

                STATE=`aws batch describe-compute-environments \
                | jq '.computeEnvironments[]| select(.computeEnvironmentArn == "'${COMPUTE_ENVIRONMENT_ARN}'")|.state ' -r`

                let COUNT++
        done

        if [ ${COUNT} -lt ${LOOP_COUNT} ];then
                # DISABLEDになってもすぐに削除できないので3秒wait
                sleep 3
                aws batch  delete-compute-environment --compute-environment ${COMPUTE_ENVIRONMENT_ARN}
        else
                echo "computing environment is not DISABLED"
                exit 1
        fi

}

# regionの設定
aws configure set default.region ap-northeast-1

# 最新のオプティマイズAMI名を取得
aws ssm get-parameters  --names /aws/service/ecs/optimized-ami/amazon-linux/recommended > ssm_get_parameters.org

# 最新のオプティマイズAMI名を取得
SOURCE_AMI=`cat /ssm_get_parameters.org | jq .Parameters[0].Value -r | jq .image_name -r`
SOURCE_AMI_ID=`cat /ssm_get_parameters.org  | jq .Parameters[0].Value -r | jq .image_id -r`

echo "#### base ami: ${SOURCE_AMI} ####"

# packer設定ファイル作成
cat packerfile.org | sed -e "s/{{%AMI_NAME%}}/${AMI_NAME}/g" \
                         -e "s/{{%SOURCE_AMI%}}/${SOURCE_AMI}/g" \
                         -e "s/{{%SOURCE_AMI_ID%}}/${SOURCE_AMI_ID}/g" \
                         -e "s/{{%HTTP_PROXY%}}/${HTTP_PROXY}/g" \
                         -e "s/{{%HTTPS_PROXY%}}/${HTTPS_PROXY}/g" \
                         -e "s/{{%SECURITY_GROUP_ID%}}/${SECURITY_GROUP_ID}/g" \
                         -e "s/{{%SUBNET_ID%}}/${SUBNET_ID}/g" \
                         -e "s/{{%ENV%}}/${ENV}/g" \
                         > /packerfile.json

echo "#### packerfile.json ####"
cat /packerfile.json

# optimized AMIを作成
LATEST_SOURCE_AMI_ID=`packer build /packerfile.json | tee output.log | tail -2 | head -2  \
                      | awk 'match($0, /ami-.*/) { print substr($0, RSTART, RLENGTH) }'`

if [ "${LATEST_SOURCE_AMI_ID}" == "" ]; then
    echo "#### output.log ####"
    cat /output.log
	exit 0
fi

echo "#### optimized ami: ${LATEST_SOURCE_AMI_ID} ####"

# 最新のオプティマイズAMIから作成したAMIが登録されていないか確認する
# 仕様として、tagにbase-ami $SOURCE_AMI を含むことを記す
LIST_RECENT_SOURCE_AMI_ID=`aws ec2  describe-images --filters  Name=tag-key,Values=base-ami,Name=tag-value,Values=${SOURCE_AMI_ID} \
			| jq .Images[].ImageId -r`

for RECENT_SOURCE_AMI_ID in  ${LIST_RECENT_SOURCE_AMI_ID}; do
	LIST_COMPUTE_ENVIRONMENT_ARN+="`aws batch  describe-compute-environments \
                          | jq '.computeEnvironments[] | select(.computeResources.imageId == "'${RECENT_SOURCE_AMI_ID}'") | .computeEnvironmentArn' -r` "
done

for OLD_COMPUTE_ENVIRONMENT_ARN in ${LIST_COMPUTE_ENVIRONMENT_ARN} ; do

        # 新規コンピューティング環境の名称
        NEW_COMPUTE_ENVIRONMENT=${BASE_COMPUTE_ENVIRONMENT}-${YYYYMMDD}

        # 最近(RECENT) のAMIを使用したコンピューティング環境の設定ファイルを作成する
        # 元の環境のstate が ENABLEDでないとあとで失敗する
        aws batch describe-compute-environments \
        | jq '.computeEnvironments[] | select(.computeEnvironmentArn == "'${OLD_COMPUTE_ENVIRONMENT_ARN}'")| {computeEnvironmentName,type,state,computeResources,serviceRole}' \
        > /compute_environment.org

        echo "#### compute_environment.org ####"
        cat /compute_environment.org

        # 最新(LATEST) のAMIを使用したコンピューティング環境の設定ファイルを作成する
        cat compute_environment.org | sed -e "s/\"imageId\": \".*\"/\"imageId\": \"${LATEST_SOURCE_AMI_ID}\"/g" \
        -e "s/\"computeEnvironmentName\": \".*\"/\"computeEnvironmentName\": \"${NEW_COMPUTE_ENVIRONMENT}\"/g" \
         > /compute_environment.json

        echo "#### compute_environment.json ####"
        cat /compute_environment.json

        echo "#### create job environment:${NEW_COMPUTE_ENVIRONMENT} ####"
        NEW_COMPUTE_ENVIRONMENT_ARN=`aws batch create-compute-environment \
                                    --cli-input-json file://compute_environment.json | jq '.computeEnvironmentArn' -r`

        LIST_JOB_QUE=`aws batch describe-job-queues \
        | jq '.jobQueues | map(select(.computeEnvironmentOrder[].computeEnvironment=="'${OLD_COMPUTE_ENVIRONMENT_ARN}'")) | .[].jobQueueArn' -r`

        for JOB_QUE in ${LIST_JOB_QUE} ; do
                # job queueのstatusをupdateするために、ジョブに登録されている環境とorder を取得する
                LIST_JOB_QUE_COMPUTE_ENV=(`aws batch describe-job-queues --job-queues ${JOB_QUE}\
                                  | jq '.jobQueues[].computeEnvironmentOrder[].computeEnvironment' -r`)
                LIST_JOB_QUE_ORDER=(`aws batch describe-job-queues --job-queues ${JOB_QUE}\
                                  | jq '.jobQueues[].computeEnvironmentOrder[].order' -r`)

                UPDATE_LIST=""
                # ジョブが登録されている環境のうち、古いコンピューティング環境を新しいものと置き換える
                for ((i=0; i < ${#LIST_JOB_QUE_COMPUTE_ENV[@]}; i++)) ;do
                        if [ ${LIST_JOB_QUE_COMPUTE_ENV[i]} = ${OLD_COMPUTE_ENVIRONMENT_ARN} ]; then
                                UPDATE_LIST+="order=${LIST_JOB_QUE_ORDER[i]},computeEnvironment=${NEW_COMPUTE_ENVIRONMENT_ARN} "
                        else
                                UPDATE_LIST+="order=${LIST_JOB_QUE_ORDER[i]},computeEnvironment=${LIST_JOB_QUE_COMPUTE_ENV[i]} "
                        fi
                done

                echo "#### register job queue: ${JOB_QUE} ####"
                # ジョブ再登録
                aws batch update-job-queue --job-queue ${JOB_QUE} --compute-environment-order ${UPDATE_LIST}
        done

        echo "#### delete old compute environment: ${OLD_COMPUTE_ENVIRONMENT_ARN} ####"
        # コンピューティング環境の削除
        delete_batch_compute_environment ${OLD_COMPUTE_ENVIRONMENT_ARN}
done

exit 0
```