Cronから実行するEC2スナップショットスクリプト

実行条件

  • awscliがインストールされている
  • IAMロールなどでEC2周りの権限を解放しておく
#!/bin/sh

# 取得したインスタンスのidを並べる
INSTANCE_ID=(i-xxxxx1 i-xxxxx2 i-xxxxx3)

SHELLDIR=`dirname ${0}`
SHELLDIR=`cd ${SHELLDIR}; pwd`
SHELLNAME=`basename $0`

LOG_DIR="/var/log"
LOG_SAVE_PERIOD=14
LOG_FILE="${LOG_DIR}/${SHELLNAME}.log"
echo $LOG_FILE

REGION=ca-central-1
SNAPSHOTS_PERIOD=2

AWS="/usr/bin/aws --region ${REGION}"


rotate_log() {
    (( cnt=${LOG_SAVE_PERIOD} ))
    while (( cnt > 0 ))
    do
        logfile1=${LOG_FILE}.$cnt
        (( cnt=cnt-1 ))
        logfile2=${LOG_FILE}.$cnt
        if [ -f $logfile2 ]; then
            mv $logfile2 $logfile1
        fi
    done

    if [ -f $LOG_FILE ]; then
        mv ${LOG_FILE} ${LOG_FILE}.1
    fi
    touch $LOG_FILE
}

print_msg() {
    echo "`date '+%Y/%m/%d %H:%M:%S'` $1" | tee -a ${LOG_FILE}
}

create_snapshot() {
    for ID in `echo $@`
    do
        print_msg "Create snapshot Start"
        VOL_ID=`${AWS} ec2 describe-instances --instance-ids ${ID} --output text | grep EBS | awk '{print $5}'`
        if [ -z ${VOL_ID} ] ; then
            echo ${VOL_ID}
            print_msg "ERR:ec2-describe-instances"
            logger -f ${LOG_FILE}
            exit 1
        fi
        print_msg "ec2-describe-instances Success : ${VOL_ID}"
        ${AWS} ec2 create-snapshot --volume-id ${VOL_ID} --description "Created by SYSTEMBK(${ID}) from ${VOL_ID}" >> ${LOG_FILE} 2>&1
        if [ $? != 0 ] ; then
            print_msg "ERR:${SHELLDIR}/${SHELLNAME} ec2-create-snapshot"
            logger -f ${LOG_FILE}
            exit 1
        fi
        print_msg "Create snapshot End"
    done
}

delete_old_snapshot() {
    for ID in `echo $@`
    do
        VOL_ID=`${AWS} ec2 describe-instances --instance-ids ${ID} --output text | grep EBS | awk '{print $5}'`
        print_msg "Delete old snapshot Start"
        SNAPSHOTS=`${AWS} ec2 describe-snapshots --output text | grep ${VOL_ID} | grep "Created by SYSTEMBK" | wc -l`
        while [ ${SNAPSHOTS} -gt ${SNAPSHOTS_PERIOD} ]
        do
            ${AWS} ec2 delete-snapshot --snapshot-id `${AWS} ec2 describe-snapshots --output text | grep ${VOL_ID} | grep "Created by SYSTEMBK" | sort -k 11,11 | awk 'NR==1 {print $10}'` >> ${LOG_FILE} 2>&1
            if [ $? != 0 ] ; then
                print_msg "ERR:${SHELLDIR}/${SHELLNAME} ec2-delete-snapshot"
                logger -f ${LOG_FILE}
                exit 1
            fi
            SNAPSHOTS=`${AWS} ec2 describe-snapshots | grep ${VOL_ID} | grep "Created by SYSTEMBK" | wc -l`
        done
        print_msg "Delete old snapshot End"
    done
}

rotate_log

print_msg "INF:$SHELLDIR/${SHELLNAME} START"
create_snapshot ${INSTANCE_ID[@]}
delete_old_snapshot ${INSTANCE_ID[@]}
print_msg "INF:$SHELLDIR/${SHELLNAME} END"

exit 0

続きを読む

AWSのEC2で行うCentOS 7の初期設定

AWSのEC2で行うCentOS 7の最低限の初期設定をまとめてみました。まだすべき設定が残っているとは思いますので、こちらを更新していければと思ってい

開発環境

  • Mac OS X(El Capitan) 10.11.6
  • CentOS 7 (x86_64) – with Updates HVM

事前に用意しておく必要があるもの

  • 接続先EC2のパブリックDNS
  • デフォルトユーザ(CentOS 7の場合デフォルトはcentos)
  • EC2からダウンロードした秘密鍵(デフォルトは****.pem)

参考

AWSのEC2にSSH接続

秘密鍵の配置設定

以下のコマンドを実行してEC2からダウンロードした秘密鍵を【.ssh】ディレクトリに配置し、管理しやすくします。

$ mv /Users/ユーザ名/Downloads/秘密鍵名.pem ~/.ssh/

秘密鍵の権限設定

SSHを機能させるためには秘密鍵が公開されていないことが必要ですので、以下のコマンドを実行てし権限の設定をします。

$ chmod 400 ~/.ssh/秘密鍵名.pem

SSH接続

以下のコマンドを実行してAWSのEC2にSSH接続します。

$ ssh -i ~/.ssh/秘密鍵名.pem ユーザ名@パブリックDNS

ログイン完了

以下が表示がされたらログイン完了です。

[centos@ip-パブリックDNS ~]$

CentOS 7の初期設定

SELINUXの無効化

以下のコマンドを実行してSELINUXを無効化します。【Disabled】になったら無効化完了です。

# ステータス確認
$ getenforce

# 設定変更
$ sudo sed -i -e 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config

# 再起動
$ sudo reboot

# SSH再接続
$ ssh -i ~/.ssh/秘密鍵名.pem ユーザ名@パブリックDNS

# 無効化確認
$ getenforce

パッケージの更新

以下のコマンドを実行してCentOS 7のパッケージを更新します。

# パッケージの更新
$ sudo yum update -y

パッケージの自動更新設定

以下のコマンドを実行してyum-cronをインストールし、パッケージの自動更新を設定します。

# インストール
$ sudo yum install yum-cron -y

# 有効化確認
$ systemctl list-unit-files | grep yum-cron

# 有効化
$ sudo systemctl enable yum-cron

# 自動更新設定
$ sudo sed -i "s/^apply_updates.*$/apply_updates = yes/g" /etc/yum/yum-cron.conf

# 起動
$ sudo systemctl start yum-cron.service

# 起動確認
$ systemctl status yum-cron.service

タイムゾーンの変更

以下のコマンドを実行してタイムゾーンを変更します。

# 現在の設定確認
$ timedatectl status

# ローカルタイムを【Asia/Tokyo】に変更
$ sudo timedatectl set-timezone Asia/Tokyo

# 現在の設定確認
$ timedatectl status

ロケールとキーマップの変更

以下のコマンドを実行してロケールとキーマップを日本語対応に変更します。

# 現在の設定確認
$ localectl status

# 選択できるキーマップの確認
$ localectl list-keymaps

# ロケールを日本語とUTF-8に変更
$ sudo localectl set-locale LANG=ja_JP.utf8

# キーマップをjp106に変更
$ sudo localectl set-keymap jp106

# 現在の設定確認
$ localectl status

不要なサービスの停止(例:postfix)

以下のコマンドを実行して不要なサービスの停止をします。

# 有効化サービス一覧確認
$ systemctl list-unit-files --type service | grep enabled

# ステータス確認
$ systemctl status postfix.service

# 停止
$ sudo systemctl stop postfix.service

# 無効化
$ sudo systemctl disable postfix.service

# ステータス確認
$ systemctl status postfix.service

ユーザーアカウント追加

ユーザーアカウント追加

以下のコマンドを実行して新規ユーザーアカウントを追加します。今回は【newuser】として作成します。

# newuserを追加
$ sudo adduser newuser

sudo権限の変更

以下のコマンドを実行してsudoersファイルを安全に編集します。

# sudoersファイルの編集
$ sudo visudo

作成したnewuserグループをsudoコマンドがパスワード無しで実行できるように追記します。

## Same thing without a password
# %wheel        ALL=(ALL)       NOPASSWD: ALL
+ %newuser       ALL=(ALL)       NOPASSWD: ALL

ユーザーアカウント切り替え

以下のコマンドを実行してnewuserへ切り替えます。

# newuserへ切り替え
$ sudo su - newuser

公開鍵認証設定

公開鍵認証用ファイルの作成

以下のコマンドを実行して公開鍵認証用のディレクトリとファイルを作成します。

# newuserのホームディレクトリに.sshディレクトリを作成
$ mkdir .ssh

# ファイルパーミッションを700(所有者のみ、読み取り、書き込み、削除が可能)に変更
$ chmod 700 .ssh

# authorized_keysを作成
$ touch .ssh/authorized_keys

# ファイルパーミッションを600(所有者のみ、読み取りおよび書き込みが可能)に変更
$ chmod 600 .ssh/authorized_keys

キーペアのパブリックキーをコピー

以下のコマンドを実行してパブリックキーをコンピュータから取得します。

# パブリックキーをコンピュータから取得
$ ssh-keygen -y

# キーを持つファイルのパスを指定
Enter file in which the key is (/Users/****/.ssh/id_rsa): /path_to_key_pair/my-key-pair.pem

表示されたインスタンスのパブリックキー(※以下は例)の末尾の【キーペア名】を除いてコピーします。

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQClKsfkNkuSevGj3eYhCe53pcjqP3maAhDFcvBS7O6V
hz2ItxCih+PnDSUaw+WNQn/mZphTk/a/gU8jEzoOWbkM4yxyb/wB96xbiFveSFJuOp/d6RJhJOI0iBXr
lsLnBItntckiJ7FbtxJMXLvvwJryDUilBMTjYtwB+QhYXUMOzce5Pjz5/i8SeJtjnV3iAoG/cQk+0FzZ
qaeJAAHco+CY/5WrUBkrHmFJr6HcXkvJdWPkYQS3xqC0+FmUZofz221CBt5IMucxXPkX4rWi+z7wB3Rb
BQoQzd8v7yeb7OzlPnWOyN0qFU0XA246RA8QFYiCNYwI3f05p6KLxEXAMPLE

取得したキーペアのパブリックキーをペースト

以下のコマンドを実行して、コピーしたキーペアのパブリックキーを【authorized_keys】にペーストします。

#.ssh/authorized_keysを編集
$ vi .ssh/authorized_keys

追加したユーザーアカウントでAWSのEC2にSSH再接続

以下のコマンドを実行してAWSのEC2にSSH再接続します。

$ ssh -i ~/.ssh/秘密鍵名.pem 追加したユーザーアカウント@パブリックDNS

ログイン完了

以下が表示がされたらログイン完了です。

[centos@ip-パブリックDNS ~]$

ブログ記事の転載になります。

続きを読む

AWSのEC2で行うAmazon Linuxの初期設定

AWSのEC2で行うAmazon Linuxの最低限の初期設定をまとめてみました。まだすべき設定が残っているとは思いますので、こちらを更新していければと思っています。

開発環境

  • Mac OS X(El Capitan) 10.11.6
  • Amazon Linux AMI 2017.09.1 (HVM), SSD Volume Type – ami-33c25b55

事前に用意しておく必要があるもの

  • 接続先EC2のパブリックDNS
  • デフォルトユーザ(Amazon Linuxの場合デフォルトはec2-user)
  • EC2からダウンロードした秘密鍵(デフォルトは****.pem)

参考

AWSのEC2にSSH接続

秘密鍵の配置設定

以下のコマンドを実行してEC2からダウンロードした秘密鍵を【.ssh】ディレクトリに配置し、管理しやすくします。

$ mv /Users/ユーザ名/Downloads/秘密鍵名.pem ~/.ssh/

秘密鍵の権限設定

SSHを機能させるためには秘密鍵が公開されていないことが必要ですので、以下のコマンドを実行てし権限の設定をします。

$ chmod 400 ~/.ssh/秘密鍵名.pem

SSH接続

以下のコマンドを実行してAWSのEC2にSSH接続します。

$ ssh -i ~/.ssh/秘密鍵名.pem ユーザ名@パブリックDNS

ログイン完了

以下が表示がされたらログイン完了です。

Last login: Mon Jan 15 17:27:41 2018 from ***.***.***.***

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2017.09-release-notes/

Amazon-linuxの初期設定

パッケージの更新

以下のコマンドを実行してAmazon-linuxのパッケージを更新します。

# パッケージの更新
$ sudo yum update -y

パッケージの自動更新設定

以下のコマンドを実行してyum-cronをインストールし、パッケージの自動更新を設定します。

# インストール
$ sudo yum install yum-cron -y

# 有効化確認
$ sudo chkconfig --list yum-cron

# 有効化
$ sudo chkconfig yum-cron on

# 自動更新設定
$ sudo sed -i "s/^apply_updates.*$/apply_updates = yes/g" /etc/yum/yum-cron.conf

# 起動
$ sudo service yum-cron start

# 起動確認
$ sudo service yum-cron status

タイムゾーンの変更

以下のコマンドを実行してインスタンスのローカルタイムとハードウェアクロックを変更します。

# 現在の設定確認
$ date

# ローカルタイムを【Japan】に変更
$ sudo ln -sf /usr/share/zoneinfo/Japan /etc/localtime

# ハードウェアクロックを【Japan】に変更
$ sudo sed -i s/ZONE=\"UTC\"/ZONE=\"Japan\"/g /etc/sysconfig/clock

# システム再起動
$ sudo reboot

# 現在の設定確認
$ date

文字コードを日本語に変更

以下のコマンドを実行して文字コードを日本語対応に変更します。

# 文字コードを日本語に変更
$ sudo sed -i s/LANG=\"en_US.UTF-8\"/LANG=\"ja_JP.UTF-8\"/g /etc/sysconfig/i18n

不要なサービスの停止

以下のコマンドを実行してGUIで不要なサービスの停止することができます。

# 不要サービスの一括設定(GUI)
$ sudo ntsysv

ユーザーアカウント追加

ユーザーアカウント追加

以下のコマンドを実行して新規ユーザーアカウントを追加します。今回は【newuser】として作成します。

# newuserを追加
$ sudo adduser newuser

sudo権限の変更

以下のコマンドを実行してsudoersファイルを安全に編集します。

# sudoersファイルの編集
$ sudo visudo

作成したnewuserグループをsudoコマンドがパスワード無しで実行できるように追記します。

## Same thing without a password
# %wheel        ALL=(ALL)       NOPASSWD: ALL
+ %newuser       ALL=(ALL)       NOPASSWD: ALL

ユーザーアカウント切り替え

以下のコマンドを実行してnewuserへ切り替えます。

# newuserへ切り替え
$ sudo su - newuser

公開鍵認証設定

公開鍵認証用ファイルの作成

以下のコマンドを実行して公開鍵認証用のディレクトリとファイルを作成します。

# newuserのホームディレクトリに.sshディレクトリを作成
$ mkdir .ssh

# ファイルパーミッションを700(所有者のみ、読み取り、書き込み、削除が可能)に変更
$ chmod 700 .ssh

# authorized_keysを作成
$ touch .ssh/authorized_keys

# ファイルパーミッションを600(所有者のみ、読み取りおよび書き込みが可能)に変更
$ chmod 600 .ssh/authorized_keys

キーペアのパブリックキーをコピー

以下のコマンドを実行してパブリックキーをコンピュータから取得します。

# パブリックキーをコンピュータから取得
$ ssh-keygen -y

# キーを持つファイルのパスを指定
Enter file in which the key is (/Users/****/.ssh/id_rsa): /path_to_key_pair/my-key-pair.pem

表示されたインスタンスのパブリックキー(※以下は例)の末尾の【キーペア名】を除いてコピーします。

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQClKsfkNkuSevGj3eYhCe53pcjqP3maAhDFcvBS7O6V
hz2ItxCih+PnDSUaw+WNQn/mZphTk/a/gU8jEzoOWbkM4yxyb/wB96xbiFveSFJuOp/d6RJhJOI0iBXr
lsLnBItntckiJ7FbtxJMXLvvwJryDUilBMTjYtwB+QhYXUMOzce5Pjz5/i8SeJtjnV3iAoG/cQk+0FzZ
qaeJAAHco+CY/5WrUBkrHmFJr6HcXkvJdWPkYQS3xqC0+FmUZofz221CBt5IMucxXPkX4rWi+z7wB3Rb
BQoQzd8v7yeb7OzlPnWOyN0qFU0XA246RA8QFYiCNYwI3f05p6KLxEXAMPLE

取得したキーペアのパブリックキーをペースト

以下のコマンドを実行して、コピーしたキーペアのパブリックキーを【authorized_keys】にペーストします。

#.ssh/authorized_keysを編集
$ vi .ssh/authorized_keys

デフォルトユーザ(ec2-user)を削除する場合

追加したユーザーアカウントでAWSのEC2にSSH再接続

以下のコマンドを実行してAWSのEC2にSSH再接続します。

$ ssh -i ~/.ssh/秘密鍵名.pem 追加したユーザーアカウント@パブリックDNS

ログイン完了

以下が表示がされたらログイン完了です。

Last login: Mon Jan 15 17:27:41 2018 from ***.***.***.***

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2017.09-release-notes/

デフォルトユーザーの削除

以下のコマンドを実行してデフォルトユーザ(ec2-user)とそのホームディレクトリを削除します。

# ec2-userとそのホームディレクトリの削除
$ sudo userdel -r ec2-user

ブログ記事の転載になります。

続きを読む

Glueの使い方的な⑦(Step Functionsでジョブフロー)

Step FunctionsでGlueのジョブフローを作る

Glueの使い方的な③(CLIでジョブ作成)“(以後③と書きます)で書いたように、現在Glueのジョブスケジュール機能は簡易的なものなので、複雑なジョブフロー形成には別のスケジューラーが必要になる場合もあります。
例えばGlueのクローラーとGlueジョブもそれぞれにスケジュール機能があり統合したジョブフローを作ることがGlueだけでは出来ません(例えばクローラーを実行し終わったらジョブを実行するとか)。今回はサーバーレスなジョブフローのサービスであるStep Functionsを使って、クローラーを実行し正常終了したら後続のジョブを実行するというフローを作ってみます。

全体の流れ

  • Glue処理内容
  • StepFunctionsの処理内容
  • 前準備
  • Step FunctionsでStateMachine作成
  • 実行

処理内容

Glueの使い方的な①(GUIでジョブ実行)“(以後①と書きます)で実行したものと同じクローラーとジョブを使います。入力データも出力結果も①と同じです。
今回行うのはGlueクローラー処理が終わったら次のGlueジョブ処理開始というジョブフロー形成です。

あらためて①のクローラーとジョブの処理内容は以下の通りです

クローラーの内容

入力のCSVファイルからスキーマを作成します

ジョブの内容

“S3の指定した場所に配置したcsvデータを指定した場所にparquetとして出力する”

Step Functionsを使ったジョブフローの内容

図の四角をStep Functionsでは”State”と呼びます。処理の1単位と思ってください。

ジョブフローは以下のような形です。

Stateごとに流れを説明します

  • “Submit Crawler Job”でLambdaを使いGlueクローラーを実行
  • “Wait X Seconds”で指定時間待つ
  • “Get Crawler Job Status”でLambdaを使いGlueクローラーの状態をポーリングして確認
  • “Job Complete?”で状態を判定して結果によって3つに処理が分岐
    • 失敗なら”Job Failed”エラー処理
    • 終了なら”Run Final Glue Job”でLambdaを使い後続のGlueジョブを実行
    • 処理中なら”Add Count”でLambdaを使いカウンタをインクリメント。
      • “Add Count”の後”Chk Count”でカウンタをチェックし3回以上になっていたら”Job Failed Timeout”でタイムアウト処理、3未満なら”Wait X Seconds”に戻りループ処理

スクリーンショット 0030-01-13 21.47.05.png

前準備

①と同じです

今回使うサンプルログファイル(19件)

csvlog.csv
deviceid,uuid,appid,country,year,month,day,hour
iphone,11111,1,JP,2017,12,14,12
android,11112,1,FR,2017,12,14,14
iphone,11113,9,FR,2017,12,16,21
iphone,11114,007,AUS,2017,12,17,18
other,11115,005,JP,2017,12,29,15
iphone,11116,001,JP,2017,12,15,11
pc,11118,001,FR,2017,12,01,01
pc,11117,009,FR,2017,12,02,18
iphone,11119,007,AUS,2017,11,21,14
other,11110,005,JP,2017,11,29,15
iphone,11121,001,JP,2017,11,11,12
android,11122,001,FR,2017,11,30,20
iphone,11123,009,FR,2017,11,14,14
iphone,11124,007,AUS,2017,12,17,14
iphone,11125,005,JP,2017,11,29,15
iphone,11126,001,JP,2017,12,19,08
android,11127,001,FR,2017,12,19,14
iphone,11128,009,FR,2017,12,09,04
iphone,11129,007,AUS,2017,11,30,14

入力ファイルをS3に配置

$ aws s3 ls s3://test-glue00/se2/in0/
2018-01-02 15:13:27          0 
2018-01-02 15:13:44        691 cvlog.csv

ディレクトリ構成

in0に入力ファイル、out0に出力ファイル

$ aws s3 ls s3://test-glue00/se2/
                           PRE in0/
                           PRE out0/
                           PRE script/
                           PRE tmp/

ジョブのPySparkスクリプト

se2_job0.py
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job

## @params: [JOB_NAME]
args = getResolvedOptions(sys.argv, ['JOB_NAME'])

sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)
## @type: DataSource
## @args: [database = "se2", table_name = "se2_in0", transformation_ctx = "datasource0"]
## @return: datasource0
## @inputs: []
datasource0 = glueContext.create_dynamic_frame.from_catalog(database = "se2", table_name = "se2_in0", transformation_ctx = "datasource0")
## @type: ApplyMapping
## @args: [mapping = [("deviceid", "string", "deviceid", "string"), ("uuid", "long", "uuid", "long"), ("appid", "long", "appid", "long"), ("country", "string", "country", "string"), ("year", "long", "year", "long"), ("month", "long", "month", "long"), ("day", "long", "day", "long"), ("hour", "long", "hour", "long")], transformation_ctx = "applymapping1"]
## @return: applymapping1
## @inputs: [frame = datasource0]
applymapping1 = ApplyMapping.apply(frame = datasource0, mappings = [("deviceid", "string", "deviceid", "string"), ("uuid", "long", "uuid", "long"), ("appid", "long", "appid", "long"), ("country", "string", "country", "string"), ("year", "long", "year", "long"), ("month", "long", "month", "long"), ("day", "long", "day", "long"), ("hour", "long", "hour", "long")], transformation_ctx = "applymapping1")
## @type: ResolveChoice
## @args: [choice = "make_struct", transformation_ctx = "resolvechoice2"]
## @return: resolvechoice2
## @inputs: [frame = applymapping1]
resolvechoice2 = ResolveChoice.apply(frame = applymapping1, choice = "make_struct", transformation_ctx = "resolvechoice2")
## @type: DropNullFields
## @args: [transformation_ctx = "dropnullfields3"]
## @return: dropnullfields3
## @inputs: [frame = resolvechoice2]
dropnullfields3 = DropNullFields.apply(frame = resolvechoice2, transformation_ctx = "dropnullfields3")
## @type: DataSink
## @args: [connection_type = "s3", connection_options = {"path": "s3://test-glue00/se2/out0"}, format = "parquet", transformation_ctx = "datasink4"]
## @return: datasink4
## @inputs: [frame = dropnullfields3]
datasink4 = glueContext.write_dynamic_frame.from_options(frame = dropnullfields3, connection_type = "s3", connection_options = {"path": "s3://test-glue00/se2/out0"}, format = "parquet", transformation_ctx = "datasink4")
job.commit()

入力のCSVデータのスキーマ

クローラーによって作成されるスキーマ

スクリーンショット 0030-01-13 22.01.43.png

StepFunctionsでStateMachine作成

StepFunctionsは一連のジョブフローをJSONで定義しこれを”StateMachine”と呼びます。
StateMachine内の処理の1つ1つの四角をStateと呼びます。処理の1単位です。
このJSONの記述はASL(AmazonStatesLanguages)と呼ばれStateTypeとしてChoice(分岐処理)やWait(待ち)やParallel(並列実行)などがJSONだけで表現出来ます。またTaskというStateTypeからはLambdaやアクティビティ(EC2からStepFunctionsをポーリングする)を定義できます。前述の通り今回はLmabdaを使います。

マネージメントコンソールからいくつかあるテンプレートを元に作ることも出来ますが、カスタムでJSONを一から作ることもできます。

新規StateMachine作成画面
“Author from scrach”で一からJSON作成

スクリーンショット 0030-01-14 10.24.59.png

“Template”を選ぶとASLのStateパターンのいくつかのテンプレが選べます

スクリーンショット 0030-01-14 10.28.14.png

左側の”コード”部分にJSONを書き、右側の”ビジュアルワークフロー”の部分にJSONコードで書いたフローがビジュアライズされます

スクリーンショット 0030-01-14 10.29.50.png

StateMachine

今回のStateMachieのJSONは以下です。
内容は前述の通りです。
※[AWSID]のところは自身のAWSIDと置き換えてください

{
  "Comment": "A state machine that submits a Job to Glue Batch and monitors the Job until it completes.",
  "StartAt": "Submit Crawler Job",
  "States": {
    "Submit Crawler Job": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:[AWSID]:function:glue-test1-cr1",
      "ResultPath": "$.chkcount",
      "Next": "Wait X Seconds",
      "Retry": [
        {
          "ErrorEquals": ["States.ALL"],
          "IntervalSeconds": 120,
          "MaxAttempts": 3,
          "BackoffRate": 2.0
        }
      ]
    },
    "Wait X Seconds": {
      "Type": "Wait",
      "SecondsPath": "$.wait_time",
      "Next": "Get Crawler Job Status"
    },
    "Get Crawler Job Status": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:[AWSID]:function:glue-test1-crcheck",
      "Next": "Job Complete?",
      "InputPath": "$",
      "ResultPath": "$.response",
      "Retry": [
        {
          "ErrorEquals": ["States.ALL"],
          "IntervalSeconds": 1,
          "MaxAttempts": 3,
          "BackoffRate": 2.0
        }
      ]
    },
      "Job Complete?": {
      "Type": "Choice",
      "Choices": [{
          "Variable": "$.response",
          "StringEquals": "FAILED",
          "Next": "Job Failed"
        },
        {
          "Variable": "$.response",
          "StringEquals": "READY",
          "Next": "Run Final Glue Job"
        }
      ],
      "Default": "Add Count"
        },
    "Add Count": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:[AWSID]:function:glue-test1-addcount",
      "Next": "Chk Count",
      "InputPath": "$",
      "ResultPath": "$.chkcount",
      "Retry": [
        {
          "ErrorEquals": ["States.ALL"],
          "IntervalSeconds": 1,
          "MaxAttempts": 3,
          "BackoffRate": 2.0
        }
      ]
    },
      "Chk Count": {
      "Type": "Choice",
      "Choices": [{
          "Variable": "$.chkcount",
          "NumericGreaterThan": 3,
          "Next": "Job Failed Timeout"
        }],
      "Default": "Wait X Seconds"
    },
    "Job Failed": {
      "Type": "Fail",
      "Cause": "Glue Crawler Job Failed",
      "Error": "DescribeJob returned FAILED"
    },
        "Job Failed Timeout": {
      "Type": "Fail",
      "Cause": "Glue Crawler Job Failed",
      "Error": "DescribeJob returned FAILED Because of Timeout"
    },
    "Run Final Glue Job": {
      "Type": "Task",
      "Resource": "arn:aws:lambda:ap-northeast-1:[AWSID]:function:glue-test1-job1",
      "End": true,
      "Retry": [
        {
          "ErrorEquals": ["States.ALL"],
          "IntervalSeconds": 1,
          "MaxAttempts": 3,
          "BackoffRate": 2.0
        }
      ]
    }
  }
}

Lambda

今回使うLambdaは4つです。流れも振り返りながら見ていきます
書き方はいろいろあるし今回はエラーハンドリングも甘いのであくまでも動きのイメージをつかむための参考程度にしてください。最後のGlueジョブの実行についてはジョブの終了判定とかはしてないです。

“Submit Crawler Job”

GlueのAPIを使ってクローラーのStartを行う

glue-test1-cr1
# coding: UTF-8

import sys
import boto3
glue = boto3.client('glue')

def lambda_handler(event, context):
    client = boto3.client('glue')
    response = client.start_crawler(Name='se2_in0')
    return 1

“Wait X Seconds”

Waitで指定秒数待つ

“Get Crawler Job Status”

GlueのAPIを使ってクローラーのステータスを取得します

glue-test1-crcheck
# coding: UTF-8

import sys
import boto3
import json
glue = boto3.client('glue')

def lambda_handler(event, context):
    client = boto3.client('glue')
    response = client.get_crawler(Name='se2_in0')
    response = response['Crawler']['State']
    return response

“Job Complete?”

Choiceで取得したステータスが、”READY”なら正常終了、”FAILED”なら失敗、それ以外は実行中の分岐処理

“Job Failed”

ステータスが失敗なら
FailでStepFunctionsをエラーさせます

“Run Final Glue Job”

ステータスが正常終了なら
GlueのAPIを使ってジョブをStartします

glue-test1-job1
# coding: UTF-8

import sys
import boto3
import json
glue = boto3.client('glue')

def lambda_handler(event, context):
    client = boto3.client('glue')
    response = client.start_job_run(
    JobName='se2_job0')
    return response['JobRunId']

“Add Count”

クローラーがまだ実行中なら
カウンタにインクリメントします

glue-test1-addcount
# coding: UTF-8

import sys
import boto3
import json
glue = boto3.client('glue')

def lambda_handler(event, context):
    chkcount = event["chkcount"]
    chkcount = chkcount + 1

    return chkcount

“Chk Count”

choiceでカウンタが3未満か3以上かをチェックします

“Job Failed Timeout”

Failでカウンタが3以上だった時のエラー処理

“Wait X Seconds”

3未満の場合はここに戻りループ処理

実行

Step Functionsを実行

作成したStateMachineを選び”新しい実行”をクリック

スクリーンショット 0030-01-14 10.54.54.png

JSONに引数を入れて”実行の開始”をクリック
今回はJSON内で使う変数で”wait_time”を60秒で待ちの時間として入力しています

スクリーンショット 0030-01-14 10.55.52.png

実行状況

スクリーンショット 0030-01-14 10.59.44.png

CloudWatchイベントでスケジュール

あとは上記で作成したStateMachineをCloudWatchイベントでCRON指定すれば定期的実行されるジョブフローの完成です。This is Serverless!

スクリーンショット 0030-01-13 22.34.00.png

その他

今回はクローラー実行後にジョブ実行というシンプルなフローでしたが、Step Functionsは並列度を替えたり引数の受け渡しをしたり、さらにLambdaでロジックを書くことができるので自由度高く複雑なフローの作成が行えます。Glueとの相性はいいのではないでしょうか?

JSON部分も30分もあれば学習完了というカジュアルさがありLambdaを使ってAPI操作で様々なAWSの処理を繋げるのにはとてもいい印象です。

かなりシンプルな処理だったのですがコードがやや多い印象で、より複雑な処理になると結構大きいJSONになりそうで、JSONなのでコメント書けないとか少し大変な部分が出て来るのかもしれません。

バージョン管理を考えるとCliでの処理で運用したほうが良さそうですが、こういったサービスはGUIでの良さもあるのでどちらに比重を置いた運用がいいかは考慮が必要かもです

本文中で使ったカウンタのステート情報はDynamoDBなどに入れた方が良いかもです。

マイクロサービス化しやすいので、極力本来の処理のロジックをLambda側にやらせてそれ以外のフロー処理(分岐とかカウンタインクリメントとか)をJSONで書くのがいいと思います。今回カウンタはLambdaでやってしまいましたが。

ログはCloudWatchLogsに出ます

To Be Continue

TODO

参考

StepFunctions BlackBelt資料
https://www.slideshare.net/AmazonWebServicesJapan/20170726-black-beltstepfunctions-78267693

続きを読む

Glueの使い方的な③(CLIでジョブ作成)

CLIによる操作でGlueジョブを作る

Glueの使い方①(GUIでジョブ実行)“(以後①と書きます)で書いたように、現在GlueではGUIからジョブのコピーができないので、テスト時やデプロイ時などにもCLIでのジョブ操作が便利な場面があります

今回は①で実行したジョブをCLIで作成します

IAM role

ジョブ作成のコマンド発行するノードに付与するIAM roleもこの時に使ったtest-glueを使います。今回手元ではMacのPCでしたが本来だとジョブの作成や変更操作を行うスケジューラーなどに付与するIAM roleになると思います。
付与されるポリシーはこの2つ
・AmazonS3FullAccess
・AWSGlueServiceRole

全体の流れ

  • 前準備
  • CLIでジョブ作成
  • トリガー作成

前準備

①で使ったジョブで実行されているPySparkスクリプトを持ってきます

Glueのジョブで実行されるスクリプトはここにあります

AWSマネージメントコンソールからGlueをクリック、左側メニューのETLの”Jobs”をクリック
対象ジョブにチェックを入れ、”Action”をクリックし”Edit job”をクリック

スクリーンショット 0030-01-01 18.39.07.png

ジョブの内容が表示されます

Script pathに入力されているS3のパスが、このジョブで実行されるPySparkスクリプトの保存先です。
デフォルトだと以下の場所にスクリプトは保存されます。今回はデフォルトのままです。

s3://aws-glue-scripts-[AWSアカウントID]-[リージョン名]/[ユーザー名]/[ジョブ名]

スクリーンショット 0030-01-02 15.54.51.png

ローカルにもってくる

このファイルをローカルにダウンロードておきます。
ダウンロードしたPySparkスクリプトは前回GUIのみで操作して作られたスクリプトです。
処理内容は、”指定したS3にあるcsvファイルを指定したS3にparquetとして出力する”というものです

se2_job0.txt
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job

## @params: [JOB_NAME]
args = getResolvedOptions(sys.argv, ['JOB_NAME'])

sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)
## @type: DataSource
## @args: [database = "se2", table_name = "se2_in0", transformation_ctx = "datasource0"]
## @return: datasource0
## @inputs: []
datasource0 = glueContext.create_dynamic_frame.from_catalog(database = "se2", table_name = "se2_in0", transformation_ctx = "datasource0")
## @type: ApplyMapping
## @args: [mapping = [("deviceid", "string", "deviceid", "string"), ("uuid", "long", "uuid", "long"), ("appid", "long", "appid", "long"), ("country", "string", "country", "string"), ("year", "long", "year", "long"), ("month", "long", "month", "long"), ("day", "long", "day", "long"), ("hour", "long", "hour", "long")], transformation_ctx = "applymapping1"]
## @return: applymapping1
## @inputs: [frame = datasource0]
applymapping1 = ApplyMapping.apply(frame = datasource0, mappings = [("deviceid", "string", "deviceid", "string"), ("uuid", "long", "uuid", "long"), ("appid", "long", "appid", "long"), ("country", "string", "country", "string"), ("year", "long", "year", "long"), ("month", "long", "month", "long"), ("day", "long", "day", "long"), ("hour", "long", "hour", "long")], transformation_ctx = "applymapping1")
## @type: ResolveChoice
## @args: [choice = "make_struct", transformation_ctx = "resolvechoice2"]
## @return: resolvechoice2
## @inputs: [frame = applymapping1]
resolvechoice2 = ResolveChoice.apply(frame = applymapping1, choice = "make_struct", transformation_ctx = "resolvechoice2")
## @type: DropNullFields
## @args: [transformation_ctx = "dropnullfields3"]
## @return: dropnullfields3
## @inputs: [frame = resolvechoice2]
dropnullfields3 = DropNullFields.apply(frame = resolvechoice2, transformation_ctx = "dropnullfields3")
## @type: DataSink
## @args: [connection_type = "s3", connection_options = {"path": "s3://test-glue00/se2/out0"}, format = "parquet", transformation_ctx = "datasink4"]
## @return: datasink4
## @inputs: [frame = dropnullfields3]
datasink4 = glueContext.write_dynamic_frame.from_options(frame = dropnullfields3, connection_type = "s3", connection_options = {"path": "s3://test-glue00/se2/out0"}, format = "parquet", transformation_ctx = "datasink4")
job.commit()

CLIでジョブ作成

今回はAWS Cliを使います(他の各言語のSDKでも同じ操が作可能です)

まずawscliが古いとglueの操作ができないのでupgradeしておきましょう

pip install awscli --upgrade

Cliによるジョブ作成は、先程ダウンロードしたPySparkスクリプトファイルをリネームします。se2_job0.txtをse2_job2.pyにします。それをS3の任意の場所(今回はs3://test-glue00/se2/script/)にアップロードし、JSON形式でそこを指定してジョブ作成します

今回のジョブ作成に使ったJSON

test.json
{
    "Name": "se2_job2", 
    "Description": "test", 
    "Role": "test-glue", 
    "ExecutionProperty": {
        "MaxConcurrentRuns": 1
    }, 
    "Command": {
        "Name": "glueetl", 
        "ScriptLocation": "s3://test-glue00/se2/script/se2_job2.py"
    }, 
    "MaxRetries": 0, 
    "AllocatedCapacity": 5
}
  • Name
    ジョブ名

  • AllocatedCapacity
    “The number of AWS Glue data processing units (DPUs) to allocate to this Job.From 2 to 100 DPUs can be allocated; the default is 10.”
    とあるようにこのジョブに割り当てるDPUを指定します。2-100で指定。デフォは10
    https://docs.aws.amazon.com/glue/latest/webapi/API_CreateJob.html

  • ExecutionPropertyのMaxConcurrentRuns
    ジョブの最大同時実行数。デフォは1

  • ScriptLocation
    PySparkスクリプトファイルの保存場所

  • MaxRetries
    ジョブの最大リトライ数。デフォは0
    ※別途スケジューラーを使ってジョブを実行してるならリトライ制御はスケジューラー側に任せたほうがよいかも

  • CommandのName
    “glueetl”でなければなりません。固定のようです

スケルトン出力

他のCLIと同じく補助機能でJSONのスケルトンを作るコマンドもあります。

cli
$ aws glue create-job --generate-cli-skeleton 
{
    "Name": "", 
    "Description": "", 
    "LogUri": "", 
    "Role": "", 
    "ExecutionProperty": {
        "MaxConcurrentRuns": 0
    }, 
    "Command": {
        "Name": "", 
        "ScriptLocation": ""
    }, 
    "DefaultArguments": {
        "KeyName": ""
    }, 
    "Connections": {
        "Connections": [
            ""
        ]
    }, 
    "MaxRetries": 0, 
    "AllocatedCapacity": 0
}

実行結果

こんな感じで実行します
以下実行結果です

cli
$ aws glue create-job --cli-input-json file://test.json
{
    "Name": "se2_job2"
}

コマンドラインに他の引数があればJSONを上書きします。ファイルよりコマンドの引数の方が強いです。
ベースのJSONを作っておいて、DPUなどを状況に応じて大きくするとかいいと思います

引数では以下を指定できます

$ aws glue create-job help

CREATE-JOB()                                                      CREATE-JOB()



NAME
       create-job -

DESCRIPTION
       Creates a new job.

       See also: AWS API Documentation

SYNOPSIS
            create-job
          --name <value>
          [--description <value>]
          [--log-uri <value>]
          --role <value>
          [--execution-property <value>]
          --command <value>
          [--default-arguments <value>]
          [--connections <value>]
          [--max-retries <value>]
          [--allocated-capacity <value>]
          [--cli-input-json <value>]
          [--generate-cli-skeleton <value>]

GUIからもジョブが作成されていることがわかります。

スクリーンショット 0030-01-02 16.05.41.png

ジョブを実行

対象ジョブのse2_job2をチェックし、Actionの”Run job”をクリックします。
画面のように正常に完了しています。
スクリーンショット 0030-01-02 16.10.18.png

同じ結果がS3へ出力されています。
このようにコマンドであれば同じジョブを作りやすいので、DPUだけ変えるとか、テストのためにテストデータの入力や出力だけ変えて同じ処理を実行するなど、パラメータの一部だけ変更したジョブを作りやすいです。

ただ、画面からわかるように今回の出力のparquetファイル1つとその他メタデータが最新の日付で出力と更新がされていますが、コピー元ジョブで実行したparquetファイルが1つ残っています。

スクリーンショット 0030-01-02 16.12.04.png

コピー元ジョブの処理内容は

“S3にある1つのcsvファイルをparquetにしてS3に出力する”という処理でした。

Athenaでクエリ実行すると件数が倍の38件で、出力が重複していることがわかります。(元データは19件でした)

スクリーンショット 0030-01-02 16.17.43.png

ジョブフローを設計する時やスケジューラーの機能などで、処理したデータはムーブしたり消したりする場合もありますし、処理対象のディレクトリをタイムスタンプなどで判別して古いデータは処理対象から除外する場合もあると思います。どういったジョブフローにしているかに依存する部分かもですが、Glueにはこういった事象を防ぐブックマークという機能があります。ブックマーク機能を有効にすると既に処理したデータを処理の対象外とすることができます。
これはまたの機会で書けたらと思います。

トリガー作成

Glueには簡易的なスケジュール機能にTriggerがあります。
以下3つのスケジューリングができます。今回はCRON形式で作成してみます
選べるTrigger Typeは以下3つです
・CRON形式
・前のジョブが完了したら実行
・手動実行

スクリーンショット 0030-01-02 16.36.19.png

このトリガーの対象とするジョブを選びます。ジョブ名の横にある”Add”をクリックすることで選択できます。
“Next”をクリックし最後にサマリがでるので問題なければ”Finish”をクリックします。

スクリーンショット 0030-01-02 16.37.22.png

Triggerのコマンドライン操作もいくつか書いておきます。

先程作ったse2_trigger2トリガーの内容表示

get-trigger
$ aws glue get-trigger --name se2_trigger2
{
    "Trigger": {
        "Predicate": {}, 
        "Name": "se2_trigger2", 
        "Schedule": "cron(0 0 * * ? *)", 
        "Actions": [
            {
                "Arguments": {
                    "--job-bookmark-option": "job-bookmark-enable"
                }, 
                "JobName": "se2_job2"
            }
        ], 
        "State": "CREATED", 
        "Type": "SCHEDULED"
    }
}

トリガー作成用のjsonスケルトン表示

skelton
$ aws glue create-trigger --generate-cli-skeleton 
{
    "Name": "", 
    "Type": "SCHEDULED", 
    "Schedule": "", 
    "Predicate": {
        "Logical": "AND", 
        "Conditions": [
            {
                "LogicalOperator": "EQUALS", 
                "JobName": "", 
                "State": "STARTING"
            }
        ]
    }, 
    "Actions": [
        {
            "JobName": "", 
            "Arguments": {
                "KeyName": ""
            }
        }
    ], 
    "Description": ""
}

スケルトンを元に先程のトリガse2_trigger2と同じ内容で、名前だけse2_trigger3に変更したJSON作成

testtrigger2.json
$ cat testtrigger2.json 
{
    "Name": "se2_trigger3", 
    "Type": "SCHEDULED", 
    "Schedule": "cron(0 0 * * ? *)", 
    "Actions": [
        {
            "JobName": "se2_job2", 
            "Arguments": {
                "--job-bookmark-option": "job-bookmark-enable"
            }
        }
    ], 
    "Description": ""
}

se2_trigger3を作成

$ aws glue create-trigger --cli-input-json file://testtrigger2.json
{
    "Name": "se2_trigger3"
}

トリガー更新
以下でcronの時間を0時から2時に変更してます。

$ cat testtrigger2_upd.json 
{
    "Name": "se2_trigger3", 
    "TriggerUpdate": {
        "Name": "se2_trigger3", 
        "Description": "", 
        "Schedule": "cron(0 2 * * ? *)", 
        "Actions": [
            {
                "JobName": "se2_job2", 
                "Arguments": {
                    "--job-bookmark-option": "job-bookmark-enable"
                }
            }
        ] 
    }
}
$ aws glue update-trigger --cli-input-json file://testtrigger2_upd.json
{
    "Trigger": {
        "Predicate": {}, 
        "Name": "se2_trigger3", 
        "Schedule": "cron(0 2 * * ? *)", 
        "Actions": [
            {
                "Arguments": {
                    "--job-bookmark-option": "job-bookmark-enable"
                }, 
                "JobName": "se2_job2"
            }
        ], 
        "State": "CREATED", 
        "Type": "SCHEDULED"
    }
}

その他

CloudFormationも対応しているのでそちらでもCLI操作や自動化が可能かと思います。

ジョブのデプロイ時、より慎重にやるなら、出力先を一時的な別のS3や一時的なDBに変更したジョブを作り、既存と並行稼働させるのもいいかと思います。

ジョブフローの形成は、どのようなデプロイツールを使っているか、どのようなジョブスケジューラーを使っているかにもよるものです。

To Be Continue

Triggerの設定時やStartJobRunコマンドの”–job-bookmark-option”でブックマークを有効無効にできます。
ブックマーク機能について今後書いていければと思います。

Glueのトリガーで設定できるスケジュールは見ての通り現在は簡易的なものです。
AWS Step Functionsで少し複雑なジョブフロー作成について今後書いていければと思います。

参考

AWS CLI
http://docs.aws.amazon.com/cli/latest/reference/glue/create-job.html
http://docs.aws.amazon.com/glue/latest/webapi/API_CreateJob.html

PythonのAPI
http://boto3.readthedocs.io/en/latest/reference/services/glue.html#Glue.Client.create_job

続きを読む

Beanstalk worker tierの 単一コンテナの Dockerを Docker for Macで起動するメモ

Beanstalk worker tierの 単一コンテナの Dockerを Docker for Macで起動するメモ

  • Worker tierのサンプルアプリケーションを Macでいじりたかった

Docker for Macをインストール

  • Install Docker for Mac

    • Docker.dmgを実行して起動
  • brew install dockerや VirtualBoxインストールは不要

単一コンテナの Docker をダウンロード

ビルド

cd docker-singlecontainer-v1/

ls
# Dockerfile        Dockerrun.aws.json  application.py      cron.yaml

docker build .
# (略)

docker images
# REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
# (略)

docker ps
# CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
# (略)

docker run -i -t $(docker images -q | head -1)
# (略)
# IOError: [Errno 2] No such file or directory: '/tmp/sample-app/sample-app.log'

vi application.py 
vi Dockerrun.aws.json 
# "/sample-app" を削る

docker build .
# (略)

docker images
# REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
# (略)

docker ps
# CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
# (略)

docker run -i -t $(docker images -q | head -1)
Serving on port 8000...

ログイン

  • 別コンソールより
cd docker-singlecontainer-v1/

ls
# Dockerfile        Dockerrun.aws.json  application.py      cron.yaml

docker ps
# CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS               NAMES
# (略)

docker exec -it $(docker ps -q | head -1) bash
# ログイン

root@xxxxxxxxxxxx:/# curl -XGET http://localhost:8000/
# <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
# <html>
# (略)

root@xxxxxxxxxxxx:/# exit

続きを読む

AWS Lambdaを使ってEC2インスタンスを定時にcronで停止、起動させる。

イベントソースに「CloudWatchイベント」を指定することで、Lambda関数を一定時間ごとに実行させることができます。
イベントソースとは、Lambda関数を呼び出す引き金となるもののことです。
イベントソースにはS3やDynamoDB、API Gatewayなど色々なものを指定することができます。

EC2の利用料金を節約するために、使わない時間はインスタンスを停止しておきたいという人もいると思います。

使わない時間に停止して、使いたい時間に起動させるという使い方ができれば、EC2の利用料金を大きく削減できる可能性があります。
たとえば、開発用の環境は夜の20時には落として、朝の7時に上げるように設定するとかですね。

今回はLambda関数を使って、定時にEC2を停止、起動させてみます。

まずはLambdaで関数を作成します。スクラッチ(1から作成する)でOKです。
Lambda Management Console - Google Chrome 2018-01-03 08.58.24.png

イベントにCloudWatchイベントを設定します。
Lambda Management Console - Google Chrome 2018-01-03 08.59.10.png

CloudWatchイベントの詳細を設定していきます。
Lambda Management Console - Google Chrome 2018-01-03 08.46.20.png

ルールタイプは「スケジュール式」とします。

cron 式は、協定世界時 (UTC) です。

とあります。
つまり、このcron式で設定された時刻+9時間が日本時間となります。

ルールのスケジュール式については以下のページが参考になります。
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/ScheduledEvents.html

フィールド ワイルドカード
0-59 , – * /
時間 0-23 , – * /
1-31 , – * ? / L W
1-12 または JAN-DEC , – * /
曜日 1-7 または SUN-SAT , – * ? / L #
1970-2199 , – * /
cron(0 13 * * ? *)

と指定しているので、22時にLambda関数が起動する設定ですね。

では実際にLambda関数を作成していきます。

「StopEC2Instance」という自分が作成した関数名をクリックします。
Lambda Management Console - Google Chrome 2018-01-03 09.01.12.png

画面の下の方でコードを書く部分があるので、そこに以下のような関数を書きます。

import boto3

region = 'ap-xxxxxxxx-x'
instances = ['x-xxxxxxxx']
def lambda_handler(event, context):
    # TODO implement
    ec2 = boto3.client('ec2', region_name=region)
    ec2.stop_instances(InstanceIds=instances)

    return 'Stopped these instances: ' + str(instances)

instance = ['x-xxxxx']にはEC2のインスタンスIDを指定して下さい。

region = 'ap-xxxxxxxx-x'にはリージョンのIDを指定します。
https://docs.aws.amazon.com/ja_jp/general/latest/gr/rande.html
東京だったらap-northeast-1ですね。

リージョン名 サービス対象 エンドポイント プロトコル Amazon Route 53 ホストゾーン ID*
米国東部 (オハイオ) us-east-2 apigateway.us-east-2.amazonaws.com HTTPS ZOJJZC49E0EPZ
米国東部(バージニア北部) us-east-1 apigateway.us-east-1.amazonaws.com HTTPS Z1UJRXOUMOOFQ8
米国西部 (北カリフォルニア) us-west-1 apigateway.us-west-1.amazonaws.com HTTPS Z2MUQ32089INYE
米国西部 (オレゴン) us-west-2 apigateway.us-west-2.amazonaws.com HTTPS Z2OJLYMUO9EFXC
アジアパシフィック (ムンバイ) ap-south-1 apigateway.ap-south-1.amazonaws.com HTTPS Z3VO1THU9YC4UR
アジアパシフィック (ソウル) ap-northeast-2 apigateway.ap-northeast-2.amazonaws.com HTTPS Z20JF4UZKIW1U8
アジアパシフィック (シンガポール) ap-southeast-1 apigateway.ap-southeast-1.amazonaws.com HTTPS ZL327KTPIQFUL
アジアパシフィック (シドニー) ap-southeast-2 apigateway.ap-southeast-2.amazonaws.com HTTPS Z2RPCDW04V8134
アジアパシフィック (東京) ap-northeast-1 apigateway.ap-northeast-1.amazonaws.com HTTPS Z1YSHQZHG15GKL
カナダ (中部) ca-central-1 apigateway.ca-central-1.amazonaws.com HTTPS Z19DQILCV0OWEC
欧州 (フランクフルト) eu-central-1 apigateway.eu-central-1.amazonaws.com HTTPS Z1U9ULNL0V5AJ3
欧州 (アイルランド) eu-west-1 apigateway.eu-west-1.amazonaws.com HTTPS ZLY8HYME6SFDD
欧州 (ロンドン) eu-west-2 apigateway.eu-west-2.amazonaws.com HTTPS ZJ5UAJN8Y3Z2Q
南米 (サンパウロ) sa-east-1 apigateway.sa-east-1.amazonaws.com HTTPS ZCMLWB8V5SYIT

関数の作成が終わったら右上の「保存」を押します。

複数のインスタンスを指定したい場合はinstances = ['x-xxxxxxxx','x-xxxxxxxx','x-xxxxxxxx']のように配列にIDを入れます。

ここまで作ったら関数をテストします。

右上のテストをクリックして、「テストイベントの設定」を選択してください。
Lambda Management Console - Google Chrome 2018-01-03 11.14.00.png

イベントテンプレートは「Scheduled Event」とします。
Lambda Management Console - Google Chrome 2018-01-03 09.07.42.png

作成できたら、上部には以下のように表示されているはずです。
Lambda Management Console - Google Chrome 2018-01-03 10.44.32.png

「テスト」を実行してEC2インスタンスが停止することを確認します。

次に、起動用のLambda関数を作成します。
基本的には停止の関数を作ったときと同じことをやります。

スクリプトは以下の通りです。

import boto3

region = 'ap-xxxxxxxx-x'
instances = ['x-xxxxxx','x-xxxxx','x-xxxxxxx','x-xxxxxx']
def lambda_handler(event, context):
    # TODO implement
    ec2 = boto3.client('ec2', region_name=region)
    ec2.start_instances(InstanceIds=instances)

    return 'Started these instances: ' + str(instances)

この関数を実行すると、EC2インスタンスが起動することが確認できました。

続きを読む

LaradockをAWS Elastic Beanstalkで使用する

LaravelではLaradock(Docker)を使うことによって簡単に環境を構築できます。

環境間での差異を無くすため「Laradockを本番/開発環境でも使いたい!」という方のために、今回はLaradockをAWS Elastic Beanstalkで使用する方法をご紹介します。

概要

  • Elastic Beanstalkは複数コンテナ環境(Multi-Container Docker)を使用
  • Laradockから、NginxとPHP-FPMのイメージを利用

Laradockはv5.85を使用しますが、基本的にどのバージョンでも実現可能です。

実装方法

AWS ECR にImageを保存

まずは使用するImageをAWS ECRに保存します。

NginxとPFP-FPMをビルドしましょう。

docker-compose build nginx php-fpm

次にAWS ECSのコンソール画面からそれぞれのリポジトリを作成します。(nginx、php-fpm)
作成後、ビルドしたイメージにタグを付けプッシュします。

docker tag nginx:latest 123456789.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest
docker push 123456789.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest

(この辺の操作はコンソール画面にも説明があります)

Dockerrun.aws.jsonの作成

以下のように記述します。

Dockerrun.aws.json
{
    "AWSEBDockerrunVersion": 2,
    "volumes": [
        {
            "name": "application",
            "host": {
                "sourcePath": "/var/app/current/laravel"
            }
        },
        {
            "name": "php-ini",
            "host": {
                "sourcePath": "/var/app/current/php-fpm/php71.ini"
            }
        },
        {
            "name": "nginx-sites",
            "host": {
                "sourcePath": "/var/app/current/nginx/sites"
            }
        }
    ],
    "containerDefinitions": [
        {
            "name": "php-fpm",
            "image": "123456789.dkr.ecr.ap-northeast-1.amazonaws.com/php-fpm:latest",
            "essential": true,
            "memory": 128,
            "mountPoints": [
                {
                    "sourceVolume": "application",
                    "containerPath": "/var/www"
                },
                {
                    "sourceVolume": "php-ini",
                    "containerPath": "/usr/local/etc/php/php.ini"
                }
            ]
        },
        {
            "name": "nginx",
            "image": "123456789.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:latest",
            "essential": true,
            "memory": 128,
            "mountPoints": [
                {
                    "sourceVolume": "application",
                    "containerPath": "/var/www"
                },
                {
                    "sourceVolume": "nginx-sites",
                    "containerPath": "/etc/nginx/sites-available"
                }
            ],
            "links": [
                "php-fpm"
            ],
            "portMappings": [{
                "hostPort": 80,
                "containerPort": 80
            }]
        }
    ]
}

そして上記のDockerrun.aws.jsonのvolumesで指定されているファイルを作成していきます。
最終的な構成はGitHubで公開しています。

Laravelフォルダを設置

applicationボリューム(/var/app/current/laravel)を作成します。

Dockerrun.aws.jsonがあるディレクトリが/var/app/current/なので、その階層にlaravelフォルダを設置します。
(フォルダ名はlaravelとなっていますが、任意なので変更しても構いません)

そのlaravelフォルダにcomposer.jsonなどのアプリ本体がある感じです。

php-iniとnginx-sites を作成

Laradockのdocker-compose.ymlには、php-fpmのvolumeに以下が指定されています。
./php-fpm/php${PHP_VERSION}.ini:/usr/local/etc/php/php.ini

build時に環境変数のPHP_VERSIONが71である場合は、「./php-fpm/php71.ini」が必要です。
そのため「/var/app/current/php-fpm/php71.ini」というファイルを作成します。このファイルはLaradockからそのままコピーします。

nginxも同様で「/var/app/current/nginx/sites」フォルダを作成し、そこにdefault.confを設置します。
設定は以上です。

あとはElastic Beanstalkへアップロード

Dockerrun.aws.jsonやlaravelディレクトリを1つのzipファイルにまとめ、Elastic Beanstalkへアップロードすれば完了です。
デプロイ毎に圧縮するのは面倒なので、実際にはCircleCIなどを利用して自動デプロイを行うことをおすすめします

Queueや定期タスクが必要な場合

Elastic Beanstalkではcronをインストールできないため、Laradockのphp-workerなどは利用できません。
Queueや定期タスクが必要な場合は、laravel-aws-workerというパッケージを使用します。このパッケージをインストールしてWorker環境を別に作成することで、Queueや定期タスクを処理してくれます。

続きを読む

足元サーバがないのでlambdaでConfluenceに日報の投稿をさせてみる。

みなさまこんにちは。
AWS Lambda Advent Calendar 2017 の 12/25分での投稿となります。最後の日に一枠空いていたので、Lambda自体初めてですが、投稿ドリブンで枠をとって実際に手を動かしてみました。

はじめに

やりたいこと / きっかけ

現在の職場は基本的にサービス用のインフラはクラウド(主にAWS) を使っており、オフィス内もネットワーク機器以外にはサーバと呼べるものがNASくらいしかありません。
(職場のみなさんも、それで特に困っている様子は無いみたいなのですが…)

ただ、定時ダッシュするわたしには、ちょっとでも時間が惜しい。
何かしら日常業務をcronなどで自動でやらせて負担を軽減したいところですが、足元サーバが無いので、その辺をどうしたものかと悩んでおりました。

さて、そんな折。
チームの毎週の振り返りをConfluenceのブログ投稿を使ってまとめているのですが、割とマクロを駆使してフォーマットを決めているページなので、毎回新規でページを準備するのが大変になって来ました。
(20171225現時点で、Confluenceのブログ投稿には、テンプレートの機能が無いのです!)

ついつい凝ったマクロ(TipとかTOCとかパネルとか)を利用しているため、わたしがお休みの場合に代わりの方が投稿を起こすのが大変…というのが何回か発生。
『さすがにそろそろ自動化出来ない?』と言うメンバーの声が出てきたので、なんとかしようというタスクがやってきました。

まずはAPIを叩いて投稿テスト!

Confluenceについては過去いくつか記事を書いていますが、REST APIがあるので、そこを叩けば投稿はできそうです。

あとは、何かしらでスクリプトを組んだら、定期実行できるものがあればいい。
でも、AWSにあるサーバはデプロイや開発用で、個人のスクリプトを動かすわけにはいきません…。

足元サーバの代わりにLambda?

どうしたものかと思っていた時、DBのバックアップやインスタンスの上げ下げを自動化したいねーと言う話が出てきました。こちらも問題は、どこでスケジュール実行するかです。
そこで、「Lambda使ってできるんじゃないの?」と言うコメントをいただきました。それがつい最近です。

そして幸いに、このAWS Lambda Advent Calendar 2017で、たくさんのナレッジが紹介されているのを発見。
AWSのリソースだけでなく、Slackをはじめ外部のAPIを叩ける、pythonのようなスクリプト言語でも処理を書けると言うのがわかりましたので、「じゃあConfluenceも叩いてみればいいかも!」と思い立ちました。

個人でAWS Lambda使うにはお金も敷居も高すぎる。でも、今回は会社のリソースで、それなりの名分もあり、しかもConfluenceもクラウド版を使っているので好都合です。

Pythonのスクリプトを調整

confluenceBlogPostSample という関数を定義していきます。
この辺りはあまり考えずに設定できました。

sample-screenshot.png

実際の処理のスクリプトの作成は以下の通りです。
ConfluenceのREST API用のPythonのパッケージは、現在リポジトリがGitHub上に移動しています。

こちらを使ってもいいのですが、Lambdaでスクリプトで関数を定義する場合は「お作法」があるため、このパッケージはオーバースペックです。
単純にPOSTができればいいので、requestsを使ったリックソフト様のREST APIでConfleunceのページを生成 という記事を参考にさせていただきました。

まずは面倒だった自分の日報での投稿を試しに、以下の様なスクリプトを組みました。
その際の前提は以下の通りです。

  • ブログ投稿は個人スペースで試す
  • 毎日同じタイトルでは投稿できないので、連続投稿でテストしても大丈夫な様に、タイトルを動的に変更 (YYYYmmddHHMMをそえる)
  • Confluenceマクロを多用しているので、まずはTOC(見出し)マクロがちゃんと埋め込めるか確認

また、Lambdaとして利用するためには、以下の様なお作法があります。

  • 単体のスクリプトなら画面に貼り付け出来る
  • APIにアクセスするためのrequestsはLambdaのデフォルトでは使えない
  • Lambda内で動的にpip installができないので、必要なモジュールがある場合は、ソースコードでzipにまとめてアップロードすること
  • 実行のメインになる関数は、lambda_function.py というファイル名で、lambda_handler()という関数

この辺り、今回のアドベントカレンダーの記事が本当に参考になりました! みなさまありがとうございます!

lambda_function.py の中身

この様になっています。
実際は、requestsのソースコードが必要なので、zipファイルにまとめてアップロードしています。

# coding: utf-8
import os, json, textwrap
from datetime import datetime
import requests

# ページの作成
# http://CONFLUENCEのサーバーURL/rest/api/content にアクセスして
# ページを作成します
# (HTTP メソッドは post )
def create_page():
  BASE_URL="CONFLUENCEのサーバーURL"
  AUTH=("akiko.xxxx@example.com", "パスワード")
  HEADERS = {"content-type":"application/json"}
  json_data = create_json_data()
  response = requests.post(
        BASE_URL + "/rest/api/content",
        auth=AUTH,
        data = json.dumps(json_data),
        headers=HEADERS)
  response.raise_for_status()
  return response

# json データの生成
def create_json_data():
  now = datetime.now()
  date_str = now.strftime("%Y%m%d%H:%M:%S")

  # テストなので個人スペース
  space_key="~akiko.xxxx"
  page_title='{} テストの投稿'.format(date_str)

  # Lambdaの画面で環境変数を設定できるので、そのテスト(Lambdaからのポストかどうかのチェック用)
  posted_by = os.environ["POSTED_BY"]

    # 投稿内容はHTML形式で記載可能
  # Confluenceのマクロも、<ac:structured-macro/>で指定します
  # この例では、Tipマクロの中にTOCマクロをセットしています
  #
  content = textwrap.dedent('''
<ac:structured-macro ac:name="tip">
  <ac:rich-text-body>

  <ac:structured-macro ac:name="toc">
    <ac:parameter ac:name="printable">true</ac:parameter>
    <ac:parameter ac:name="style">square</ac:parameter>
    <ac:parameter ac:name="maxLevel">2</ac:parameter>
    <ac:parameter ac:name="indent">5px</ac:parameter>
    <ac:parameter ac:name="minLevel">2</ac:parameter>
    <ac:parameter ac:name="class">bigpink</ac:parameter>
    <ac:parameter ac:name="exclude">[1//2]</ac:parameter>
    <ac:parameter ac:name="type">list</ac:parameter>
    <ac:parameter ac:name="outline">true</ac:parameter>
    <ac:parameter ac:name="include">.*</ac:parameter>
  </ac:structured-macro>     

  </ac:rich-text-body>
</ac:structured-macro>
<h2>テストの投稿になります: 見出し1つめ</h2>
<h2>テストの投稿になります: 見出し2つめ</h2>
<h2>テストの投稿になります: 見出し3つめ</h2>
<br/>
<p>generated at: {date_str} {posted_by}</p>

''').format(date_str=date_str, posted_by=posted_by).strip()   

  payload = {
              "type":"blogpost",
              "title":page_title,
              "space":{"key":space_key},
              "body":{
                "storage":{
                  "value" : content,
                  "representation":"storage"
                }
              }
            }
  return payload

# main function
def lambda_handler(event, context):
  result = create_page()

    # APIの結果を出力
  print(result.json())

とりあえずやってみた!

定期実行はCloudWatchイベントとして設定できる様ですが、まずは手動実行を試します。
画面右上の「テスト」をクリックすると実行されます。

ただし、案の定最初は勝手がよくわからなかったので、何度か失敗….。
CloudWatch側にも、「ログストリーム」として記録されるんですね^^;
(後でチームの方にびっくりされない様にお話しせねば)

script-error.png

Pythonのスクリプトの構文エラーやモジュールのエラーなどで何回かアップし直して、やっと成功しました。

成功しました!

何度目かのエラーのあと、晴れて成功!!
POST後のレスポンスが画面に出力されています。

after-test.png

CloudWatch側のログにも、実行の前後とPOST後のレスポンスが出力されています。

cloudwatch-log.png

Confluence側はどうなった?

さて、問題の投稿はどうなったかというと…。
だいぶ荒削りではありますが、 ac:structured-macro のタグがうまく埋め込まれて、想定通りにブログ投稿が描画されていました!

posted-via-lambda.png

同じ日に数回投稿しても、名前がかぶらないのでなんとか別々の投稿として扱われています。
これでやりたいことのめどが立ちそうです!

これから先のこと

さて、なんとかLambdaから投稿ができましたが、まだ文章は荒削りなので、もっと見栄えを調整していかないといけません。
それから、Lambda側の定期実行の処理や、POST完了後にSlackに通知すると行った処理も必要になります。

テストに使っているアカウントとパスワードもソースに書いてしまっています。
Lambda側の環境変数に設定するかKMSを使って暗号化する、投稿用アカウントも権限を最小限に絞ったAPIアクセス専用のアカウント(&アクセスキー)に変更するといった処理が必要になります。
この辺り、調整できたら追記したいと思います。

また、内容的にはLambdaよりもConfluence寄りになっていますので、Confluenceを利用されている方の参考になりましたら幸いです。

続きを読む

OpsWorksのTime-based instancesをLambdaで無理やり日付ベースで稼働設定する

前置き

こんにちは。mediba advent calendar 2017 21日目担当のmoriです。

最近はめっきりコードを書くことが減り、コーディング技術が錆びついている状態で、さて何をやろうかと考えてはみたものの良い題材が思い浮かびませんでした。

なので、1年前にこの記事でやろうとしていた、
「OpsWorksのTime-based instancesを日付ベースで設定できるようにする」
という試みを、今の自分の知識でやってみようかと思います。

ものすごい成長したというわけではないですが、さすがに1年前の自分には勝てるでしょう。(というか記事を読み返すと、正直すごくイケてない)

OpsWorksでこんな事をやって建設的なのかどうかは余所へ置いておいて、
実際問題としてTime-based設定をするのに、マウスをクリックし続けるのは御免です。

やりたいこと

本記事のタイトルの通り、OpsworksのTime-basedを日付ベースで稼働設定できるようにすることです。要点は以下。

  • 特定のインスタンスを、毎月イベントがある3、13、23日だけ稼働させる。

    • 厳密にはイベント前日23時から、イベント翌日11時まで動かす。
    • それ以外は停止。

身も蓋もなく言えば、cronでその日にインスタンスをSTARTさせるシェルでも作れば良いかもしれませんが、肝心のその日にシェルが動かなかったりといった失敗した時も考えて、敢えてTime-basedに拘ってみます。

今回のポイント

  • Time-basedは曜日ベースでしか稼働設定できない。
  • Lambdaでやる。(時代はサーバーレス)
  • Pythonが自分的にアツイ気がするので採用。(今回初めて書く)
  • S3に設定ファイルを置く。

これに加え、1年前は時間と知識と技術が足りてなくて手が回ってなかったところも極力カバーできるように、ざっくりLambda関数を作って見ました。

実践

■Lambdaで使用するロールに、必要なポリシーを追加

        {
            "Effect": "Allow",
            "Action": [
                "lambda:InvokeFunction"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::xxx/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "opsworks:*"
            ],
            "Resource": "*"
        }
  • s3のResourceは、設定ファイルを配置するバケットを指定。(バケットの作成はここでは省略)
  • opsworksの権限は緩めに設定しているが、本来は対象のスタックに限定するべき。

■s3バケットに設定ファイルをアップロード

今回設定ファイルとして使うのは以下の3つ。
これらを任意のバケットにアップロードします。

1.インスタンスを動かす対象日の設定

event_calendar.json
[3, 13, 23]

2.対象のインスタンスIDリスト

  • ここで指定するのは「EC2 instance ID」ではなく、「OpsWroks ID」なので注意
  • 一応環境の違いに対応するため、ファイル名の頭に「dev」をつけている。
dev_instance_list.json
[
 "abcdefgh-1234-43aa-9876-22222ce7733c",
 "stuvwxyz-5678-43bb-1234-33333f165c08"
]

3.Time-basedに設定する稼働スケジュール

  • 「イベント前日23時から、イベント翌日11時まで動かす。」

    • これを実現するために、イベントとその前後3日分のスケジュールを記載。
    • 「Before」がイベント前日、「Target」がイベント当日、「After」がイベント翌日
schedule.json
{
 "Before": {
  "0": "off",
  "1": "off",
  "2": "off",
  "3": "off",
  "4": "off",
  "5": "off",
  "6": "off",
  "7": "off",
  "8": "off",
  "9": "off",
  "10": "off",
  "11": "off",
  "12": "off",
  "13": "off",
  "14": "on",
  "15": "on",
  "16": "on",
  "17": "on",
  "18": "on",
  "19": "on",
  "20": "on",
  "21": "on",
  "22": "on",
  "23": "on"
 },
 "Target": {
  "0": "on",
  "1": "on",
  "2": "on",
  "3": "on",
  "4": "on",
  "5": "on",
  "6": "on",
  "7": "on",
  "8": "on",
  "9": "on",
  "10": "on",
  "11": "on",
  "12": "on",
  "13": "on",
  "14": "on",
  "15": "on",
  "16": "on",
  "17": "on",
  "18": "on",
  "19": "on",
  "20": "on",
  "21": "on",
  "22": "on",
  "23": "on"
 },
 "After": {
  "0": "on",
  "1": "on",
  "2": "on",
  "3": "off",
  "4": "off",
  "5": "off",
  "6": "off",
  "7": "off",
  "8": "off",
  "9": "off",
  "10": "off",
  "11": "off",
  "12": "off",
  "13": "off",
  "14": "off",
  "15": "off",
  "16": "off",
  "17": "off",
  "18": "off",
  "19": "off",
  "20": "off",
  "21": "off",
  "22": "off",
  "23": "off"
 }
}

■Lambda関数を作成

1.Labmda関数を新規作成

  • ランタイムは今回は「Python 3.6」を指定。
  • ロールは最初にポリシーを設定したものを使用。

スクリーンショット 2017-12-22 10.55.53.png

2.環境変数を定義。

今回、環境変数として設定するのは以下の4つです。

  • S3_BUCKET ・・・ 設定jsonファイルをアップロードしているバケット名
  • EVENT_SCHEDULE_JSON ・・・ 上記作った「Time-basedに設定する稼働スケジュール」のファイル名
  • ENV ・・・ 実行される環境。今回は上記の「対象のインスタンスIDリスト」作成の際で触れたように「dev」環境とする。
  • EVENT_CALENDAR ・・・ 対象となるイベント日リストのjsonファイル

スクリーンショット 2017-12-22 13.56.15.png

3.コード本体

初Pythonなので、ツッコミどころ満載だとは思いますが、少なくとも動くはずです。

time-based-ctl.py
import os
import boto3
import json
import datetime

def lambda_handler(event, context):
    # Time-basedは曜日指定で設定なので、それ用の配列を用意
    weekday_list = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

    # s3とopsworksのclient
    s3 = boto3.resource('s3')
    s3_client = s3.meta.client
    ops_works_client = boto3.client('opsworks', region_name='us-east-1')

    # s3のbucket名を環境変数から取得
    bucket_name = os.environ['S3_BUCKET']

    # 設定jsonファイルのオブジェクトキーを環境変数から取得    
    obj_key_event = os.environ['EVENT_SCHEDULE_JSON']
    obj_key_event_calendar = os.environ['EVENT_CALENDAR']
    obj_key_instance_list = os.environ['ENV'] + "_instance_list.json"

    # 設定json取り込み(tryでやってみる)
    try:
        # 稼働スケジュールの設定を取得
        response_event = s3_client.get_object(Bucket = bucket_name, Key = obj_key_event)
        schedule_json_event = json.loads(response_event['Body'].read())

        # 対象のインスタンスを取得
        response_instance_list = s3_client.get_object(Bucket = bucket_name, Key = obj_key_instance_list)
        instance_list = json.loads(response_instance_list['Body'].read())

        # 対象日リストを取得        
        event_calendar = s3_client.get_object(Bucket = bucket_name, Key = obj_key_event_calendar)
        event_calendar_list = json.loads(event_calendar['Body'].read())

    except Exception as e:
        print(e)


    # 今日の日付を取得。UTCなので、日本時間にする。(他に方法あるかも)
    today = datetime.datetime.now() + datetime.timedelta(hours = 9)
    # この変数の意味は後述
    skip = False

    # ここからは個別に解説
    # [A]forループ
    for target_idx in range(2, 6): 
        # 対象日の曜日を特定しセット
        target_day = today + datetime.timedelta(days = target_idx)
        tareget_weekday = weekday_list[target_day.weekday()]

        # [B] 1つ前のループで対象日の稼働スケジュール設定をしていたらスキップ
        if skip and target_day.day not in event_calendar_list:
            skip = False
            continue
        elif target_day.day in event_calendar_list:
            # 対象日の前日と翌日を特定
            before_day = target_day - datetime.timedelta(days = 1)
            after_day = target_day + datetime.timedelta(days = 1)

            if skip:
                scuedule_setting = {
                    weekday_list[target_day.weekday()]: schedule_json_event['Target'],
                    weekday_list[after_day.weekday()]: schedule_json_event['After']
                }
            else:
                scuedule_setting = {
                    weekday_list[before_day.weekday()]: schedule_json_event['Before'],
                    weekday_list[target_day.weekday()]: schedule_json_event['Target'],
                    weekday_list[after_day.weekday()]: schedule_json_event['After']
                }

            # インスタンスの分だけ、Time-baseのセットを実施
            for target_instance in instance_list:
                response = ops_works_client.set_time_based_auto_scaling(
                                                         InstanceId = target_instance,
                                                         AutoScalingSchedule = scuedule_setting,
                                                         )
            # 対象日前後の稼働スケジュールをいじっているので、次ループはスキップさせる。
            skip = True
        else:
            # 対象日以外は停止させて置く必要があるので、そのように設定。
            for target_instance in instance_list:
                response = ops_works_client.set_time_based_auto_scaling(
                                                                         InstanceId = target_instance,
                                                                         AutoScalingSchedule = { weekday_list[target_day.weekday()]: {}}
            skip = False

[A]の補足

    for target_idx in range(2, 6): 
        # 対象日の曜日を特定しセット
        target_day = today + datetime.timedelta(days = target_idx)
        tareget_weekday = weekday_list[target_day.weekday()]

rangeを「2,6」で回している理由
Time-basedの設定ベースが曜日ベースである都合上、1週間以上先は設定できないこと、今回は対象日だけではなくその前後日の稼働スケジュールも操作することを踏まえ、2日後〜6日後としています。
何かの拍子で、この処理が1日や2日動かなかったとしても、リカバリはできるようになっています。
※7日後は実行当日に当たる曜日の稼働スケジュールをいじることになる為、含めない。

[B]の補足

        # [B] 1つ前のループで対象日の稼働スケジュール設定をしていたらスキップ
        if skip and target_day.day not in event_calendar_list:
            skip = False
            continue
        elif target_day.day in event_calendar_list:
            # 対象日の前日と翌日を特定
            before_day = target_day - datetime.timedelta(days = 1)
            after_day = target_day + datetime.timedelta(days = 1)

            # 前ループが対象日だった場合、稼働スケジュール設定の対象から外す
            if skip:
                scuedule_setting = {
                    weekday_list[target_day.weekday()]: schedule_json_event['Target'],
                    weekday_list[after_day.weekday()]: schedule_json_event['After']
                }
            else:
                scuedule_setting = {
                    weekday_list[before_day.weekday()]: schedule_json_event['Before'],
                    weekday_list[target_day.weekday()]: schedule_json_event['Target'],
                    weekday_list[after_day.weekday()]: schedule_json_event['After']
                }

スキップの意味
対象日の設定が2日連続だった場合の対応です。スキップしないと、稼働スケジュールがイベントの日ように設定したものが、完全停止状態に上書きされてしまいます。

elifでもスキップ判定をみているのも同様の理由です。(この場合は、イベント当日設定の24時間稼働が、イベント前日の設定に上書きされてしまう)

■lambda関数を動かす

あとはトリガーを[CloudWatch Events]で、cron設定にて1日1回動くように設定するだけです。

まとめ

とまぁ、駆け足で実装をしてみました。
これでOpsworksのTime-basedで、イベント日にほぼ確実に自動(?)スケールアウトができるようになります。

Python自体は初心者どころか、ほぼ初見のド素人な訳ですが、印象的には書きやすかったので、もう少し綺麗なコードが書けるように勉強してみようかと思います。

次はニッチな用途ではなく、普通に役立つ何かを紹介したいです・・・。

続きを読む