docker基盤としてECSを採用しました③

こんにちはmatsです。

少し間が空いてしまいましたが、引き続きECSまわり記事です。

cloud-init

IMではSpot FleetでECSの基盤を構築しているのですが、その際に重要なのが cloud-init になります。ECS Optimized AMIにcloud-initで最低限の設定を行うことで、Ansible等での構成管理やAMIの管理工数を削減しています。今回はそのcloud-initについてお話します。

cloud-config

まず、記述方法についてです。ECSのドキュメントなど多くの記事でBash形式の記述が取り上げられていますが、cloud-config 形式のほうが圧倒的におすすめです。IMはAmazon Linux (ECS Optimized AMI)なので、他のディストロでは挙動が異なる可能性はありますが、Bash形式よりも細かく挙動を制御することが出来ます。

S3 からの読み込み

cloud-init の設定はS3に配置し、http経由で読み込むようにしています。 理由としては、AutoScaling や SpotFleet でcloud-initの設定内容を変更するためには、設定の作り直しが必要で、運用上不便な点が多いからです。 権限は VPC Endpoint で与えています。 複数ファイル記述することも出来るので、用途に応じて分割すると良いかと思います。

#include
https://s3-ap-northeast-1.amazonaws.com/<bucket>/test1.yml
https://s3-ap-northeast-1.amazonaws.com/<bucket>/test2.yml

cloud-init で行っている設定

ユーザ作成

#cloud-config

users:
  - default
  - name: matsuda
    lock_passwd: true
    gecos: Kazuki Matsuda
    groups: wheel,docker
    sudo: [ "ALL=(ALL) NOPASSWD:ALL" ]
    shell: /bin/bash
    ssh-authorized-keys: [ "ssh-rsa AA ... DQ==" ]

docker のセキュリティ的にはログイン可能なユーザは作らないほうが良いのですが、過渡期なので docker の操作ができるユーザを作っています。

なお、 - default で ec2-user を作成しているらしいので注意が必要です。

AutoScaling環境でのユーザ管理でもAMIの作り直しなどが不要になるので、ユーザ管理の方法としてはかなりイケてるんじゃないでしょうか。

Spot Instance の中断検知

#cloud-config

packages:
  - aws-cli
  - jq

write_files:
  - path: /etc/ecs/deregister.sh
    permissions: '0755'
    content: |
        #!/bin/sh
        region=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed -e 's/.$//g')
        ecs_cluster=$(curl -s http://127.0.0.1:51678/v1/metadata | jq -r '.Cluster')
        ecs_instance=$(curl -s http://127.0.0.1:51678/v1/metadata | jq -r '.ContainerInstanceArn')
        for i in {1..11}
          do
            if curl -s http://169.254.169.254/latest/meta-data/spot/termination-time | grep -q .*T.*Z; then
              aws ecs deregister-container-instance --region $region --cluster $ecs_cluster --container-instance $ecs_instance --force
            fi
            sleep 5
          done

  - path: /etc/cron.d/spot_retire
    content: |
        PATH=/sbin:/bin:/usr/sbin:/usr/bin
        * * * * * root /etc/ecs/deregister.sh

弊社では Spot Instance をヘビーに使っているのですが、ECSの基盤として利用する際は中断処理を検知してコンテナをオフロードする必要があります。 こちらの設定も cloud-init で行っております。

5秒おきに meta-data を参照し、中断の時刻が取得できた場合はECS Clusterから退役するようなcronを設定しております。

Instance Store (SSD) のマウント

#cloud-config

bootcmd:
  - [ cloud-init-per, instance, docker_storage_setup, /usr/bin/docker-storage-setup ]
  - |
      yum install -y mdadm
      for i in 0 1 2 3 4 5 6 7 ; do
          disk=/dev/nvme${i}n1
          dir=/mnt/nvme${i}
          if [ -e "$disk" ]; then
              mkfs.ext4 -F $disk
              mkdir -p $dir
              mount $disk $dir
              rm -rf $dir/*
          fi
      done
      chmod -R 777 /mnt

dokcer上で Instance Store を利用するには、docker engineの起動前にファイルシステムのマウントをする必要があります。 これを実現するに弊社では bootcmd ステップで処理を行っております。 なお、ひとつめの

  - [ cloud-init-per, instance, docker_storage_setup, /usr/bin/docker-storage-setup ]

は、Docker Storage の初期化を行っているようで、 /etc/cloud/cloud.cfg.d/90_ecs.cfg からコピーしてきて記述しております。 (merge_how が記述されていないので、上書きしてしまうようです。)

その他

#cloud-config

write_files:
  - path: /etc/sysctl.d/12-docker.conf
    permissions: '0644'
    content: |
        net.core.somaxconn = 65535
        net.core.netdev_max_backlog = 16384
        net.ipv4.ip_local_port_range = 1024 65535
        net.ipv4.tcp_tw_reuse = 1
        net.ipv4.tcp_max_syn_backlog = 65535
        vm.swappiness = 1

  - path: /etc/cron.d/reboot_ecs-agent
    content: |
        PATH=/sbin:/bin:/usr/sbin:/usr/bin
        0 * * * * root docker kill ecs-agent

runcmd:
  - sysctl -p /etc/sysctl.d/12-docker.conf

merge_how: 'list(append)+dict(recurse_array)+str()'

あとは、

  • カーネルパラメータのチューニング
  • ecs-agentが詰まることがあるので、定期的な再起動
  • cloud-config が複数あった場合の結合方法

などの設定を行っております。

まとめ

上記のような設定を clous-init 用のリポジトリで管理しており、GitHub の Pull Request に連動する形でS3へコピーしております。

これらが、弊社で行っているECSに適した形の構成管理になります。Ansible等を使うのもいいですが、変更箇所が少ない場合は cloud-init の方が都合がいい部分も多いのでおすすめです。

AWS Summit で公演してきました

こんにちはmatsです。

最近、更新サボっていてすいません。。 (ちなみにブログの更新は評価対象ですw)

5月30日(火)~6月2日(金)の間行われている、「AWS Summit Tokyo 2017」で公演してきました。

講演資料はこちら

風邪なのか前日から喉の調子が悪い上、めちゃめちゃ緊張していて全く余裕がなく、ほとんどスピーカーノート読んでましたが無事終えることが出来ました。(笑)

できるだけ空中戦にならないように、実使用に沿った話になるように心がけていたのもあってか、思いの外評判は良かったようです。

もし、IMのシステムや開発スタイルに興味をもって頂けましたら、Wantedlyあたりから遊びに来てみてください。

会社に行くほどでは、、、という方はFacebookで個人的に凸って頂いても大丈夫です。

ではでは。

「AWS Summit Tokyo 2017」に登壇することになりました

こんにちはmatsです。 最近、更新をサボっていてそろそ書かないとと思い、久しぶりに更新してみています。

本ブログでも何回か書いていますが、IMではSpot fleetやECSといったAWSならではのサービスを数多く活用しております。

その点が評価されてなのか、この度「AWS Summit Tokyo 2017」にてお話する機会を頂きました!

弊社HP

corp.intimatemerger.com

AWS Summit Tokyo 2017 - イベントページ

www.awssummit.tokyo

基本的にはブログで書いているような内容+αのお話をする予定ですが、当日会場でしかお見せ出来ない内容もありますので、ぜひお越し頂ければと思います。 が、何の奇跡なのか皆さん暇なのか分かりませんが、既に満席となっておりますね。。。

セッション後はお話する時間がありますので、IMの技術やIMのAWSの使い方、マーケティングテクノロジーに興味のある方はお声がけ頂ければと思います。

それでは当日お会いしましょう。

docker基盤としてECSを採用しました②

こんにちはmatsです。

今回はdocker環境への全面移行について、運用環境や全体構成について書こうかと思います。

全体構成

f:id:intimatemerger:20170223123255p:plain

デプロイフロー

GitHubとCircleCIの連動を軸に自動化しています。流れ的には

  1. [GitHub] PRをmasterブランチにマージ
  2. [CircleCI] 自動テスト
  3. [CircleCI] docker build でイメージ作成
  4. [CircleCI] ECRに対してdocker push
  5. [CircleCI] API経由でECS Task Definitionを更新
  6. [CircleCI] API経由でECS Serviceを更新
  7. [ECS] Sevice設定を元にコンテナが展開される

masterブランチが勝手にデプロイされるので、デプロイという作業は行っていないような感じです。

また、ECSのコンテナの入れ替えはELBのヘルスチェックが通るまで古いコンテナを破棄しないため、Blue-Greenデプロイの様な感じになります。

Spot Fleet

dokcer基盤のインスタンスは全てSpot Fleetにて起動しています。 Spot Fleetの利用にあたってはcloud-initを駆使しているので、そのうちcloud-initに絞った記事を公開しようかと思います。

用途に応じてSpot FleetとECS Clusterを分けていますが、分け方としては

  • ジョブワーカー用の基盤はCPUのコア数指定でSpot Fleetを入札
  • Webサーバ用の基盤はインスタンスの台数指定でSpot Fleetを入札

のような感じです。

また、用途によらず基盤のオートスケールは行っていません。 理由としては

  • ECSのリソース予約がまだチューニング段階
  • オートスケール速度が緩慢
  • そもそもスポットインスタンスを使っているので、それほどコストメリットが効かない

などがあげられます。

ログハンドリング

詳細はまた別途書こうかと思いますが、基本的に

  • アプリケーションのログ → fluentdでBigQueryやMySQL、S3に収集
  • コンテナの標準出力 → logging-driverのsyslogでpapertrailへ

のような感じで行っています。papertrailでなくてもいいかともいますが、remote syslogに対応したサービスだとdocker engineから直接送ることが出来るので非常に楽に運用することが出来ます。

次回以降はもう少し個々の要素について細かく書いていこうかと思います。

docker基盤としてECSを採用しました①

こんにちはmatsです。

IMでは2016年の夏頃からdocker環境への全面移行を行っております。 一旦、一段落したので、色々とまとめてみようかと思います。

なぜECSを採用したのか

dockerの基盤としてはKubernetesやDocker Swarmなど、いくつか選択肢があり、IMでもいくつか検討してみましたが、下記の理由でECSを採用しました。

  • Spot Fleet との相性がいい
  • コンテナのオートスケールをCloudWatch連動で出来る
  • 管理画面をAWSがホストしてくれる
  • Docker Swarmをサポート予定(らしい)

色々理由はあるのですが、一番はサクッと試してみることができて、ラフに運用が出来るのが一番の理由だったりします。

何をdocker環境で動かしているのか

IMのほぼすべてのシステムがdocker環境で稼働しており、下記のようなサービスがあります。

  • Python製のアプリケーション(uwsgi)
  • Python製のジョブワーカー(SQS連動)
  • Elasticsearch

しかし、Aerospikeまわりで数ミリ秒レベルの高速性が要求される箇所に関しては、dockerのネットワークレイテンシの影響が強く出るため採用していません。

dockerの運用で気をつけていること

基盤のAMIはAWSが用意したものをそのまま使う

設定内容によっては、AWSが用意しているECS向けのAMIをそのまま使いにくいケースもあるかもしれませんが、IMでは自前でAMIをビルドすることは行っておりません。

下記のようなことを徹底しており、docker基盤のLinuxの入れ替えが簡単に行えるようにしております。

  • AWSが用意しているECS向けのAMIをそのまま使う
  • 設定変更はcloud-initで行う
無理にAlpine Linuxを採用しない

docker界隈ではそのイメージサイズの小ささからAlpine Linuxが注目されていますが、IMでは採用するケースはあまり多くありません。

IMのアプリケーションはpythonで書かれているものがほとんどですが、ライブラリの中にはC拡張のものも少なくなく、OSによってはインストールのハードルが高いことがあるので、ライブラリの動作確認が取れているDebian (Ubuntu)を採用するケースが多いです。

また、アプリケーションエンジニアの多くが自分で調べて解決できる環境という点でも、マイナーOSの採用は慎重になったほうがいいかと思います。

基盤の運用にはSpot Fleetを使う

運用負荷とサーバコストの観点から、Spot Fleetにて基盤の運用を行っております。

適切に運用できればオンデマンドの1/4〜1/5程度の費用で運用が出来ることと、オートスケールのように自動復旧が可能になることからECSの基盤としてはベストな選択かと思います。ただ、AMIの入れ替えについてはSpot Fleetの設定を作り直さなくてはいけなかったりするので、若干使いにくい部分もあります。

まとめ

全環境でSpot Fleetを利用できる様になったことでコストの圧縮につながり、また、アプリケーションをコンテナ化することで運用・開発効率が上がりました。

次回以降はそれぞれのサービスについて少し細かく書いていこうかと思います。

他社サイトに設置するjsタグを作る時のTips(prototype汚染の話)

こんにちは、g0eです。

アドテク業界ではjavascriptのタグ(以下、jsタグ)をクライアントサイトに埋め込んでもらうことがよくあります。 自社サイトの開発と違って、設置先のサイトでどのようなjavascriptが動いているかわからないので、他のjavascriptと競合しないように書く必要があります。

今回は他サイトに設置するjsタグを書く時に注意すべき事項の中の一つの、prototype汚染の話をしたいと思います。

プロトタイプ汚染の例

下記のコードを実行した後に、

// Objectのプロトタイプを汚染するコードの例
Object.prototype.hoge = function(){}

下記のような、オブジェクトpiyoをfor-inループで 調べるようなスクリプトを実行します。

var piyo = {foo: 1, bar: 2};
for(var k in piyo){
   console.log(k);
}

すると、コンソールに以下の文字列が表示されると思います。

foo
bar
hoge    /* !? */

そんなの当たり前だろ、と思った方はこの記事はここで読み終えていただいて大丈夫です。

あれ、hogeが混ざってきたぞ、という方はこのまま読み進めて下さい。

javascriptのプロトタイプチェーンの話はなかなか奥が深い話なので、今回は言及しませんが、上記のようにObject.prototypeやArray.prototypeなどに新しいプロパティを追加するコードがどこかにある場合、for-inループをそのまま使うのは非常に危険ということが伝わったでしょうか?

一昔前(むしろ大昔?)に流行ったprototype.jsというライブラリを利用しているサイトで、このようなプロトタイプ汚染に巻き込まれることがあります。

プロトタイプ汚染に強いfor文の書き方

hasOwnPropertyを使う

for-inループの中で取得したプロパティが、対象オブジェクト自身のプロパティ(つまり、プロトタイプチェーンをたどって取得したものではない)かどうかをチェックして利用するようにします。

var piyo = {foo: 1, bar: 2};
for(var k in piyo){
   if(piyo.hasOwnProperty(k)){
      console.log(k);
   }
}
Object.keysを使う

Object.keysを使ってプロパティの一覧を取得することでも、同じ効果を得ることができます。

var piyo = {foo: 1, bar: 2};
var keys = Object.keys(piyo);
for(var i=0;i<keys.length;i++){
   console.log(keys[i]);
}

まとめ

かなり駆け足になってしまいましたが、今回のまとめは以下の通りとなります。

  • ObjectやArrayのprototypeをいじらない(プロトタイプ汚染しない)
  • for-inループを使う時は注意する(使わない、もしくはhasOwnPropertyを併用する)

AWS Lambdaを使ってmackerelにEC2のSpotPriceを投げ込む

こんにちは。g0eです。

昨日、弊社のコーポレートサイトをリニューアルしました。 なかなかかっこよく仕上がったんじゃないかと満足している一方で、随分と更新をサボっていたこのブログにも、きちんとしたリンクが貼られてしまったので、とりあえず何か書いておこうと思います。

参考までにコーポレートサイト corp.intimatemerger.com

背景

弊社ではAWS上にスポットインスタンスをうまく活用して、elasticsearchのクラスタを構築しています。スポットインスタンスは価格をかなり抑えられる一方で、時々価格が高騰してインスタンスが強制ターミネートされてちょっとドキドキします。(昨年の11月下旬〜12月上旬は特に頻度が高かったような)

冗長化して片方のAZが全滅してもデータ欠損は発生しない構成にしたり、スポットインスタンスで十分な数が確保できない場合は、オンデマンドインスタンスが自動的に起動するようにしたり、いくつか対応策はとっているので実害はほぼなかったりするのですが(ここの話はまた機会があれば改めて書きたいなとは思っています)、まずはスポットインスタンスの価格をきちんとモニタリングして傾向をつかんで行こうというのが今回の投稿の背景だったりします。

目的

前置きが少し長くなってしまいましたが、本題としては、弊社で監視に使っているmackerelにEC2のスポットインスタンスの価格推移を投げ込んで、アラートをあげたり、中長期のトレンドを可視化していきたいという話になります。(mackerel→slackにアラートを投げてPC/スマホから監視していますが、チャートも一緒に表示されるのでなかなか便利です。)

実装

早速ですが、lambdaのコード貼っておきます。言語はpythonです。

コード書いたのが1ヶ月以上前なので若干記憶が曖昧なのですが、スポット価格を取得するAPIの仕様がちょっと特殊で、

  • 指定した期間で価格が動いた時のデータ
  • 価格変動がなければ最後に価格が動いた時のデータ

が返ってきていたと記憶しています。

コードではちょっとトリッキーですが、未来の時間を開始時間=終了時間で指定することで、APIをコールした時点で最新の価格だけを取得しています。(気持ち悪いですが、試行錯誤した中ではこれが一番良かった)

import boto3
import datetime, time, json, urllib2
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

MACKEREL_APIKEY = "XXXXXXXXXXXXXXXXXXXX" # mackerelのAPIキー
MACKEREL_SERVICE = "xxxxxxxxx" # mackerelのサービスを指定
EC2_INSTANCE_TYPES = ["m4.4xlarge","c3.2xlarge"] # 価格を調べたいインスタンスを指定
EC2_PRODUCT_DESCRIPTIONS = ["Linux/UNIX (Amazon VPC)"]

def post_mackerel (apikey, service, payload):
    headers = { 'Content-type': 'application/json', 'X-Api-Key': apikey }
    url = "https://mackerel.io/api/v0/services/{0}/tsdb".format(service)
    data = json.dumps(payload)
    req = urllib2.Request(url, data, headers)
    res = urllib2.urlopen(req)
    return res

def get_payload():
    cl = boto3.client("ec2",region_name="ap-northeast-1")
    t = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
    res = cl.describe_spot_price_history( \
        StartTime=t,
        EndTime=t,
        ProductDescriptions=EC2_PRODUCT_DESCRIPTIONS,
        InstanceTypes=EC2_INSTANCE_TYPES,
    )
    payload = []
    now = int(time.time())
    for r in res["SpotPriceHistory"]:
        if r["AvailabilityZone"] == "ap-northeast-1a":
            az = "a"
        elif r["AvailabilityZone"] == "ap-northeast-1c":
            az = "c"
        else:
            raise Exception("Invalid AvailabilityZone")
        payload.append({ \
            "time": now,
            "name": "SpotPrice."+r["InstanceType"].replace(".","-")+"_"+az,
            "value": float(r["SpotPrice"]),
        })
    logger.info("payload length ... "+str(len(payload)))
    return payload

def lambda_handler (event, context):
    payload = get_payload()
    post_mackerel(MACKEREL_APIKEY, MACKEREL_SERVICE, payload)
    logger.info("done!!")

lambdaは、最近追加されたスケジュール実行の機能を使って、↓みたいな感じで5分毎に実行しています。 f:id:intimatemerger:20160116021742p:plain

結果mackerel上のサービスメトリックスに溜まったデータが↓みたいな感じになります。一回、スパイクしていますね…。 f:id:intimatemerger:20160116021901p:plain

今回の投稿は以上になります。ほぼ既存のコード貼っただけですね…。

はやく、LambdaがVPC対応しないかなぁ…。