ちょっと話題の記事

AWS Batchでシェルスクリプトを実行する典型的パターンのご紹介

2017.11.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

去年のre:Invent 2016で発表された、AWS Batch。みなさん、お使いでしょうか?

え、名前はシンプルなのに、案外むずかしい? であれば、自分とまったく同じ印象です。

今回は、AWS Batchを利用して、シェルスクリプトを実行するための典型的パターンをご紹介。シェルスクリプトからは、任意のスクリプト(Pythonとか、Node.js)も呼び出すことができ汎用性が高いので、AWS Batchにおけるアプリケーション処理の実装方式として、参考にしていただければと思います。

ほな、いってみよ!

構成図

今回構築する環境の概要は以下の通り。シェルは大きく2つに別れます。AWSの構成要素は非常にシンプルです。

  • myjob.sh

ユーザーが作成するシェル。アプリケーション処理の本体はこちらに記述し、事前に任意のS3バケットにアップロードしておく。

  • fetch_and_run.sh

myjob.shを実行するラッパーとなるシェル。S3バケットからmyjob.shをダウンロードし、コンテナ内で実行する。コンテナイメージをビルドする際に、このシェルは埋め込まれます。

今回のチュートリアルでは、サンプルとなるmyjob.shを用意し、実行させて結果を確認します。myjob.shの名前やS3バケットの格納場所は、AWS Batchのジョブ定義で環境変数に指定可能なので、変更はいつでも可能です。

この記事を実施する前提条件

この記事では、チュートリアル形式で、AWS Batchのジョブ定義や環境などを一式作っていきますが、前提条件があります。

AWS Batchの初期設定を実施していること

AWS Batchは関連コンポーネントが非常に多く、また、裏でECSも動いているため、ぶっちゃけ敷居が高いサービスです。まだAWS Batch未経験の方は、以下の2つの公式ドキュメントを実施し、基礎を習得しておきましょう。

これで、関連コポーネンやロールなどが一通り作成されて、後続作業が楽になります。

また、最初の一歩にはこちらも合わせて参照いただければ、理解が早い。むっちゃ分かりやすいです。

クライアントでDockerのセットアップを完了していること

AWS BatchではAmazon ECRのリポジトリからコンテナを起動して処理を実行します。Amazon ECRはマネージド型AWS Dockerレジストリサービスで、Dockerコマンドにて、イメージのプッシュ、プル、管理ができます。

今回の作業でも、シェルを実行するための簡単なDockerイメージを、Dockerコマンドを利用して作成します。下記公式サイトより、環境セットアップを実施しておいてください。

AWS CLIがセットアップされていること

AWS CLIもECRへのDockerイメージ登録のために必要になります。未導入の人は、こちらを参考に

手順1:Dockerイメージのビルド

今回は、AWSが公式で用意しているGitHubリポジトリから関連ファイルをダウンロードします。

GitHub aws-batch-helpers:https://github.com/awslabs/aws-batch-helpers/archive/master.zip

ダウンロードしたZIPファイルを適当な場所で解凍し、fetch-and-runフォルダに移動すると、以下のファイルがあります。

Dockerfile

FROM amazonlinux:latest

RUN yum -y install unzip aws-cli
ADD fetch_and_run.sh /usr/local/bin/fetch_and_run.sh
WORKDIR /tmp
USER nobody

ENTRYPOINT ["/usr/local/bin/fetch_and_run.sh"]

Dockerイメージをビルドするために利用されます。unzipは、シェルスクリプトをS3にZIP形式で格納しても対応できるように導入されています。コンテナのエントリーポイントが、構成図で説明したfetch_and_run.shになります。

以下のコマンドで、Dockerイメージをビルドします。

$ docker build -t awsbatch/fetch_and_run .
Sending build context to Docker daemon 8.192kB
Step 1/6 : FROM amazonlinux:latest
---> 6133b2c7d7c2
Step 2/6 : RUN yum -y install which unzip aws-cli
---> Running in 0ef2ff3eca33
Loaded plugins: ovl, priorities
Resolving Dependencies
--> Running transaction check
---> Package aws-cli.noarch 0:1.11.132-1.47.amzn1 will be installed
--> Processing Dependency: python27-jmespath = 0.9.2 for package: aws-cli-1.11.132-1.47.amzn1.noarch
--> Processing Dependency: python27-botocore = 1.5.95 for package: aws-cli-1.11.132-1.47.amzn1.noarch
--> Processing Dependency: python27-rsa >= 3.1.2-4.7 for package: aws-cli-1.11.132-1.47.amzn1.noarch
--> Processing Dependency: python27-futures >= 2.2.0 for package: aws-cli-1.11.132-1.47.amzn1.noarch
--> Processing Dependency: python27-docutils >= 0.10 for package: aws-cli-1.11.132-1.47.amzn1.noarch
--> Processing Dependency: python27-colorama >= 0.2.5 for package: aws-cli-1.11.132-1.47.amzn1.noarch
--> Processing Dependency: python27-PyYAML >= 3.10 for package: aws-

<< 途中省略 >>

Successfully built e833d431f70c
Successfully tagged awsbatch/fetch_and_run:latest

ビルド成功後、作成されたイメージを確認します。

$ docker images
REPOSITORY                   TAG                 IMAGE ID            CREATED             SIZE
awsbatch/fetch_and_run       latest              e833d431f70c        11 minutes ago      251MB
amazonlinux                  latest              6133b2c7d7c2        2 weeks ago         165MB

無事、イメージのawsbatch/fetch_and_runが作成されていればOKです。

手順2:Amazon ECRの作成

AWS Batchで利用するDockerイメージを格納するためのECRリポジトリを作成します。

  1. Amazon ECSコンソールのリポジトリから、「今すぐ始める」をクリック
  2. リポジトリ名に「awsbatch/fetch_and_run」を入力し、「次のステップ」をクリック
  3. Dockerイメージのプッシュコマンドが表示されます。

ここに掲載されているコマンドを叩いていけば、ECRにイメージを登録できます。

手順3:DockerイメージのECRへの登録

手順2で、Webコンソール上に表示されたコマンドを順番に実施していきます。

以下はコマンド例です。XXXXYYYYZZZZで埋められているところは、個人のAWSアカウントIDが入ります。

$aws ecr get-login --no-include-email --region ap-northeast-1

  <<上の実行結果で表示されたログインコマンドを実行>>

$docker tag awsbatch/fetch_and_run:latest XXXXYYYYZZZZ.dkr.ecr.ap-northeast-1.amazonaws.com/awsbatch/fetch_and_run:latest

$docker push XXXXYYYYZZZZ.dkr.ecr.ap-northeast-1.amazonaws.com/awsbatch/fetch_and_run:latest

最終的に、Webコンソール上で、以下のようにlatestタグが表示されていればOKです。

手順4:動作確認用のサンプルシェルの作成とS3へのアップロード

アプリケーションを実行するためのサンプルシェル、myjob.shを作成します。環境変数を読み取って、echoだけしているシンプルなシェルです。

これも、先述したGitHubのダウンロードファイルに含まれています。

myjob.sh

#!/bin/bash
date
echo "Args: $@"
env
echo "This is my simple test job!."
echo "jobId: $AWS_BATCH_JOB_ID"
echo "jobQueue: $AWS_BATCH_JQ_NAME"
echo "computeEnvironment: $AWS_BATCH_CE_NAME"
sleep $1
date
echo "bye bye!!"

作成後、以下のコマンドで任意のS3バケットにアップロードします。

$aws s3 cp myjob.sh s3://<mybucket>/myjob.sh

手順5:IAMロールの作成

作成したfetch_and_runイメージは、AWS Batchジョブとして動作します。今回は、シェルスクリプトをS3から取得するため、それに必要なIAMロールを準備します。

  1. IAM Management Console画面で、新規ロールを作成します。

  2. EC2 Container ServiceのEC2 Container Service Taskを選択します。

  1. アタッチポリシー画面にて、[AmazonS3ReadOnlyAccess]をアタッチします。

  2. 名前に、ロール名「batchJobRole」をつけて、ロールを作成します。

手順6:AWS Batchのジョブ実行環境の用意

AWS Batchのジョブの動作に必要となる実行環境を用意します。

AWS Batch Compute-environments、から作成します。

今回は、既に作成していただいている環境を利用していただいて構いませんが、別の環境が必要であれば作成しておいてください。

手順7:AWS BatchのJob queuesの作成

任意のジョブ実行環境を選択し、Job queuesを作成します。

AWS Batch Job queues、から作成します。

  1. 「Create queue」ボタンをクリック

  2. 以下のパラメータを指定

  • Queue nameに「fetch_and_run-job-queue」
  • Select a compute environmentに既に作成済みのジョブ実行環境を指定
  • その他はデフォルト
  1. 「Create」ボタンをクリックし、Job queueが作成されたことを確認

手順8:AWS Batchのジョブ定義

必要なリソースは一通り作成完了したので、全てをまとめるAWS Batchのジョブ定義を作成します。

AWS Batch Job definitions、から作成します。

  1. 「Create」ボタンをクリック

  2. 以下で、パラメータを指定

  • Job definition nameに[fetch_and_run]
  • Job Roleに手順5で作成したIAMロール[batchJobRole]
  • Container imageに手順3で登録したECRのリポジトリURI[XXXXYYYYZZZZ.dkr.ecr.ap-northeast-1.amazonaws.com/awsbatch/fetch_and_run]
  • Command欄は空白
  • vCPUを[1]、Memoryを[200]、JobAttemptsを[1]
  1. 「Create job definition」ボタンをクリックし、ジョブ定義が作成されていることを確認

手順9:ジョブの送信

いよいよ最後、実際にジョブを実行するためのジョブ送信を実施します。ジョブ送信時に、ジョブ実行環境と共に、処理に必要な環境変数を設定します。

AWS Batch、から送信します。

  1. 「Submit Job」ボタンをクリック

  2. 以下のパラメーターを設定

  • Job nameに[script_test]
  • Job definitionに[fetch_and_run]の最新バージョン
  • Job queueに[fetch_and_run-job-queue]
  • Commandに[myjob.sh 60]
  • Environmemt variablesに以下を指定
  • Key=BATCH_FILE_TYPE, Value=script
  • Key=BATCH_FILE_S3_URL, Value=s3://mybucket/myjob.sh. (s3にアップロードしたバケットを指定)
  1. 「Submit job」ボタンをクリック

あとは、ジョブのステータスを更新していき、無事にsucceededになれば成功。お疲れ様でした。実行結果のログを眺めてみて、無事、myjob.shの内容が出力されていることを確認してください。

シェル本体(fetch_and_run.sh)の解説

コンテナに格納されている、fetch_and_run.shのソースはこちら。130行程度ですが、内容は、S3から該当スクリプトをダウンロードして実行しているのみです。エラー処理も実装されているので、使いやすい。

aws-batch-helpers/fetch_and_run.sh at master · awslabs/aws-batch-helpers

fetch_and_run.sh

#!/bin/bash

# Copyright 2013-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the
# License. A copy of the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
# OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and
# limitations under the License.

# This script can help you download and run a script from S3 using aws-cli.
# It can also download a zip file from S3 and run a script from inside.
# See below for usage instructions.

PATH="/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin"
BASENAME="${0##*/}"

usage () {
  if [ "${#@}" -ne 0 ]; then
    echo "* ${*}"
    echo
  fi
  cat <<ENDUSAGE
Usage:

export BATCH_FILE_TYPE="script"
export BATCH_FILE_S3_URL="s3://my-bucket/my-script"
${BASENAME} script-from-s3 [ <script arguments> ]

  - or -

export BATCH_FILE_TYPE="zip"
export BATCH_FILE_S3_URL="s3://my-bucket/my-zip"
${BASENAME} script-from-zip [ <script arguments> ]
ENDUSAGE

  exit 2
}

# Standard function to print an error and exit with a failing return code
error_exit () {
  echo "${BASENAME} - ${1}" >&2
  exit 1
}

# Check what environment variables are set
if [ -z "${BATCH_FILE_TYPE}" ]; then
  usage "BATCH_FILE_TYPE not set, unable to determine type (zip/script) of URL ${BATCH_FILE_S3_URL}"
fi

if [ -z "${BATCH_FILE_S3_URL}" ]; then
  usage "BATCH_FILE_S3_URL not set. No object to download."
fi

scheme="$(echo "${BATCH_FILE_S3_URL}" | cut -d: -f1)"
if [ "${scheme}" != "s3" ]; then
  usage "BATCH_FILE_S3_URL must be for an S3 object; expecting URL starting with s3://"
fi

# Check that necessary programs are available
which aws >/dev/null 2>&1 || error_exit "Unable to find AWS CLI executable."
which unzip >/dev/null 2>&1 || error_exit "Unable to find unzip executable."

# Create a temporary directory to hold the downloaded contents, and make sure
# it's removed later, unless the user set KEEP_BATCH_FILE_CONTENTS.
cleanup () {
   if [ -z "${KEEP_BATCH_FILE_CONTENTS}" ] \
     && [ -n "${TMPDIR}" ] \
     && [ "${TMPDIR}" != "/" ]; then
      rm -r "${TMPDIR}"
   fi
}
trap 'cleanup' EXIT HUP INT QUIT TERM
# mktemp arguments are not very portable.  We make a temporary directory with
# portable arguments, then use a consistent filename within.
TMPDIR="$(mktemp -d -t tmp.XXXXXXXXX)" || error_exit "Failed to create temp directory."
TMPFILE="${TMPDIR}/batch-file-temp"
install -m 0600 /dev/null "${TMPFILE}" || error_exit "Failed to create temp file."

# Fetch and run a script
fetch_and_run_script () {
  # Create a temporary file and download the script
  aws s3 cp "${BATCH_FILE_S3_URL}" - > "${TMPFILE}" || error_exit "Failed to download S3 script."

  # Make the temporary file executable and run it with any given arguments
  local script="./${1}"; shift
  chmod u+x "${TMPFILE}" || error_exit "Failed to chmod script."
  exec ${TMPFILE} "${@}" || error_exit "Failed to execute script."
}

# Download a zip and run a specified script from inside
fetch_and_run_zip () {
  # Create a temporary file and download the zip file
  aws s3 cp "${BATCH_FILE_S3_URL}" - > "${TMPFILE}" || error_exit "Failed to download S3 zip file from ${BATCH_FILE_S3_URL}"

  # Create a temporary directory and unpack the zip file
  cd "${TMPDIR}" || error_exit "Unable to cd to temporary directory."
  unzip -q "${TMPFILE}" || error_exit "Failed to unpack zip file."

  # Use first argument as script name and pass the rest to the script
  local script="./${1}"; shift
  [ -r "${script}" ] || error_exit "Did not find specified script '${script}' in zip from ${BATCH_FILE_S3_URL}"
  chmod u+x "${script}" || error_exit "Failed to chmod script."
  exec "${script}" "${@}" || error_exit " Failed to execute script."
}

# Main - dispatch user request to appropriate function
case ${BATCH_FILE_TYPE} in
  zip)
    if [ ${#@} -eq 0 ]; then
      usage "zip format requires at least one argument - the script to run from inside"
    fi
    fetch_and_run_zip "${@}"
    ;;

  script)
    fetch_and_run_script "${@}"
    ;;

  *)
    usage "Unsupported value for BATCH_FILE_TYPE. Expected (zip/script)."
    ;;
esac

シェル利用に必要な環境変数はこちら。

環境変数名 引数解説
BATCH_FILE_TYPE シェルスクリプトファイルの形式(zip圧縮 or テキストファイル) zip or script
BATCH_FILE_S3_URL シェルスクリプトが格納されているS3のURL s3://hamada-myjob/myjob.sh

これらの環境変数は、上の手順でも説明しましたが、ジョブ定義のEnvironment variablesで設定しましょう。ジョブ送信のタイミングで環境変数を変更することで、任意の処理を実行可能です。使いやすいっすね。

スクリプトはzip形式にも対応しているので、スクリプトを複数ファイル(python等、任意のスクリプト)も含めて記述する場合は、zip圧縮しておくことで対応可能です。

このシェルスクリプト自体は非常にシンプルなので、これを参考に、ソースファイルをCodeCommitから取得して実行するなど、いかようにも改変可能かと思います。其の場合は、コンテナに付与するIAMロールの変更なども忘れずに。

pythonスクリプトなど使いたい場合は、Dockerfileを編集して、イメージビルド時にインストールするパッケージを追加することで対応可能です。

AWS Batchにおけるシェルスクリプト実行の典型的ユースケースとして

アプリケーションの継続的なメンテナンスを考慮すると、コンテナ内にアプリケーション処理の本体を格納せずに外出しておくのは基本です。今回の方法では、コンテナ内にはアプリケーション処理をラップするシェルのみ格納しておき、メインの処理はS3に格納することで、保守性を向上しています。

myjob.shで、関連ファイルの実行など(他の言語の利用)もできるので、応用範囲はかなり広いと思います。

AWS Batchは、AWSの歴史の中でも比較的最近リリースされたサービスですが、その可能性は相当広いと改めて思いました。ECSやコンテナの知識が必須で敷居が高いのですが、できることはむちゃくちゃ幅広いです。みなさんもこれを機会に、体験してみてはどうでしょう?

 __
(祭) ∧ ∧
 Y  ( ゚Д゚)
 Φ[_ソ__y_l〉     AWS Batchダ ワッショイ
    |_|_|
    し'´J

それでは、今日はこのへんで。濱田でした。

AWS Batchの参考記事

AWS Batchの環境一式を作っていく時に注意しておくべき点が、コンパクトに纏まっています。実運用に入る前に読んでおくと参考になる点が多いかと思います。

AWS Batchは歴史が浅いため、Webコンソールの機能がまだ限定的だったりします。この記事を参考に、任意の値をCloudWatchに投げてみると、運用上いろいろ捗るかと思います。