AWS GlueでS3から差分データを取得する

日次処理などの定期処理でS3からデータを取得しparquet形式に変換やDBにLoadするといったケースについては、ETLする際によくあるのではないでしょうか?
上記のようなケースでは、ETL処理の際にS3から前日からの差分を取得する必要があります。Glueにおいて自動生成されるコードでは、対象のデータソース全てを読み込んでしまうため読み込み後にフィルタする必要があります。すべて読み込んでフィルタするのはデータ量が多いとあまり効率はよくないですよね。
今回はこうしたケースにおいてどのように取得できるか考えてみたいと思います。

差分データを取得する方法

DynamicFrameを利用して取得する方法としては以下3つがあるかと思います
①from_catalogue関数でPushdown Predicatesオプションを利用
Pushdown Predicatesを利用すると読み込み時に条件を指定してフィルタすることが可能です。
ただし、データソースがパーティショニングされている必要があるため、データソースによっては簡単に使えない場合があります。設定方法については以下で紹介されていますので参照してください。
AWS Glue の Pushdown Predicates を用いてすべてのファイルを読み込むことなく、パーティションをプレフィルタリングする | Developers.IO

②JobBookmarkを利用
JobBookmarkを利用すると前回読み込んだところまで記録されているため、差分のみ取得することが可能となります。
バックエンドで色々とやってくれるので便利でいいのですが、どのような仕組みで実現されているのか見えないため、処理に問題があった際にリカバリが難しくなることが考えられます。また、ETL処理の前に毎回クローラーを実行する必要があります。
AWS GlueのJob Bookmarkの使い方 - cloudfishのブログ

③from_options関数を利用
from_options関数を利用することでS3のパスを直接指定することが可能です。この方法の場合、データソースがパーティショニングされている必要はなくパスを指定することで読み込みが可能です。

今回は③の方法について試してみたいと思います。

準備

まずは、適当なCSVファイルをS3に配置しておいてください。

ジョブの作成

ジョブを作成し以下のコードを入力して実行してください。

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)

#from_options関数で読み込み
datasource0 = glueContext.create_dynamic_frame.from_options( connection_type = "s3",connection_options = {"paths": [ "s3://glue-testdata-xxxxx/input/"]},format="csv", format_options={ "withHeader": "true","skipFirst": "true"})

datasink2 = glueContext.write_dynamic_frame.from_options(frame = datasource0, connection_type = "s3", connection_options = {"path": "s3://glue-testdata-xxxxx/output"}, format = "csv", transformation_ctx = "datasink2")
job.commit()

ポイントはfrom_options関数を使って読み込むところになります。connection_optionsにS3のパスを指定します。ここはファイル指定や複数入力可能です。
また、format_optionsで1行目をヘッダーとして読み込むことや読み飛ばすこともオプションで可能となっていますが、現時点ではバグのため指定がきかないようなので、読み飛ばしやカラム名の指定については独自で実装してやる必要があります。
また、型変換、カラムの削除なども自分で実装する必要があります。

まとめ

定期実行してデータを取り込むような処理はよくあると思いますのでそういった場合に使えそうですね。
ただし、まだバグがあるようなので利用する際はしっかりと検証したほうがよさそうです。

obnizで停電の復旧確認をしてみた

みなさん台風21号の影響はどうでしたでしょうか?今回は私の自宅も3日ほど停電になり結構困りました。
未だに停電から復旧していない地域もあるので早く復旧してほしいですね。
停電してはじめて分かるのですが、電気が生活の基礎を支えているということに改めて気付かされます。断水していなくてもトイレの水も流せないですし、お風呂も沸かせないといった状況になり色々なことができませんでした。
当時、自宅で仕事をしていたのですが、ネット接続も切れてしまったため仕事もできなくなりました。
幸いにも親類の家は停電していなかったため、台風通過後に避難することができたためずっと不便な生活を強いられることはなかったのですが、避難できる先が無いような方は大変だったと思います。

以前も雷による停電を経験していたこともあるのですが、避難するといつ停電から復旧したかが分からないのが非常に困ります。
そのため、今回はどうにかして外部から停電の復旧を確認したいと考えていました。
自宅にサーバを立てて外部から入れるようにしていた時期もありましたが、今はそんなこともしていないのでサーバで復旧確認もできませんでした。

そこで思いついたのが、最近手に入れたobnizを復旧確認に利用する方法でした。
obnizで遊んでいる時にたまたま気がついたのですが、obnizを外部マシンのNodejsから実行するとobnizがオンラインになるまでタイムアウトせず実行し続けており、オンラインになった時点でプログラムが実行される挙動となっていたため、これを利用することで停電の復旧確認ができるのではないかと思い自宅で実証実験をしてみました。

前提としては自宅のwifiに接続されていたことと、復旧時に電源が供給されるようにしておくことです。
プログラムは実行できれば何でもいいと思います。オンラインになったらslackに通知するといったようなことも考えましたが、そもそもテストもできないため文字列を表示するだけの簡易なプログラムとしました。

const sleep = require('system-sleep');
var Obniz = require("obniz");

var obniz = new Obniz("XXXX-XXXX");
obniz.onconnect = async function () {

    while(true){
       console.log('---------------------');
       console.log('Connected!');
       sleep(5000);
    }
}

これを動かし続けておくことで、obnizがオンラインになったタイミングで実行されるはずなので電気の復旧が分かると考えました。
実際に仕事中は作業用MacのDockerコンテナから上記プログラムを動かし続けていました。

また、以下のOnlineEditorからも実行できるので、仕事中以外は時々スマホからサンプルプログラムを実行するということを試していました。
Online Editor - obniz

本当に動くか不安でしたが、停電から3日後の9/6 10時40分頃にコンソールにログが表示されていたため、おそらく電気が復旧したということが分かりました。

まとめ

今回は台風による停電のためブレーカーを落とすことはしませんでしたが、地震などの場合には復旧時に火事になるケースがあるためブレーカーを落として避難することが推奨されていますので、充分注意してください。
また、途中で気がついたのですが、自宅にAmazon Echoがある方は、スマホのアプリからオンラインかどうかが分かるため、わざわざobnizを使って確認しなくても復旧状況がわかると思います。実際にobnizで復旧確認できた際には自宅のAmazon Echoもオンラインとなっていたため本当に電気が復旧したと確信が持てました。
頻繁に停電が発生するわけではないのでそんなにニーズはないと思いますが、こういった実際の状況でIoT機器を役立てることができたのはすごく面白かったです。

obnizをAWS Fargateから動かしてみた

obnizというIoTのコントロールボードが面白そうだったので早速購入してみました。
obnizの特徴としては、wifiと小型のディスプレイが内蔵されており、プログラムもWeb上からjavascriptで可能というかなりお手軽なガジェットです。
サンプルプログラムを動かすだけなら、箱から出してwifiに接続し、QRコードスマホから読み込み開いたweb上からプログラムを実行することが可能です。
Lチカを試そうとしたのですが、今回は技術の無駄遣いをしてみようと思い、あえてAWSのFargateからobnizを操作してみました。
ちなみにnodejsで実行可能なのでLambdaでも実行できます。
f:id:cloudfish:20180901004439p:plain

準備

obniz x1
Mac(docker環境インストール済み)

手順概要

  • Dockerイメージ作成
  • obnizアプリ作成
  • Fargate設定
  • 実行確認

Dockerイメージの作成

今回はコンテナ上でobnizのアプリを作成してそれをベースイメージとします。
まずはnode環境の準備ととcanvasモジュールが使いたかったのでubuntuベースのコンテナを利用しました。

FROM ubuntu
RUN  apt-get -y update
RUN  apt-get install -y nodejs npm vim git
RUN  apt-get install -y libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev build-essential g++

イメージのビルド

docker build -t obniz_container .

obnizアプリの作成

コンテナにログイン

作成したコンテナにログインする

docker run -it obniz_container /bin/bash

アプリの作成

以下のコマンドを実行してnodeモジュールのインストールとプロジェクトを作成します

mkdir /opt/obniz_test
cd /opt/obniz_test
npm init
npm install system-sleep
git clone https://github.com/Automattic/node-canvas.git
npm install node-canvas

/opt/obniz_testにindex.jsを作成し以下を入力します。OBNIZ-IDは8桁のハード固有の番号を入力してください。
ディスプレイに「Hello Obniz!」という文字列を左から右に延々流し続けるだけのプログラムです。

const sleep = require('system-sleep');
const { createCanvas } = require('canvas');
var Obniz = require("obniz");

var obniz = new Obniz("OBNIZ-ID");
obniz.onconnect = async function () {
    for(var i=0;;i=i+8){
       var canvas = createCanvas(obniz.display.width, obniz.display.height);
       var ctx = canvas.getContext('2d');
       ctx.fillStyle = "white";
       ctx.font = "12px Serif";
       ctx.fillText('Hello Obniz!', i, 40);
       obniz.display.draw(ctx);
       if(i >= obniz.display.width){
            i = 0;
       }
       sleep(500);
    }
}

実行確認

以下コマンドを実行し画面に文字が表示されることを確認します。

node index.js

コンテナイメージの再作成

docker commit {docker ps で表示されるNAMES} obniz_test

ECRにプッシュ

作成したイメージをECRにプッシュしてください。
ECRの作成方法及びプッシュ方法については、【AWS】初めてのECRを参照

Fargate設定

次にFargateの設定を行います。
以下のリンクをクリックして、チュートリアルに沿って作成します。
https://ap-northeast-1.console.aws.amazon.com/ecs/home?region=ap-northeast-1#/firstRun

customを選択し設定をクリックします
f:id:cloudfish:20180831205827p:plain
コンテナ名を入力し、ecrにプッシュしたイメージのurlを入力します。
f:id:cloudfish:20180831205955p:plain
コマンドに「/usr/bin/node,/opt/obniz_test/index.js」を入力します。
f:id:cloudfish:20180831210019p:plain
「次へ」をクリックします。
f:id:cloudfish:20180831210208p:plain
「次へ」をクリックします。
f:id:cloudfish:20180831210231p:plain
クラスター名を入力し、「次へ」をクリックします。
f:id:cloudfish:20180831210245p:plain
設定内容を見直して作成します。
f:id:cloudfish:20180831210341p:plain
5-10分程度でタスクが実行されます。
f:id:cloudfish:20180831210416p:plain

タスクが正常に実行されると以下のように、ディスプレイに「Hello Obniz!」と表示され、左から右へ文字が流れていきます。
f:id:cloudfish:20180901010944p:plain

まとめ

今回は無理やりFargateから動かすということをやってみましたが、正直なところ利用シーンが全然思い浮かばないので全く役にたたない内容だと思いますが、Fargateも試すことができて面白かったです。
obnizは手軽に色々試せるのでかなり面白いコントロールボードだと思いますの。このサイズでディスプレイが付いているのもすごくいいですね。みなさんも是非楽しんでみてください。

AWS Glueのジョブ監視

先日、CloudWatch Eventを確認するとGlueも追加されていたのでジョブの監視をやってみました。

検証構成

f:id:cloudfish:20180814085941p:plain
CloudWatch EventでGlueのJobステータスがSucceededもしくはFailedになればSlackに通知しています。
現状、特定ジョブやイベント(Failedのみなど)のみ通知できないので、フィルタをかけれるようにLambdaを利用することにしました。特定イベントのみ通知できるようになればSNSも使えそうです。
今回はLambdaを利用しているので、Slackだけでなく他のAPIを呼び出すなども可能です。

手順

Slack通知用Lambdaの作成

事前にKMSのキーとSlackのチャンネルに通知するためのIncoming Webhookのエンドポイントを作成しておいてください。
「Lambda」→「関数の作成」をクリックし「一から作成」を選択して以下を入力します。
名前:GlueMonitorSlackNotification
ランタイム:Python3.6
ロール:テンプレートから新しいロールを作成
ロール名:lambda-glue-role
ポリシーテンプレート:KMSの復号化アクセス権限
f:id:cloudfish:20180814092416p:plain

プログラムは以下に置き換えます。

#coding:utf-8
from __future__ import print_function

import json
import urllib.parse
import boto3
import logging
import os
import sys

from base64 import b64decode
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

logger = logging.getLogger()
logger.setLevel(logging.INFO)
SLACK_CONFIG = {}

def decrypt_environment(encrypt_environment_key):
    encrypted_value = os.environ[encrypt_environment_key]
    return boto3.client('kms').decrypt(CiphertextBlob=b64decode(encrypted_value))['Plaintext'].decode()

def init():
    global SLACK_CONFIG
    SLACK_CONFIG = {
        "END_POINT": decrypt_environment('SlACK_END_POINT'),
        "ICON": ":aws:",
        "USERNAME": 'GlueJobBot'
    }
    
def post_message(event):
    post_message = {}
    post_message["icon_emoji"] = SLACK_CONFIG['ICON']
    post_message["username"] = SLACK_CONFIG['USERNAME']
    post_message["attachments"]= [0] * 1
    post_message["attachments"][0]= {}
    post_message["attachments"][0]["fallback"] = 'Glue Jobが実行しました'
    post_message["attachments"][0]["color"] = '#36a64f'
    if event['detail']['state'] == 'FAILED':
       post_message["attachments"][0]["color"] = '#E51616'
    post_message["attachments"][0]["pretext"] = ''
    post_message["attachments"][0]["author_name"] = ''
    post_message["attachments"][0]["author_link"] = ''
    post_message["attachments"][0]["title"] = 'Glue Job ' + event['detail']['jobName'] + ":" + event['detail']['state']
    post_message["attachments"][0]["text"] = 'JobID:' + event['detail']['jobRunId']

    payload_json = json.dumps(post_message)
    data = urllib.parse.urlencode({"payload": payload_json})

    req = Request(SLACK_CONFIG['END_POINT'], data.encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted!! %s", post_message["attachments"][0]["title"])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

def lambda_handler(event, context):
    init()
    post_message(event)
    

関数を作成後、環境変数にSlACK_END_POINTとIncoming Webhookのエンドポイントを入力します。
「伝送中の暗号化のためのヘルパーの有効化」をチェックし、「伝送中に暗号化する KMS キー」に作成したKMSキーを選択して、環境変数欄にある「暗号化」をクリックします。
f:id:cloudfish:20180814124215p:plain

CloudWatch Eventの作成

サービス名:Glue
イベントタイプ:Glue Job State Change
ターゲット:Lambda関数
機能:GlueMonitorSlackNotification ←作成したLambda関数を選択
f:id:cloudfish:20180814125109p:plain

Glue Jobの作成

作成済みのジョブがあればそれを利用してください。
なければ、左メニュー下のチュートリアルのジョブの追加に従ってジョブを追加してください。

実行確認

準備ができればジョブを実行し、Slackの該当チャンネルに通知されているか確認します。
ジョブが成功した時
f:id:cloudfish:20180814130323p:plain
ジョブが失敗した時
f:id:cloudfish:20180814130330p:plain
正しく設定できていれば上記のように通知されるはずです。
現時点でCloudWatch Eventではジョブの指定や失敗時のみなどの細かい設定ができないため、Lambda側でフィルタして通知する必要があります。
これでジョブの成功失敗を監視することができそうです。

Glueのメトリクス

また、Glueにジョブのプロファイリング機能が追加されています。
ジョブのプロファイリング機能を有効にすると以下のメトリクスが取得できます。以下は簡単ですが日本語に訳した内容となります(By Google翻訳
各メトリクスの詳細については、以下を参照してください。
Monitoring AWS Glue Using CloudWatch Metrics - AWS Glue
これらのメトリクスはジョブの中長期的な処理傾向に問題がないかどうかを確認できると思うので、ジョブの内容に合わせて必要なメトリクスを定期的にモニタリングもしくは監視していけばいいかと思います。

* メトリクス * 詳細
glue.driver.aggregate.bytesRead すべてのエグゼキュータで実行されているすべての完了したSparkタスクによって、すべてのデータソースから読み取られたバイト数。
glue.driver.aggregate.elapsedTime ETL経過時間(ミリ秒単位)(ジョブのブートストラップ時間は含まれません)
glue.driver.aggregate.numCompletedStages ジョブの完了段階の数
glue.driver.aggregate.numCompletedTasks ジョブ内で完了したタスクの数
glue.driver.aggregate.numFailedTasks 失敗したタスクの数
glue.driver.aggregate.numKilledTasks killされたタスクの数
glue.driver.aggregate.recordsRead すべてのエグゼキュータで実行されているすべての完了したSparkタスクによって、すべてのデータソースから読み取られたレコードの数
glue.driver.aggregate.shuffleBytesWritten 以前のレポート(AWS Glue Metrics Dashboardによって集計されたデータで、前の1分間にこの目的で書き込まれたバイト数として集計されたもの)以降、すべてのエグゼキュータがデータをシャッフルするために書き込んだバイト数
glue.driver.aggregate.shuffleLocalBytesRead 以前のレポート(AWS Glue Metrics Dashboardによって集計されたバイト数)で、すべてのエグゼキュータがそれらの間でデータをシャッフルするために読み込んだバイト数
glue.driver.BlockManager.disk.diskSpaceUsed_MB すべてのエグゼキュータで使用されるディスク容量のメガバイト
glue.driver.ExecutorAllocationManager.executors.numberAllExecutors 実行中のジョブ実行者の数
glue.driver.ExecutorAllocationManager.executors.numberMaxNeededExecutors 現在の負荷を満たすために必要な最大(実行中のジョブおよび実行中のジョブ)エグゼキュータの数
glue.driver.jvm.heap.usage
glue.executorId.jvm.heap.usage
glue.ALL.jvm.heap.usage
ドライバ、executorIdによって識別されるエグゼキュータ、またはすべてのエグゼキュータのJVMヒープによって使用されるメモリの割合(スケール:0-1)
glue.driver.jvm.heap.used
glue.executorId.jvm.heap.used
glue.ALL.jvm.heap.used
ドライバのJVMヒープで使用されるメモリバイト数、executorIdによって識別されるエグゼキュータ、またはすべてのエグゼキュータ
glue.driver.s3.filesystem.read_bytes
glue.executorId.s3.filesystem.read_bytes
glue.ALL.s3.filesystem.read_bytes
ドライバによってAmazon S3から読み込まれたバイト数、executorIdによって識別されたエグゼキュータ、または前回のレポート以降のすべてのエグゼキュータ(AWS Glue Metrics Dashboardによって前の1分間に読み取られたバイト数として集計)
glue.driver.s3.filesystem.write_bytes
glue.executorId.s3.filesystem.write_bytes
glue.ALL.s3.filesystem.write_bytes
以前のレポート(AWS Glue Metrics Dashboardによって前の1分間に書き込まれたバイト数で集計されたもの)以来、ドライバ、ExecutorIdによって識別されたエグゼキュータ、またはすべてのエグゼキュータによってAmazon S3に書き込まれたバイト数
glue.driver.system.cpuSystemLoad
glue.executorId.system.cpuSystemLoad
glue.ALL.system.cpuSystemLoad
ドライバによって使用されたCPUシステム負荷の割合(スケール:0-1)、executorIdによって識別される実行プログラム、またはすべての実行プログラム

まとめ

Glueが出た当初は、CloudWatch EventからGlueのステータス変更は取得できなかったと思うので、ステータス取得用のLambdaで別途作る必要がありましたが、CloudWatch Eventから拾えるようになったのでジョブの成功・失敗が拾いやすくなりました。もう少しCloudWatch Event側でジョブやステータスの指定など細かく制御できると失敗のみSNSで通知するなどができるので手軽にできるのですが今後の改善に期待です。
また、CloudWatchメトリクスも提供されましたので、これらも含めてGlueのジョブ監視を行なうことができそうですね。

AWS GlueでAurora Serverlessを利用する

Aurora Serverlessが一般リリースされましたね。みなさん色々ブログを書かれていますので流行りにのってGlueからもAurora Serverlessを利用できるか検証してみました。

検証構成

f:id:cloudfish:20180814150635p:plain
今回は上記のような構成で、Auroraからデータ取得してS3に出力してみました。
Aurora ServerlessにはMySQLで提供されているサンプルデータのworldデータベースを取り込んで試してみたいと思います。

Aurora Serverlessの準備

起動

Aurora Serverlessの起動方法は、以下のブログなどを参照して作成してください。
また、他にも色々と情報がでていますので、ググってみてください。
aws.amazon.com

データ準備

Aurora Serverlessはパブリック接続ができないようなので、VPC内にEC2を立ててそこからサンプルデータを取り込みます。

wget http://downloads.mysql.com/docs/world.sql.zip
unzip world.sql.zip
mysql -u ユーザー名 -p -h クラスターエンドポイント < world.sql

#データが作成されているか確認。以下のテーブルが作成されていればOKです。
mysql -u ユーザー名 -p -h クラスターエンドポイント < world.sql
mysql>  use world;
mysql>  show tables;
+-----------------+
| Tables_in_world |
+-----------------+
| city            |
| country         |
| countrylanguage |
+-----------------+

Glueの設定

接続設定

左メニューの「接続」→「接続の追加」をクリックし、以下の設定で接続を作成します。

接続名 任意(aurora-jdbc)
接続タイプ JDBC

Aurora Serverlessは接続タイプ「Amazon RDS」からはまだ選択できないのでJDBCを利用します。
「次へ」をクリック

JDBC URL jdbc:mysql://[クラスターエンドポイント]:3306/world
ユーザー名 Auroraで設定したユーザー名
パスワード Auroraで設定したパスワード
VPC Auroraと同じVPCを設定
サブネット Auroraと通信が可能なサブネットを設定
セキュリティグループ JDBC用のセキュリティグループを作成して割当

確認画面で設定内容が問題ないか確認し「完了」ボタンをクリックしてください。
※上記のセキュリティグループからAurora Serverlessへ3306ポートで通信が可能なようにAurora Serverless側のセキュリティグループを設定しておいてください。

クローラー設定

左メニューの「クローラー」→「クローラの追加」をクリックし、以下の設定でクローラーを追加してください。

クローラの名前 任意(aurora-serverless-crawler)

「次へ」をクリック

Choose a data store JDBC
接続 上記で設定した接続名を選択(aurora-jdbc)
インクルードパス world/%

「次へ」をクリック
「別のデータストアの追加」はそのままで次へ

IAMロール Glue用のロールを指定

※事前に作成していなければ、 こちらを参照し作成してください。
「このクローラのスケジュールを設定する」もそのままで次へ
「データベースの追加」をクリックし

データベース名 任意の名前(world_out)
テーブルに追加されたプレフィックス 入力しない

入力内容を確認し「完了」をクリックします。

クローラーを実行

クローラーが作成されたらクローラーを実行します。
正常終了すると作成したデータベースに以下のテーブルが追加されているはずです。

  • world_city
  • world_country
  • world_countrylanguage

エラーとなる場合は、Auroraへの疎通設定がうまくできていないためだと思いますのでセキュリティグループなどの設定を見直して再実行してください。

ジョブ設定

左メニューの「ジョブ」→「ジョブの追加」をクリックし、以下の設定でジョブを追加してください。

名前 任意(aurora-serverless-job)
IAMロール Glue用のロールを指定

※事前に作成していなければ、 こちらを参照し作成してください。
他はデフォルトのままで「次へ」をクリック

データソース world_city

「次へ」をクリック

「データターゲットでテーブルを作成する」をチェックし

データストア Amazon S3
形式 CSV
ターゲットパス 任意のS3のパス

「次へ」をクリック
「ソース列をターゲット列にマッピングします」もデフォルトのまま「次へ」をクリック
確認画面で設定内容を確認し「ジョブを保存してスクリプトを編集する」をクリックします。
スクリプトについては特に編集は行いません。

実行確認

左メニューの「ジョブ」を選択し、上記で作成したジョブを選択します。
選択後に「アクション」メニューから「ジョブの実行」をクリックしジョブが終了するのを待ちます。
正常に実行されれば実行ステータスが「Succeeded」となりますので、S3にファイルが出力されていることを確認してください。

まとめ

実際のケースではGlueからAurora Serverlessを利用することはあまりないかもしれませんが、問題なく利用できることは分かりましたので何か機会があれば利用してみたいと思います。

ELBがある時・ない時

AWSにおいてELBは気軽に利用できるサービスになっていますが、意外と意識されていないのではないでしょうか?。ELBはロードバランサとしての役割だけでなくSSL証明書のターミネーションやアクセス状況など各種メトリクス確認できることのほかにEC2間を疎結合にできるなど様々なメリットがあり、AWSでインフラ構成を検討する際には必ずといっていいほど出てくるものとなります。
今回はELBがある時とない時を比較し、ELBを使うことでどのようなメリットがあるのかみていきたいと思います。

前提

http(s)でアクセスする一般的なwebシステムを前提とします。
ここで使うELBとはClassic Load Balancerを想定しています。

ケース1

EC2が1台のみのWebサーバ構成において考えてみたいと思います。

ELBがない時

構成図
f:id:cloudfish:20180308230559p:plain

上記構成の場合、Route53(DNS設定)には以下のようなレコードの登録が必要となります。

Name Type Value
example.com A xxx.xxx.xxx.xxx

1台構成なのでELBが無くてもよいのですが、ELBが無いとどういったメリット・デメリットがあるか考えてみます。

メリット
  • ELBの利用料が不要
デメリット

 SSL証明書の終端をWebサーバで対応する必要があります。WebサーバにSSL証明書を新規に設定もしくは更新する場合、かなり手間になります。またopensslの脆弱性が出た場合なども対応する必要がある。

  • アクセス状況などの各種メトリクス確認

 サイトが高負荷になった場合や、アクセス状況を確認したい場合などに、Webサーバのアクセスログを集計して確認する必要があるため、即座に状況判断ができない。

  • 拡張性

 Webサーバを複数台に拡張する場合、大きな構成変更を伴う。
①ELBを追加するケース
 変更点:ELBの追加、EC2の追加、DNSの変更
DNSラウンドロビン
 変更点:EC2の追加、DNSの変更

ELBがある時

構成図
f:id:cloudfish:20180314092135p:plain

Route53レコード

Name Type Value
example.com A example-com-elb-123456789.ap-northeast-1.elb.amazonaws.com.

上記のような構成において、それぞれどのようなメリットがあるか考えてみましょう
EC2が1台の構成ですが、DNSからは直接EC2ではなくELBを指す形となっています。

メリット

 SSL証明書の終端をELBで対応してくれます。SSL証明書の設定や更新も手軽に実施できます。
ELB側に脆弱性が発見された場合においてもAWS側で対応してくれるので利用者側で意識する必要がありません。
またACMAWS Certificate Manager)といったAWSから提供されている無料の証明書の利用も可能です。

  • アクセス状況などの各種メトリクス確認

 サイトが高負荷になった場合やアクセス状況を確認したい場合において、ELBのメトリクスでリクエストの状況が確認できるため、アクセス状況の把握が可能

  • 拡張性

 Webサーバを複数台に拡張する場合、大きな構成変更が不要。
EC2を複製し、ELB配下にアタッチするだけで拡張が可能。(アプリが冗長構成に対応できている前提)

デメリット
  • ELBの利用料が必要

ケース2

次に以下のようなWeb層とAP層に分かれた構成で考えてみたいと思います。
比較ポイントはWeb層とAP層の間に内部ELBを配置する場合としない場合で比較してみます。

ELBがない時

構成図
f:id:cloudfish:20180617125309p:plain

メリット
  • ELBの利用料が不要
デメリット
  • APサーバへの接続

 WebサーバーはAPサーバのIPを意識する必要があり、IPが変更された場合の柔軟性がない

  • 拡張性

APサーバが拡張、縮小された場合、Webサーバー2台ともにAPサーバの接続先を追加する必要がありメンテナンス性に欠ける

ELBがある時

構成図
f:id:cloudfish:20180617125348p:plain

メリット
  • APサーバへの接続

WebサーバはAPへの接続先としてELBに接続するだけでよい

  • 拡張性

APサーバが拡張、縮小された場合、Webサーバ側の変更は不要

デメリット
  • ELBの利用料が必要

ケース2ではPrivateHostedZoneを利用してWeb層とAP層をさらに疎結合にすることも可能ですが、その反面、構成が分かりにくくなるというデメリットもあるのでそれらを踏まえて検討してください。

まとめ

ELBのある時とない時を比較してみましたがいかがでしょうか?
ELBを利用する際のデメリットとしてはほぼ費用面のみかと思いますが、月額については1000-2000円程度なので運用面でメリットを考えるとかなり安いのではないでしょうか?
個人的には特殊な要件がない限り、シングル構成であってもELBは必ず利用するようにしています。
みなさんも是非利用を検討してみてください。

CodeBuildでDockerイメージのマルチステージビルド

Dockerでマルチステージビルドという機能を知ったので検証がてらCodeBuildで試してみました。
マルチステージビルドとは、例えばjavaアプリケーションにおいて、ビルドについてはjdkが入ったイメージを利用してビルドを行い、ビルドされたバイナリだけをjreが入ったイメージにコピーしてDockerイメージを作成することをDockerイメージのビルド時にできる機能となります。こうすることで簡単に実行するDockerイメージを小さくすることが可能となります。この機能はDokcerの17.05以降で利用可能となっています。

検証内容

Javaアプリをmavenがインストールされているコンテナでビルドして、jreがインストールされているコンテナをベースにイメージを作成しECRにプッシュします。
構成イメージは以下になります。
f:id:cloudfish:20180809094159p:plain

ECRの作成

イメージプッシュ用のリポジトリを作成します。
AWSコンソールの「Elastic Container Service」→「リポジトリ」から「リポジトリの作成」をクリックし、リポジトリ名を入力して リポジトリを作成します。ここでは「multistage-ecr」という名前で作成しました。

CodeCommitの作成

AWSコンソールの「CodeCommit」→「リポジトリの作成」をクリックし、リポジトリ名を入力してリポジトリを作成ます。ここでは「multistage-test」という名前で作成しました。

ビルド用リソースの作成

以下のリソースを作成し、作成したCodeCommitのリポジトリにコミットしてください。

├── ./Dockerfile
├── ./buildspec.yml
├── ./pom.xml
└── ./src
    └── ./src/hoge
        ├── ./src/hoge/Main.class
        └── ./src/hoge/Main.java

GitHub - cloudfish7/multi-stage-build-for-codebuildに一式配置していますのでここからもDLできます。

Dockerfile

# ビルド用コンテナでjavaをコンパイル。build1と名前を付けて後続で利用
FROM maven:3.3.9-jdk-8 AS build1
RUN mkdir -p /opt/java/src
ADD ./pom.xml /opt/java/
ADD ./src /opt/java/src
RUN cd /opt/java && mvn install

# jreがインストールされたイメージにビルド用コンテナから作成したjarファイルをコピー
FROM openjdk:8u131-jre-alpine
RUN mkdir -p /opt/app/
COPY --from=build1 /opt/java/target/ /opt/app/

RUN  java -jar /opt/app/HelloWorld-1.0.jar

buildspec.yml

dockerイメージをビルドしてECRにプッシュします。
以下をセットして
{REPO_NAME}にはECRのリポジトリ
{tag}にはイメージタグをセット(何もなければlatest)
{account_id}にはAWSアカウントID

version: 0.1
 
phases:
  pre_build:
    commands:
      - $(aws ecr get-login --region ap-northeast-1 --no-include-email)
  build:
    commands:
      - docker build -t {REPO_NAME}:{tag} .
      - docker tag {REPO_NAME}:{tag} {account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/{REPO_NAME}:{tag}
      - docker push {account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/{REPO_NAME}:{tag}
  post_build:
    commands:

pom.xml

コンパイルしてjarファイルを作成します。

<project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>hoge</groupId>
  <artifactId>HelloWorld</artifactId>
  <version>1.0</version>
  <name>Java Sample App</name>

  <build>
        <outputDirectory>target/classes</outputDirectory>
        <sourceDirectory>src</sourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>hoge.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
  </build>
</project>

Main.java

package hoge;

class Main{
  public static void main(String args[]){
    System.out.println("Hello Docker!!");
  }
}

CodeBuildの作成

AWSコンソールの「CodeBuild」→「プロジェクトの作成」をクリックします。

プロジェクト名:multistage-build
ソースプロバイダ:AWS CodeCommit
リポジトリ:multistage-test ← CodeCommitのリポジトリ
環境イメージ:AWS CodeBuildによって管理されたイメージの使用をチェック
オペレーティングシステムUbuntu
ランタイム:Docker
バージョン:aws/codebuild/docker:17.09.0
ビルド仕様:ソースコードのルートディレクトリのbuildspec.ymlを使用をチェック
上記以外はデフォルトのままとしてプロジェクトを作成します。
f:id:cloudfish:20180808155111p:plain

プロジェクト作成後に、AWSコンソールの「IAM」→「ロール」から作成されたCodeBuild用のサービスロールを選択し、以下のポリシーを追加します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "ecr:BatchCheckLayerAvailability",
                "ecr:CompleteLayerUpload",
                "ecr:GetAuthorizationToken",
                "ecr:InitiateLayerUpload",
                "ecr:PutImage",
                "ecr:UploadLayerPart"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

実行確認

準備が整ったのでビルド実行してECRにプッシュされることを確認します。
AWSコンソールの「CodeBuild」→作成したプロジェクトを選択し、「ビルドの開始」をクリックします。
プロジェクト名:作成したプロジェクト名
ブランチ:master
他はデフォルとのままで「ビルドの開始」をクリックします。
f:id:cloudfish:20180808162611p:plain

ビルドが正常終了した場合、ステータスが「Succeeded」となります。
失敗した場合はステータスが「Failed」となりますので、詳細画面からビルドログを確認しエラー内容を確認してください。

正常にビルドが完了しjavaのビルドも正しく完了していると、ビルドログに以下のように出力されていると思います。ビルドイメージ作成時にJavaのアプリが正常にコンパイルされているかチェックするため実行しています。
f:id:cloudfish:20180808162833p:plain

まとめ

今回利用したjreのみのイメージだと約50MBとなり、openjdkのalpineイメージ(約100MB)と比べてもサイズを大幅に削減することが簡単にできました。
また、多段での実行が可能なので、アプリをビルド後、テスト用のイメージでテストを実行し、その後に実行用のイメージを作成するようなこともできそうです。