まーぽんって誰がつけたの?

iOS→Scala→インフラなおじさん技術メモ

AWS Spot Fleetでインフラ作ったら安すぎたので本当は内緒にしておきたい話

会社で昔発表したやつを汎用的な感じにして残しておきます。

EC2のお値段が性能そのままに8割お安くなりましたという話

Spot Fleet前のAWSのECSでクラスター構成について

  • クラスターのEC2インスタンスはオンデマンド(定価)のautoscaling
  • Docker化でスケールアップ・インが容易に
  • 増強したい場合はautoscalingでクラスタサイズを増やすだけでOK😎

問題点

  • Rolling updateなのでデプロイ時に空き容量が必要
  • 動的ポートマッピングができなかったので1インスタンスに同じサービスが入れない
  • クラスタのインスタンスを多めにしておく必要がある😇

動的ポートマッピング

  • AWSのApplication Load Balancer or Network Load Balncer という新しいタイプのELBならサポートしている
  • terraform module化して雛形作った
  • これで一つのインスタンスに複数のサービスが入れるようになった

EC2インスタンスの価格見直し

  • 今はオンデマンド(定価)で買っている
  • spotインスタンスというオークションのような形式で割安(約80%OFF)で変える仕組み
  • ただしspotインスタンスは入札価格以下になると問答無用でシャットダウン!!
  • spot fleetという複数のスポットインスタンスに入札できる仕組みがある
  • 死んでもすぐに復活しやすいコンテナにはぴったり!!

spot instanceの仕組み

f:id:masato47744:20180124223932p:plain

ここでクイズです。この場合いくらになるでしょう?

f:id:masato47744:20180124224049p:plain

こうなります

f:id:masato47744:20180124224104p:plain

入札してる価格より低かったらそれを払うだけでよい

おこぼれが減ったら

3番目移行の人は問答無用でシャットダウン

f:id:masato47744:20180124224120p:plain

おこぼれがなくなったら

全員シャットダウン!!!

f:id:masato47744:20180124224134p:plain

耐障害性を増やすには

  • 落ちる時には2分前に通知がくるので5秒ごとに見張るスクリプトを追加
  • 落ちたタイミングでコンテナを他のインスタンスに逃す
  • ざっくりこんなイメージの処理内容
while sleep 5; do
 if [ -z $(curl -Isf http://169.254.169.254/latest/meta-data/spot/termination-time)];
 then
   /bin/false
 else
   # draining
 fi
done

分散投資

人気がない安定した銘柄に入札

f:id:masato47744:20180124224217p:plain

人気銘柄

f:id:masato47744:20180124224248p:plain

まだオンデマンドで消耗してるの?

2017年の振り返り

前回1年間の振り返り書いたのは転職前が最後だったのか。っていうか転職して丸3年経ったのか、早い〜。

今年はインフラエンジニアになっていきたいと思ってやっていった1年で去年の自分と比べて新しいことをたくさん覚えられてとても嬉しい。何か新しいことを覚えるということが自分にとってかなりの仕事のモチベーションになるんだというのが32歳になってやっと分かってきた気がする。

1月〜3月

幸か不幸かインフラエンジニアをやっていきたいと思い始めたタイミングでインフラをやってた人が転職することになりその引き継ぎをやることになった。もともとコードはterraformで書かれていて、プロビジョニングもitamaeでレシピが書いてあったのでこの辺を理解しつつ気軽な開発環境が欲しいということで色々勉強しながら 既存のインフラを保守しつつterraformのmoduleを使って環境作れた。このころはAWSのこととかネットワークとか色々分かってなかった頃なので進捗が出せないのを耐え泥水をすすって基礎を理解することに時間をかけた。

4月〜6月

社内でGKEでDeisを導入しようとして整備をしていった。数文字削除しただけだけど、OSSにPR出すとかもできた。

github.com

Deisのことももちろんだけど、AWSとGCPの違いとか、Kubernetesのことを知るきっかけになった。

Kubernetesは調べれば調べるほどすごいすごいとなってLTやってきた。インフラ界隈で初の勉強会登壇できたのでちょっと嬉しかった。

7月〜9月

育休をとるので育休前にドキュメント整備したり普段やってる情シス作業の自動化などをしておいた。

www.mpon.me

GitHubの草がすっぽり空いててあー育休かっていうのと、意外とだいたい何らかのコード書いてたんだなーと思った。

f:id:masato47744:20171231163436p:plain

育休から帰ってきたあとは、社内でリリースされたサービスのあとのインフラとか監視環境を整えた。ECSクラスターをSpot Fleet化したり、module化したり、Datadogのalertを整えたりしてた。

10月〜12月

インフラ環境を整えるのを引き続きやっていっていった。errbitをsentryに切り替えたり、AWS WAF使ってリクエストはじいたり、デプロイ環境を整備したり。

そして行ってみたいと思ってたre:Inventに行くことができた。EKSが出てくれ出てくれと念じながら参加して色々と楽しめた。 re:Invent後はテックブログ1本と、 tech.recruit-mp.co.jp

JAWS-UG コンテナ支部に初参加して発表してきた。インフラ関係で登壇できたのでこれも嬉しかった。

帰ってきてからは意外と色々なまとめに時間がとられてあんまり業務できなかったけどまとめを書くために調べた内容が業務に関連してたのでまぁよかったかなと思う。

あと年末に社内で開かれた社内ISUCONで優勝できてほんとに嬉しかった。正直ほとんど一緒に組んだメンバーのおかげだけど、当時手も足も出なかったこのころから比べて成長を実感できてなんかとても本当に嬉しかった。

社内表彰制度でも表彰してもらえたり今年はなりたいなと思う方向に切り替えていって多少なりとも認めてもらえて嬉しかった。来年は引き続きインフラ関連やっていきたいけど、ソフトウェアエンジニアですと堂々と言えるように何らかのソフトウェアを作って課題を解決できるようなエンジニアになっていきたいと思っている。

Jenkinsfileでビルドするときにこれをつけておくと最高になる

Before & 最高なAfterのJenkinsfileのdiff 🍻

currentBuild.description = に好きな値を設定するだけ ❗️❗️

    stage ("start notification") {
      steps {
        slackSend color: "good", message: "ビルド開始しました"
+        script {
+          currentBuild.description = "ここに好きな値をいれる。例えばビルドのparameterなど"
+        }
      }
    }

ジョブ一覧でさっと情報が見れるようになる

f:id:masato47744:20171127073227p:plain

Jenkinsfileで複数ジョブまたがるやつをコード化できました

Jenkins pipline scriptとは?

昔はpluginで提供されてたみたいだけど、Jenkins2からは標準pluginになったという代物。Groovy DSLと、groovyのscriptでJenkinsおじさんのジョブをコードに落とせます。

Jenkinsfileでジョブを書くときのメリットとデメリットでまとめました。デメリット多そうですがそれ自体は小粒なのと、がんばれば解消できると思うのでメリットの方が大きいかなと思ってます!

今回使いたいと思った理由として、あるパラメータを指定したときだけは、既存のマイグレーションジョブをtriggerして、そうじゃないときはスキップしてほしいというのを実現したかったんですが、shだけじゃできませんでした。 これができるのがJenkins pipline scriptだけなので、これがでかい気がします💯

pros

  • shでは無理だった条件付きでの別ジョブのトリガーやスキップができるようになる!!
  • ステージごとにビルドごとの時間が表示されてCircleCiっぽい見た目になる
  • ジョブの全て(パラメータ設定からビルド後のアクションまで)をコード化できる

cons

  • ヒアドキュメントで複数行のshを書いた時にパイプが使えない・・
    • groovyスクリプトでやるか、sh自体をファイルにしといて実行するだけにする
  • groovyのsandbox環境で実行されるので、自作データクラスとかGroovyのリッチなクラスがそのままだと使えない
    • 設定で権限解放みたいなことすれば使えるみたい(試してない)
  • shの標準出力を受けたらtrimしないと改行が入る
  • Slackのnotifier pluginなどGUIだったらポチポチとチェックボックスつけるだけでいい感じにメッセージ通知してくれたけど、あれを全部自分で書かないといけない・・
    • 誰かが書いてそれを部全体でパクればOK
  • Groovyとしては正しいんだけど、JenkinsのPipilineとしては受け付けないみたいなやつがあるので最初辛い
  • パラメータのデフォルト値を変えるときはリポジトリへのプッシュが必要(画面から上書きできない)

実際のJenkinsfile

ポイントはこの辺ですね。

  • parametersでパラメータ設定ができる
  • stageがビルドの論理的な箱的な感じ
  • whenstageをskipするか設定できる
  • build jobで別jobをtriggerできる
  • postディレクティブでビルド後の処理を書く

下のやつは動かない仮のコードですが雰囲気を味わってもらうことはできるかと思います。

pipeline {
  agent any
  parameters {
    choice(
      choices: "develop\nstaging\nproduction",
      description: "デプロイ先の環境",
      name: "ENV")
    string (
      defaultValue: "",
      description: "ビルドのコミットハッシュ",
      name : "BUILD_VERSION")
  }

  stages {
    stage ("start notification") {
      steps {
        slackSend color: "good", message: "$JOB_NAMEのデプロイが開始されました"
      }
    }
    stage ("migration") {
      when {
        expression {
          // as you like
        }
      }
      steps {
        build job: migration_job,
        parameters: [
          string(name: "ENV", value: ENV),
          string(name: "BUILD_VERSION", value: BUILD_VERSION)
        ]
      }
    }
    stage ("deploy") {
      steps {
        script {
         echo "start deploy"
         slackSend color: "good", message: "$JOB_NAMEのデプロイが完了しました :ok_woman: リビジョン: ${BUILD_VERSION}"
      }
    }
  }
  post {
    success {
      slackSend color: "good", message: "$JOB_NAME [$ENV] SUCCESS - ${currentBuild.displayName} ${BUILD_VERSION}"
    }
    unstable {
      slackSend color: "warning", message: "$JOB_NAME [$ENV] UNSTABLE - ${currentBuild.displayName} ${BUILD_VERSION}"
    }
    failure {
      slackSend color: "danger", message: "$JOB_NAME [$ENV] FAILURE - ${currentBuild.displayName} ${BUILD_VERSION}"
    }
  }
}

Datadogの効果的なモニタリングとAlertについてシリーズを読んだ

狼少年

少年が繰り返し同じ嘘をついたので、本当に狼が現れた時には大人たちは信用せず、誰も助けに来なかった。そして村の羊は全て狼に食べられてしまい、死亡した。 Wikipedia

サーバーの監視でalertやwarningが増えすぎると、 だんだん暗黙の了解みたいのが入ってきてこんな状態になってしまうことはないでしょうか? 🤔

  • 最近の人 「なんかwarnがいっぱい出てるけど大丈夫でしょうか?」
  • 昔からいる人 「あーこれは大丈夫なやつ」

ぼくはどっちかというとそういう暗黙の了解側にいてしまうので、モニタリングというのは何をするべきなのかというのを勉強とまではいかないけど、Datadogが出しているMonitoring 101というシリーズ記事があるのでそれを読んでみみた。

  1. Collecting the right data
  2. Alerting on what matters
  3. Investigating performance issues

f:id:masato47744:20171118023127p:plain

alertすべきはWork metrics

metricsの種類

metricsには、Work metricsResource metricsEventに分かれる。

Work metrics

Work metricsはシステムが正常に動いてるかどうかを判断できるメトリクスのこと。Work Metricsはさらに4つに分かれていて、throughput, success, error, performanceとなる。 例えばウェブサーバーのWork metricsはこういう値

f:id:masato47744:20171114165101p:plain

DBだったらこう

f:id:masato47744:20171114165127p:plain

Resource metrics

Resource metricsはDisk IOやCPU使用率などのメトリクスの事を指す。同様にutilizationsaturationerrorsavailabilityの4つに分かれる。 ついついぼくが設定してしまっていたのは、CPU使用率が90%overです。みたいなalert。これは実はalertとしては筋が悪い。別にCPU使用率が高くてもレスポンスタイムなどが遅くなっていなければ問題ないのだ。

Event

eventはPRがmergeされたとか、deployされたとかバッチが失敗したとかそういう出来事のことを指す。

効果的なモニタリングとAlert

Alertは緊急度によって3種類に分けられる。

  • 夜だろうがなんだろうがすぐに対応しないといけないPage
  • すぐではないけどいずれやらないといけないNotification
  • とりあえず記録しておいて通知はしなくてよいOKなRecord

という分類。

そして、Pageに分類されるものとして通知をするときに、本当にすぐに対応しなければならないのか、緊急度がとても高いものかどうかということを考える。 ちなみにPagerが英語でポケベルみたいなもののことを指すので、多分Pageっていうんだと思う。

なので効果的なAlert、モニタリングとしては以下のようなやり方になる。

  • 緊急度が高いWork Metricsが想定外の値であればPage(電話やSlack通知)
  • 残りディスク容量が少ないなどのResource MetricsはNotification(Warning的なSlack通知)
  • Pageの場合はいったい何が起きているのかをResource MetricsやEventを使って調査していく

Pageに該当するものはなんだろう

この記事で紹介されている例

f:id:masato47744:20171114165143p:plain

実際の現場でいうと、このあたりになるのかなと考えてみた。本当に失敗してはいけないバッチとかって今ってjenkinsの結果見るだけしかやってなくて、それだと埋もれてしまうのでそういうのも本来は別途通知しないといけないなと思った。

  • 外形監視でのヘルスチェックに失敗した
  • LBのヘルスチェック成功しているインスタンスの数が0になった
  • LBのレスポンスタイムが平均xxxms以上になっていないか
  • サーバーのHTTPのステータスコードの5xx系の全体に占める割合がxx%を超えていないか
  • バッチが失敗した

ECSの概念を理解しよう

※ 追記
結構ちょこちょこブクマしてもらっているので意外と需要あるのかな。
もし、記事見て分からないところあったら
Twitterなり気軽に質問してもらって大丈夫です!!

社内でインフラエンジニア増やしたいなと思ってECSの概念を理解してもらおうと思って書いたやつです。

問題を間に挟みつつ理解の手助けになればいいなと思ってます。

今更ですがDockerとは?

分かってる人はもううんざりかもしれませんが、一応復習。ECSの概念を理解するのに必要なコンテナを起動すると何が起きてるのかを再確認します。

普通の仮想サーバー

普通の仮想サーバーの場合は、sshでログインして、yumみたいなパッケージ管理システムでinstall、サービスをデーモンでバックグラウンドで起動しておくという感じですね。

f:id:masato47744:20171114163700p:plain

Dockerの場合

Dockerはコンテナという単位で何かのプロセスを起動する仕組みです。基本的には、1コンテナで1プロセスだけが動いていると思っていてください。 あるマシンの上でDocker Daemonが起動していて、そこに命令することでコンテナを起動することができます。

f:id:masato47744:20171114163712p:plain

Dockerが普通の仮想サーバーと違うのは、こんな風に仮想サーバー上でnginxサービスをデーモンで起動しておくのではなく、1つのプロセスとしてコンテナを起動しておくというところです。 この図のnginxコンテナは最後にnginxをスタートさせてそのプロセスがフォアグランドで起動しつづけています。それをdocker daemonがデーモンとして動かしているという感じです。

問題

Dockerコンテナで最後に、tail /var/log/bootstarp.logを実行するコンテナ、tail -f /var/log/bootstrap.logを実行するコンテナをdaemonモードで動かしたときにそれぞれどのような挙動になるでしょうか?具体的に言うと、

-fがついてないやつ。

FROM ubuntu

CMD ["tail", "/var/log/bootstrap.log"]

-fがついてるやつ。

FROM ubuntu

CMD ["tail", "-f", "/var/log/bootstrap.log"]

これらを、 docker run -d したときに該当のコンテナは生きてるでしょうか?死んでるでしょうか?

答えは動かして確認してみましょう。

普通のインフラの構成を見てみよう

ECSの構成に行く前に、普通のインスタンスでサーバー動かしてたころのやつってどうなってるんだというのを見ておきましょう。 例えば、 https://www.example.com っていうただ単にnginxが動いてるサーバーがあったとして、その場合はDNSがロードバランサーの名前に変換して、ロードバランサーがyum installしたnginxがデーモンで起動しているEC2インスタンスにリクエストを割り振って、レスポンスをそこからもらうっていう流れです。 Tips: 443がLBで80に変えてあるのはSSL終端(ssl terminate)と呼ばれてます。

f:id:masato47744:20171114164034p:plain

これを1台のEC2でdockerコンテナでやるとしたら?

ここで賢い人々は、よし、Dockerはコンテナがたくさんあげれるからお金を減らすためにEC2インスタンスを1台にしよう!と考えます。それを図にするとこうなります。

f:id:masato47744:20171114164203p:plain

そう、あえて意図的に赤くしましたが、同じインスタンスなので、ポートを変えてあげないといけないのです。あと、Dockerにはsudo権限みたいのを渡しておかないと80みたいなwell-known portは使わせてもらえないかも。(ここは曖昧だけどあんまり本質ではないので気にしないでください)

  • 要は、portを管理しないといけないってこと。ロードバランサーとコンテナのポートのひもづけはやらないといけない。
  • せっかくDokcerでimmutableにしたのに、結局はSSHログインしないといけないこと(Dockerデーモンに命令を出さないといけない)
  • これ、今インスタンス1台だからいいけど、増えたらどうすんの。辛くない?
  • コンテナ死んだらどうする?sshログインしてまたdocker起動しますか?

はい、これでECSのようなオーケストレーションツールが必要になってくるという訳です。

ECSの仕組み

ECSは正式名称はAmazon EC2 Container Serviceです。EC2というのが入ってるので、EC2をベースとしているのです。 ※ いつのまにか名前が変わってElastic Container Serviceになってました AWSのEC2を使って、クラスタ構成を作ってくれて、ロードバランサーのポートマッピングなどをやってくれて、コンテナの生死監視と自動復旧などをやってくれます。さっきあげた問題点が全て解決してますね。

じゃあ、ECSが内部的にはそれをどうやって実現しているか見てみましょう。結構単純です。

コンテナインスタンス

AWSのEC2のクラスタ構成を作ってくれる部分が最初ぼくは謎だったんですが、分かるとなんだそんな単純なことかってなります。 AWSのコンソール上から見えるClusterという概念。これは複数のEC2インスタンスを取りまとめてるだけなんですが、それをどうやって実現してるんでしょうか。 それは、各EC2インスタンスに、ecs-agentというdockerのコンテナを起動してるだけなんです!また、そのagentはOSSで公開されてます

f:id:masato47744:20171114164228p:plain

要はさっきの説明でnginxとか起動させてましたがそれと同列でecs-agentっていうのをコンテナとして起動してconfにcluster名を渡してあげるだけです。それだけで、人間は、クラスタというまとまった単位で管理できる訳です。

実際は、手動でインスタンスを起動してそこにecs-agentを入れてみたいなことはせずに、autoscaling groupというAWSの機能を使ってガッとインスタンスをたてて、そのときの起動スクリプトでecs-agentを動かします。

どうでしょう?簡単じゃないですか??

taskとは?

clusterが起動する理屈はわかりました。では、今度は、コンテナがどう起動しているかを見ていきます。これは、社内メンバーのまとめで気づいたんですが、ECSでは単にコンテナを起動して終了させるだけなら、TASKという概念でまかなえるのです。 Serviceという概念があることを知ってる人もいると思いますが、ここでは、単にTaskが何なのかを見ていきましょう。

f:id:masato47744:20171114164246p:plain

さっきDockerの問題のところでやったTailするだけの例に出てきてもらいましょう。tailだけを実行するコンテナを実行するための情報のことをTask Definitionといい、それをもとに、ECSがコンテナを起動してくれます。起動されたコンテナのことはTaskと呼ばれます。 Taskの場合は、コンテナが死んでようが生きていようが気にせずに、単にコンテナを動かすだけになります。この場合は、コンテナはtailコマンドを実行したあとすぐにkillされてしまいます。これがTaskの一生です。 この1回起動したら、死んでよいというのは、バッチのジョブなどに向いています。

Serviceとは?

ECSを構成する概念の一つです。 オーケストレーションツールなしでDockerだけで運用する場合の問題としてコンテナの死活管理がありましたが、それを解決するものです。

通常のWebサービス等でECSを使う場合は、下記の理由でServiceを使うことになります。

  • 基本起動しっぱなしでリクエストを待ち受けて欲しい
  • 死んだら再起動してほしい
  • 外部からの接続を受けてポートマッピングして欲しい

図に表すとこうなります。リクエストがきて、ロードバランサーで各コンテナにリクエストが振られるところまでは基本的に何も変わりがありません。ただ、各コンテナがServiceというくくりでまとめられているところが違います。

f:id:masato47744:20171114164258p:plain

では、Serviceのすごいところを説明していきます。 まず、Serviceというか、ALBというApplication Load Balancerのすごいところでもあるんですが、ポートを動的にマッピングしてくれます。 さっきのDockerの例だと、同じインスタンス内でポートがかぶらないようにと人間が気を使って設定していましたが、それはやる必要がなくサービスがやってくれます。

次に、desired count=4というところです。これはどういうことかというと、コンテナ4つたててくれーって指定しておいたら、コンテナが死んだとしても自動的に復旧してくれます!desiredとは人間が要望している数ですね。それに従って機械が何度でも復活させてくれる訳です。

もう一つ、これは完全にそうとは言えないんですが、ある程度賢くコンテナを分散して配置してくれます。この部分は設定をがんばったりしないといけないので、完璧とは言えない部分ですが、そういうこともやってくれるという訳です。

問題

ここまでを踏まえて、以下のことをやってみましょう。

  • 手動でインスタンスをたちあげてECSクラスターをたててみる
  • autoscaling groupでECSクラスターをたててみる
  • 何かサービスを動かしてみる

ECSのlogdriverにawslogsを指定した場合はawslogs-stream-prefixをつけたほうがいい

awslogs-stream-prefixをつけない場合

log stream名が "${docker psのCONTAINER ID}${randomな文字列}" となる。

f:id:masato47744:20171114161600p:plain

これだと、例えば、あるコンテナのログだけ見たいっていう場合に、sshログインして、docker psしないといけない😱

💯 awslogs-stream-prefixをつける場合

なんでもいいからprefixを追加するだけで、なぜか、急に顧客が本当に欲しかったStream名が手に入る。 "${awslogs-stream-prefix}/${container name}/${ecs-task-id}"

これはawslogs-stream-prefixにplayという文字列を設定した場合の例。

f:id:masato47744:20171114161621p:plain

これでどのコンテナのログか見ることが簡単にできる。なので、とりあえず、awslogs-stream-prefixつけておいた方がいい!! prefixなしのときも、この命名規則で作ってくれよとは思う。

参考: http://qiita.com/bohebohechan/items/8943786929ab5833d2a8

プロダクションレディマイクロサービスを読んでハッとさせられた

とにかくハッとさせられた

この本を読んで、マイクロサービスとか関係なく、社内のインフラもまだまだやれることあるなー、というかこの本に書いてあるレベルにしなければという気になった。

最初、雑にマイクロサービスってこういうものだよーみたいな本かと思って読み始めてみたけど全然違っていた。マイクロサービスを本番で使えるレベルにするにはどうなってないといけないかという本だった。

どんな本?

UberのSREの人が書いた本。その人が1000以上あるマイクロサービスを標準化するために8つの原則を考え出してそれについてそれぞれ語られていくみたいな話。組織論みたいなところまで話がいくのでコードはでてきません。どっちかっていうとプロセスに重きをおいた抽象的な感じ。

なので逆に言えば、マイクロサービスじゃないから関係ないやという感じではなくたとえEC2のインスタンスで運用してようとためになる話だなと思った。 個人的には、Uberに1000以上もマイクロサービスがある気がしないんだよな〜🤔 (Googleが1000以上あるって言うならならなんとなく分かる)

きっと、自分が思ってるマイクロサービスの粒度よりもすごく小さいか、それか本当にいろいろなサービスがあるんだろうな。

8つの原則って??

  1. 安定性
  2. 信頼性
  3. スケーラビリティ
  4. 耐障害性
  5. パフォーマンス
  6. 監視
  7. ドキュメント
  8. 大惨事(カタストロフィ)対応

これらの基準を満たしたサービスのことをプロダクションレディと表現している。

第1章はモノリシックなサービスを比較対象にしつつ、マイクロサービスとはなんぞやを説明してくれています。 で、その後の章で8つの原則について掘り下げつつプロダクションレディにするためのチェックポイントを書いていってくれています。

やっていき😤

他社のすごい事例とか見たり、自分が思いついたようなことは既に2〜3年前ぐらいから考えられていたことだったりして、今さらがんばってもしょうがないのかもしれないと弱気になることもある。 でも、立ち止まらずに一歩ずつ進んでいこう、目新しくなくたっていいから基本的なことをちゃんとやっていこうという前向きな気持ちになった。

もっと開発のtry and errorを素早く試せる環境とか、リリースまでのスピードをあげたり、品質を高くしたり、何をどうすればいいかすぐには分からないけど、そんな感じのことをやっていきたいという気になれた。

Cloudwatch Logsが簡単便利安いだった

事の発端

Railsのlogrotateされたあとのファイルをどこに保存しようかと迷ったのがきっかけでした。 fluentdなどでS3に保存していくとか作るかーと思ってたんですが、社内のメンバーに相談してみたところAWSのCloudwatch Logsで簡単にできるよとのことで、調べてみたら本当に簡単便利安いだった。

インストールと設定

  • installはyum install awslogsでさらっとできる
  • 設定もあんま深く考えなければこれだけでできる
[/u/apps/log/production.log]
datetime_format = %Y-%m-%dT%H:%M:%S
file = /u/apps/log/production.log
buffer_duration = 5000
log_stream_name = {hostname}
initial_position = start_of_file
log_group_name = /u/apps/log/production.log

ロググループとログストリーム

これの概念がとてもいい感じで、同じ設定ファイルでもログストリームを分けることでホストごととかで分類できるようになる。

ロググループは同じなんだけど、log_stream_name = {hostname}という設定を入れているので勝手にホストごとに分かれてくれる。便利。

検索もさらっとできる

S3に入れていたらAthenaとかでやるのかなーめんどうそうだなーとか色々考えてたけど、文字入れたり、日付でフィルターしたり、結果もすぐ返ってくる。 cloudwatch logsは昔はこんな簡単に検索できなかったそうな。本当いい時代に生まれました。

f:id:masato47744:20171110004307p:plain

ECSのログドライバーにもできる

ECSのコンテナのログってlogdriverに設定したところに送られるようにできるんだけど、fluentdで集めてElasticsearchに送ってKibanaで見るとかそういうの作るのも一手間あってちょっとめんどいなーと思ってたけど、Cloudwatch logsにも送ることもできる。

なので、さくっとログ確認したいなー用途はこれでいいかも。

お値段

取り込みに$0.76/G、保存に$0.033/Gなので、月間100Gあったとしたら(0.76+0.033)*100G$79.3なので1万円弱

保存できて検索までできてこのお値段!!

fluentdのS3 output pluginでは権限に注意しよう

現象

fluentdが集めてきたログをS3にputしていたつもりが、全体のうちの1/3ぐらいしかputされずそれ以外がロストしていたという話です。

調べてみると・・エラー吐いてた

fluentdのヘルスチェックとかは確認していて、fluentdが死んでいる様子はない。 S3にもログ自体は吐かれている、冗長化している片方のインスタンスが死んでいるわけでもない。 ということですぐに気づけませんでした。

fluentdがエラーを吐いてたので見てみるとS3の権限っぽいエラーが。

2017-04-21 12:50:46 +0000 [warn]: temporarily failed to flush the buffer. next_retry=2017-04-21 12:50:48 +0000 error_class="Aws::S3::Errors::Forbi
dden" error="" plugin_id="object:2b1bacaec3f4"
  2017-04-21 12:50:46 +0000 [warn]: suppressed same stacktrace

Aws::S3::Errors::Forbiddenというエラー内容からIAMとかBucket Policyだとピンときて確認してみるも、PutObject権限はきちんとついていて路頭に迷うことに。

Aws::S3::Errors::Forbiddenでググりまくると・・

https://groups.google.com/forum/#!topic/fluentd/Vvok7FTVuq4 このページの真ん中あたりのこの部分がヒントになった。

If an IAM user has permissions to the bucket but doesn't have !AM permissions to s3 that user can write files but @bucket.objects[s3path].exists? will fail with 'Permission Denied' when true but not when false (Weird).

fluentdがs3にbufferをs3にwriteしに行く際に、ある特定の区切りの時間のオブジェクトはまだ存在しないので書き込みできる。

で、そのあと同じs3のファイルに対して一度existsを確認してから追記しにいくんだろうけど、このexistsで失敗していると思われる。

実際に、現状のIAMロールはs3:*に対してAllowされてるけど、bucket policyはs3:PutObjectのみになっていた!!!!

fluent-pluginの該当コード部分

pluginのこの該当コード https://github.com/fluent/fluent-plugin-s3/blob/master/lib/fluent/plugin/out_s3.rb#L237 のwhile条件を抜けてしまう。

        end while @bucket.object(s3path).exists?

なので、1回目は書き込みにいけるが、その後追記しにいくときに失敗する。

1/3ぐらいしか残っていないというのも、残ってるのは特定の時間で新規でwriteした分で、本来であれば残りのログもそのobjectに対してwirteしにいけたが、bucketのポリシーが足りないためexists確認ができずに、失敗してしまったと思われる。

対応

バージョンによるみたいですがv0.8.0からはcheck_bucketcheck_objectのオプションが追加されていて、それだとs3:PutObjectだけでいけるみたいです。

Example when check_bucket=false and check_object=false When the mentioned configuration will be made, fluentd will work with the minimum IAM poilcy, like: "Statement": [{ "Effect": "Allow", "Action": "s3:PutObject", "Resource": ["*"] }] https://github.com/fluent/fluent-plugin-s3

Release 0.8.0 - 2016/12/20

まとめ

最初動いたからといって油断してはいけない

Rails5とpumaを使ったときのlogrotateでハマった話

現象

f:id:masato47744:20171110000311p:plain

普通Railsのアクセスログってproduction.logに出力されますが、出力されないことがあったという怪談の話です。

対応を1行でまとめると、Puma使うときはRails5からはRAILS_LOG_TO_STDOUTを設定しようということです。

👇 ここからは怪談の詳細です

原因1: logrotateの後処理の実行タイミングが悪かった

開発環境で確認してみたところ、logがログローテートされたあとファイル名の後ろにyyyy-mm-ddが付与されますが、 そのファイルに対して引きつづきログが出力されつづけていたのです🙀

で、既存のログローテートでこんな設定が書いてありました。

lastaction
    puma_pid=/path/to/puma.pid
    test -s $puma_pid && /usr/bin/kill -HUP -U root "$(cat $puma_pid)" > /dev/null 2>&1 || :
endscript

ざっくり言うと、lastactionなのでgzに圧縮されたタイミングで、pumaのプロセスにSIGHUPシグナルを送るという処理が行われてます。

logrotateスクリプトの調査これを参考にするとlastactionはgzに圧縮されたタイミングなので、こういうことになっていたんでしょう。

今回はpostrotateが適切と考えたので変更しました。

-    lastaction
+    sharedscripts
+    postrotate

原因2: pumaとシグナルの関係

さっきのlastactionでHUPのシグナルを送っています。このシグナルはpumaにとってどんなことを意味するんでしょうか。 signals.md | puma/pumaを見ると、

HUP reopen log files defined in stdout_redirect configuration parameter

stdout_redirect設定したファイルに対してログファイルをreopenしますと書いてあります。

なんかよくわからないけどログをreopenしてくれるんだろう☺️ めでたしめでたしと思って手動でkillコマンドでHUPを送るもreopenされない・・・

よくよく調べてみると、pumaのstdout_redirectの設定はcapistrano-pumaのデフォルトを使ってるんですが、対象はpuma_access.logでした。 なので、HUPを送ってもrailsのlogではなくpuma_access.logをreopenするだけなのです😢

logrotate後にRailsのlogの場所が分からなくなったpuma君がRailsのログを書き込めなくなってたということでした。

Rails5 + pumaの組み合わせではRAILS_LOG_TO_STDOUTstdout_redirectを使おう

RAILS_LOG_TO_STDOUTなしの場合、workerプロセスのloggerがそれぞれproduction.logをつかみます。 pumaにworkerのloggerの出力先を書き換える機能がないのでUSR2 or USR1シグナルでworkerをまるごと再起動しないと、古いログファイルを掴んだままになります。

pumaのstdout_redirectproduction.logを指定しているとき、workerの標準出力の向き先がproduction.logになります。 RAILS_LOG_TO_STDOUTを設定しておくと、loggerは標準出力にログを出力するので、標準出力の向き先のproduction.logにログが出力されます。 HUPシグナルを送ると、この標準出力の向き先だけが書き換わります。workerをまるごと再起動する必要はありません。

実際の設定方法

PumaとRails5で特に何も設定しないと、Railsのloggerは直接log/production.logを開くようになってます。 以下の設定をすることで変更することができます。

RailsでRAILS_LOG_TO_STDOUTの設定

環境変数RAILS_LOG_TO_STDOUTを指定するとログをSTDOUTに吐くようになります。

  if ENV["RAILS_LOG_TO_STDOUT"].present?
    logger           = ActiveSupport::Logger.new(STDOUT)
    logger.formatter = config.log_formatter
    config.logger = ActiveSupport::TaggedLogging.new(logger)
  end

pumaを使って標準出力をファイルに書き出すstdout_redirectの設定

pumaの場合stdout_redirectオプションを使うとworkerの標準出力を指定したファイルに向けます。

まとめ

なんとなく誰かがいい感じに設定してくれていたlogrotate。 きっとうまくいってると思って見過ごしてたけど奥が深いなと感じました。

GASでconstの挙動がおかしいので使わない方がよさそう

GASだとconstのスコープが違う?

最近のJavascriptとかTypeScript界隈では、varletconstが変数の宣言として使えて、scalaとかswift脳からするとなるべく再代入は避けたいのでconstを使いたい。

例えばfor文内で一時的な変数を宣言して利用しようとした場合こんなコードがあったとして、

function test() {
  
  for (var i = 0; i < 5; i++) {
    const k = i * 2;
    var m = i * 2;
    Logger.log('k = ' + k + ', m = ' + m);
  }
  
}

kもmもiが2倍された値が表示されるはずなんだけど、constで宣言した方は0のままになってしまう。一度代入されたらそれ以降変更されないようだ。グローバルスコープっぽい動き。

f:id:masato47744:20171109235828p:plain

なので、GASでconstを使う場合は、本当に一度でも代入したら変更しない定数にのみ使う方がよい。というかあんまり使わない方がいいかも。

node.jsでやってみるとちゃんとconstに再代入されてる

これが想定していた挙動

f:id:masato47744:20171109235851p:plain

AWSのRoute53でpublic/private zoneの名前のつけ方に注意

Route53のzoneの名前を同じにしていると・・

Route53では、外部公開用のpublic zoneとVPC内に閉じたprivate zoneで、それぞれレコードを登録することができます。 しかし、public/privateなzoneでdomain nameを同じにするとpublic zoneの名前が引けなくなってしまいます。

例としてこんな設定のときに、

zone type VPC
mpon.me. Public
mpon.me. Private 開発用VPC

次のようにPublicなゾーンにレコードを登録していたとします。

zone record
Public dev-www

これで、例えば、開発用VPCにいるEC2インスタンスなどからdev-wwwの 名前を引こうと思ってもひけなくなってしまいます。

対処法としてprivate zoneにも同じ内容のDNSレコードを登録しました。

zone record
Public dev-www
Private dev-www

これからインフラを作ることがある人は

zoneを作るときに、Domain nameを以下のように変えればOKです。かぶらないようにしましょう!

Domain name type
mpon.me. Public
mpon.internal Private

Datadogでいい感じにAlertの通知を出し分けるやり方

出しわけたいユースケース

例えば、GCPの2つのprojectからCloud SQLの複数のDatabaseのmetricsが送られてくる場合。 project Aの場合は、こっちのチャンネルに通知、project Bの場合は、そっちのチャンネルに通知したいみたいなときに有効です。

metricsの設定方法

まずは、出しわけたいグループごとに、 avg by 部分に設定していく。

f:id:masato47744:20171109015939p:plain

こうすると、project_idごと、database_idにmetricsが分かれて表示されて、個別にalertしてくれるようになる。

メッセージ部分の設定方法

template部分で、#is_matchを使うと、if文みたいなことが書けます。 これとタグの値を組み合わせれば出し分けたりができるわけですが、 このときに注意が必要なのが、グルーピングに利用したタグの値しか利用できませんでした。 利用できる変数は{{を入力すると予測変換が出てくるので、そこに表示されてなければ利用できないと思った方がよいです。

公式ページを見る感じだと、タグを使えますって書いてあるからなんでもいいかと思ってたけどそうじゃなかったでした。

ここで言えば、avg byで指定した、project_id.nameとdatabase_id.nameが条件として利用できます。nameまでつけるのがポイント。

crondはtimezone変えただけだと反映されない

cronはUTCのまま

JSTにタイムゾーン変更したときに、crondをrestartしないと実行がUTCのままでした。 service crond restart で反映しました。

参考: Timezoneを設定したらcronの実行時間がずれる

EC2の初期スクリプトとかでrebootいる?

AWSのEC2のuser data等で初期スクリプトで設定すると思うけど、それは大丈夫なのかなとふと思いました。 もろもろやったあと、rebootしてるからたまたま大丈夫なのかやらなくても大丈夫なのかちょっと気になりました。

Linux インスタンスの時刻の設定 にもrestart必要と書いてあった。

システムを再起動し、すべてのサーバーとアプリケーションで新しい時間帯情報を取得します。