UserData、OpsWorks、Lambdaを組み合わせ、常に新鮮なSpotFleetインスタンスでサービスを運用する

f:id:vasilyjp:20180927090637j:plain

インフラエンジニアの光野(@kotatsu360)です。 今週のテックブログは豪華二本立てでお送りいたします。

一本目はバックエンドエンジニアの木曽による「福利厚生を使ってAWSソリューションアーキテクト アソシエイトを取得しました」でした。

二本目はUserData、OpsWorks、Lambdaを組み合わせ、常に新鮮なSpotFleetインスタンスでサービスを運用するという取り組みについて紹介させていただきます。 なお、本記事は先日開催されたX-Tech JAWS 【第2回】~9割のX-Techと1割の優しさで切り拓く未来~での登壇資料を補足するものです。適宜スライドを取り上げて説明をいたします。

キーワード:EC2 SpotFleet、AWS Lambda、UserData、Apache Mesos

ネットワーク概要

弊社が運営するIQONは、独自のクローラーを用いて契約済みECサイトから情報を収集、整理、掲載しています。 そのためIQONにおいてクローラーは非常に重要な存在です。

このクローラー、何度かの刷新を経て現在は次の構成を取っています。

  • アプリケーションはコンテナ化
  • 計算資源は、Amazon EC2 SpotFleetによるクラスタをApache Mesosで抽象化
  • クロール状況に応じてコンテナ数が増減

全体図は以下のとおりです1

これらは、大きく3つのフェーズで構築されました。

本記事で触れる範囲はPhase.3です2

Phase.3以前の悩み

SpotFleetによる柔軟な構成を取ることでインスタンスを立てる手間は無くなりました。 しかし、1つだけ手元に残った作業があります。custom AMIの更新作業です。 f:id:vasilyjp:20180301185109p:plain

Packer by HashicorpでAMIの作成自体は自動化していましたが、AMI作成用のクレデンシャルや実行方法は属人化しています。これを解決することを目的としたのがPhase.3です。

UserDataとOpsWorksとLambda

2017年の中頃、構成管理にOpsWorksを導入していたということもあり3、SpotFleetインスタンスたちも同様に管理することにしました。

f:id:vasilyjp:20180301185143p:plain

順を追って説明します。

1. インスタンスの起動とUserDataによるOpsWorksの呼び出し

まず、SpotFleetインスタンスが起動します。SpotFleetの起動設定として次のUserDataが登録されています。

  # SpotFleet インスタンスについての記述
  # 記述...
  UserData:
    Fn::Base64: !Sub
      - |
        #cloud-config
        repo_update: true
        repo_upgrade: security # RedHat系ならこっち
        apt_upgrade: true      # Debian系ならこっち
        packages:
          - python-pip
        runcmd:
          - LC_ALL=C sudo pip install awscli
          - /usr/local/bin/aws opsworks --region us-east-1 register --infrastructure-class ec2 --stack-id ${OpsWorksStackId} --local --use-instance-profile 2>&1 | tee /tmp/register.log
          - export INSTANCE_ID=$(grep 'Instance ID' /tmp/register.log | grep -oE '[a-z0-9\-]+$')
          - while ! /usr/local/bin/aws opsworks --region us-east-1 assign-instance --instance-id $INSTANCE_ID --layer-ids ${OpsWorksLayerId1} ${OpsWorksLayerId2} ${OpsWorksLayerId3}; do echo 'wait...'; sleep 20; done
      - OpsWorksStackId: !Ref OpsWorksStack
        OpsWorksLayerId1: !Ref OpsWorksLayerCommon
        OpsWorksLayerId2: !Ref OpsWorksLayerMesosBase
        OpsWorksLayerId3: !Ref OpsWorksLayerMesosSlave
  # SpotFleet インスタンスについての記述
  # 記述...

これはCloudFormationテンプレートによるUserDataの設定です。依存するOpsWorksのID類が解決され、最終的にはBase64エンコードされた文字列が登録されます。 runcmdでは、3つのタスクを行っています。

  1. awscliのインストール
  2. OpsWorksへの登録(register)
  3. OpsWorksレイヤへの追加(assign)

whileループはregisterの終了を待ってassignするための処理です4

OpsWorksの呼び出しがUserDataの役割で、これ以降は全てOpsWorks側で行います。

2. OpsWorksによる構成管理とMesosクラスタへの参加

OpsWorks側にはSetupDeployの2つのタイミングでレシピが実行されるように設定されています。

  OpsWorksLayerMesosSlave:
  # レイヤについての記述
  # 記述...
    CustomRecipes:
      Setup:
        - 'mesos::slave'
      Deploy:
        - 'mesos::attach'
  # レイヤについての記述
  # 記述...

mesos::slaveはMesosエージェントのインストールなどの前準備、mesos::attachがエージェントの再起動や最終的な設定変更を担当しています。 最終的な設定変更が終了するとMesosクラスタの一員として新しいタスクを処理できるようになります。

Chefはレシピの実行順を制御することができません5。その為、OpsWorksのライフサイクルを利用してSetup完了後(=環境構築後)のDeployイベントに最終的な設定を任せることにしています6

余談ですが、昔、手でcustom AMIを作成していた時代には、mesos::attach相当の内容がUserDataとして書かれていました。UserDataからOpsWorksを呼び出すなら一緒にChefのレシピにした方が楽だという事でお引っ越しです。

閑話休題。

3. Lambdaによる状態監視とリトライ

2. OpsWorksによる構成管理とMesosクラスタへの参加までが終わると、新しいSpotFleetインスタンスはMesosクラスタの一員としてタスクを処理します。 ここからは運用面のフォローについて補足します。

OpsWorksで構築を行っていると、時たまネットワーク的な不調で構成管理に失敗することがあります。リトライすると直ります。一時は手でフォローしていたのですが辛くなったので今はLambdaに状態監視とリトライを任せています。

f:id:vasilyjp:20180301185213p:plain

CloudWatch Eventsで定期的にLambdaを発火、LambdaはOpsWorksに登録されているインスタンスを監視し、必要に応じて再setupのリクエストを投げます。

4. OpsWorksから削除

最後はOpsWorksからインスタンスを削除する部分です。OpsWorksは明示的にderegisterのリクエストを打たない限り、インスタンスを管理し続けようとします。 その為、SpotFleetインスタンスを落とす際にはインスタンス自身がderegisterをリクエストするようcronスクリプトで準備しています。

#!/bin/bash
# terminate notificationを受け取ったときのスクリプト
RES=$(curl -w '%{http_code}' -o /dev/null -s -- 'http://169.254.169.254/latest/meta-data/spot/termination-time')
if [ "${RES}" = "404" ]; then
    :
else
    # SpotFleetインスタンスなので再起動することが無い。困ったら破棄。tmp以下のファイルで連携しても大して問題にならない
    export INSTANCE_ID=$(grep 'Instance ID' /tmp/register.log | grep -oE '[a-z0-9\-]+$')
    /usr/local/bin/aws opsworks --region us-east-1 deregister-instance --instance-id $INSTANCE_ID 2>&1 | tee -a /tmp/register.log
    
    # mesosのエージェントを止めたり、監視とめたり、Dockerコンテナ止めたり、、、
    # 後始末
fi

毎分メタデータから終了予告の有無をチェックし、必要であればderegisterします7。 1点注意があります。ユーザ操作でSpotFleetクラスタのサイズを変える場合はtermination-timeが設定されません。何らか手動で操作する場合は、このスクリプト相当の作業を手で行う必要があります。

日常的なSpotFleetクラスタのサイズ変更は、これまた別のLambdaがコントロールしています。そちらはLambda(lambda.amazonaws.com)による実行となるためかtermination-timeが設定されます。特に手を出すことはありません。

メリット・デメリット

custom AMIを作成して利用するというやり方から、OpsWorksを利用するやり方への変遷についてご紹介しました。 改めてメリット・デメリットを整理します。

メリットは次の3つです。

  1. 事前にcustom AMIを作成する準備が不要
    • クレデンシャルや手順書を用意する必要がない
  2. 常にパッケージ類が最新
    • 固定したいものがあれば、それもできる
  3. 同じ枠組みでサービスの全インスタンスが管理される
    • EC2インスタンスとSpotFleetインスタンスを共通のレシピから構築

一方、毎回OpsWorksで管理させることのデメリットです。

  1. Mesosクラスタに参加するまでのリードタイムが長い
    • 10〜15分、custom AMI時代は3〜5分
  2. たまに構築が失敗する
    • Lambdaでフォロー
  3. インターネットに優しくない
    • 毎回パッケージ類を取得するため

新鮮さと速度のトレードオフ。 custom AMI作成の自動化を検討したこともありますが、作成自動化+SpotFleetクラスタの更新まで考えると最終的に今の形に落ち着きました。

まとめ

本記事では、UserDataとOpsWorks、Lambdaを使ったSpotFleetインスタンスの構成管理についてご紹介しました。新鮮なインスタンスが使えること、custom AMIの更新がなくなったこと、より健全な運用体制ができたと感じています。

さて、弊社のMesosクラスタに関して、これまでの取り組みを以てネットワーク構成面での不満は概ね解消されました。そのため次はMesos自体も含めた各種ソフトウェアのバージョンアップに取り組む予定です。いずれまたご紹介できればと考えています。

VASILYでは現状に満足せず、より健全な姿を目指して挑戦できる、挑戦が好きなエンジニアを募集しています。興味がある方は以下のリンクからお申し込み下さい。


  1. この構成は、2018年3月で本番投入からちょうど1年になりました🎉

  2. Phase.2についてはDocker / Apache Mesos / Marathon による3倍速いIQONクローラーの構築にて紹介しています

  3. CloudFormationとOpsWorksでインフラを育てる

  4. なお、OpsWorksはwaitのAPIがあるため、/usr/local/bin/aws opsworks --region us-east-1 wait instance-registered --instance-ids $INSTANCE_IDと書けば、whileで待つ必要はありません。最近、知りました。。

  5. 大抵は書いた順ですが、include_resipeやnotifiesによる呼び出しを全て把握して書くのは煩雑です

  6. AWS OpsWorks Stacks のライフサイクルイベント

  7. temination-timeはdeprecatedらしく、instance-actionを使うように勧められています。

カテゴリー