データ解析のための統計モデリング入門 読書メモ 2章

統計モデリングベイズ統計、確率プログラミングの入門書を読みあさってこの辺がちょっと理解できるようになったので、データ解析のための統計モデリング入門の例題を全てなぞってみようと思います。この文章はその記録です。

2章は、割とサクッと終わりました。種子の数という一変量をポアソン分布に当てはめてみるということをやりました。 最初は、算術平均で、次は最尤推定で。最尤推定はある値lambdaを決めたとき、全ての個体について  p(y_i|\lambda) の積です。 そしてそれが、算術平均と一致することを確認しました。あとは、大数尤度、確率分布の選択について学びました。

という感じです。これは例えば、僕の職務では、レスポンスタイムを対数正規分布に当てはめるみたいなことに利用できそうです。やってみたことはあるんですが、当てはまりが全然良くなりませんでした。これを解決するためには、レスポンスタイムにはばらつきがあるので、そのばらつきを確率分布で表現して加えてあげる必要があるのだろうと、考えています。GLMやGLMMを学んでいくにつれてこの辺の話題も出てくるはず。

Implementing Service Level Objectives 読書メモ

Chapter 1

  • 私達はサービスの世界にいる
    • monitoring as a service , database as a service, etc...
  • 更にマイクロサービスアプローチを採用し機能をサービスに分割している
  • あらゆる物がサービスに見える

    • が何がサービスかを定義するのは難しい
    • 構成要素それぞれも確かにサービスだが全体としてもサービス
    • ECサイトを構築するために利用するIaCもサービスだしECサイトもサービス
  • 信頼性を確保したい、そしてそれを複雑なシステムとともに、あるいは、複雑なシステムの中で実現したい

    • そのために過去とは違う方法で考える必要がある
  • あなたはグローバルに配置されたサービスに対して責任があるかもしれない、あるいは、少しのVMを実行するだけの責任かもしれない
    • それらに共通で言えることとして、それを利用するユーザーが居るはずだ
    • そうであるなら、ユーザーの視点で考える必要がある
  • この最初の章では以下に注目する
    • サービスとそれらのユーザーに関する真実を確立すること
    • SLOがどのように機能するかの、いくつかの要素に関する概要
    • 残りの部分を読むにあたって覚えておいたほうが良いこと

Service Truths

  • いろんなサービスに当てはまることはいくつかあるけど、全部のサービスに当てはまることは3つだけ
  • 1つめ
    • 適切な信頼性は重要な運用要件の1つ
    • サービスの目的はユーザーに十分な信頼性を提供すること
    • 信頼性には可用性以外にも品質・信頼性・応答性など他の多くが含まれる
    • サービスが信頼できるというのは、サービスがユーザーに必要なことをしているということ
      • 信頼性がユーザーにとって十分であること(完璧である必要はない)
  • 2つめ
    • ユーザーに対してどのように動作しているか見えるが信頼できるかどうかを決定する
      • あなたがどうみえるかではない!
    • いかなるメトリック(ログのエラー、何らかの可用性メトリック)がサービスが信頼できることを表していてもユーザーが信頼性があると思わなければ、信頼性はない!
  • 3つめ

    • サービスが完璧である必要はない
  • NOTE

    • ユーザーとはなにか → あなたのサービスに依存するものだ
    • それは人かもしれないし、顧客のソフトウェアかもしれないし、別の内部のサービスかも知れない、ロボットかも

Grafana LokiをAWSで動かす

Grafana LokiはデータストアにS3とDynamoDBを利用することができる。 このおかげで、AWSで動作させるのは非常にかんたんである。

設定の schema_config セクションと storage_config セクションに、それぞれDynamoDBとS3の設定をすればよいだけだ。 基本的には、権限はAssumeRoleで渡すことになると思うので、bucketnameなどを入れるだけである。

schema_config:
  configs:
    - from: 2020-02-14
      store: aws
      object_store: s3
      schema: v11
      index:
        prefix: log-
        period: 168h
storage_config:
  aws:
    s3: s3://ap-northeast-1/$S3
    dynamodbconfig:
      dynamodb: dynamodb://ap-northeast-1

DynamoDBのテーブルはLokiが勝手に作っていくので、 table_manager セクションでスループットや retention や retention を過ぎたら削除するかどうかを設定してあげる。

table_manager:
  retention_deletes_enabled: true
  retention_period: 336h
  index_tables_provisioning:
     provisioned_write_throughput: 1
     provisioned_read_throughput: 1
  chunk_tables_provisioning:
     provisioned_write_throughput: 1
     provisioned_read_throughput: 1

以上で動くには動く。

SSRFをProxyで防御する

SSRF

危険性

<?php
require_once('./htmlpurifier/library/HTMLPurifier.includes.php');
$purifier = new HTMLPurifier();

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$html = curl_exec($ch);

$aws_container_credentials_relative_uri = getenv('AWS_CONTAINER_CREDENTIALS_RELATIVE_URI');
error_log("container_credentials : {$aws_container_credentials_relative_uri}");

echo $purifier->purify($html);

このようなプログラムをECSにデプロイして検証した(SSRF徹底入門 のコードを利用させていただきました)。

当たり前だが、

curl 'http://169.254.170.2:8080/ssrf.php?url=http://169.254.170.2/v2/credentials/x-1111-xxxx-1111-x' | jq .
curl 'http://3137331111:8080/ssrf.php?url=http://2852039170/v2/credentials/x-1111-xxxx-1111-x' | jq .

で普通にトークンが抜ける。下は、IPv4アドレスの10進数表記。これも普通に通る。

{
  "RoleArn": "arn:aws:iam::452509140030:role/ssrf",
  "AccessKeyId": "xxxx",
  "SecretAccessKey": "xxxx",
  "Token": "xxxx",  o            [
  "Expiration": "2020-02-06T12:01:03Z"
}

上のようなJSONトークンが返ってくる。

Fargateにおける危険性

Fargateにおいてクレデンシャルを取得するためのパスは環境変数 $AWS_CONTAINER_CREDENTIALS_RELATIVE_URI に格納されている。 上記のコードではあえて、この値を標準出力にアウトプットしている。

本来なら、環境変数をネットワーク越しに取得できるような脆弱性が作り込まれていない限り、クレデンシャルは抜けない。 Fargateにおいては AWSのクレデンシャルに関しては そんなに気にすることもなさそう。

Proxyによる防御

ユーザーから入力されたURLにアクセスする部分で、そのアクセスをProxy経由にすることで防御する。

Proxyはプライベートアドレスとリンクローカルアドレスへのアクセスを禁止する。

ProxyにはSquidを利用して、設定は下記のようになる。

access_log stdio:/proc/self/fd/1 combined
coredump_dir /var/cache/squid
error_directory /etc/squid/errors

# 宛先
acl localhost_dst dst localhost
acl private       dst 10.0.0.0/8
acl private       dst 172.16.0.0/12
acl private       dst 192.168.0.0/16
    
# ipv4 link local
acl linklocal dst 169.254.0.0/16

# Allow Ports
acl SSL_ports  port 443
acl Safe_ports port 80
acl Safe_ports port 443
acl Safe_ports port 1025-65535
acl CONNECT method CONNECT

# Rule
# deny
http_access deny  !Safe_ports
http_access deny  CONNECT !SSL_ports
# 宛先がローカルアドレスであれば拒否
http_access deny  localhost_dst
# 宛先がプライベートアドレスであれば拒否
http_access deny  private
# 宛先がリンクローカルアドレスであれば拒否
http_access deny  linklocal

# allow
http_access allow localhost
http_access allow all
http_port 0.0.0.0:3128

acl NOCACHE src all
cache deny NOCACHE

防御できているかを検証

ECSに上記したSSRF脆弱性のあるPHPアプリをデプロイし、防御用のProxyをサイドカーで動作させる。

そうした状態で、

  • curl 'http://xxx:8080/ssrf.php?url=http://ifconfig.me'
  • curl 'http://xxx:8080/ssrf.php?url=http://169.254.170.2'
  • curl 'http://xxx:8080/ssrf.php?url=http://2852039170/'

を実行する。最初のURLはリクエストに成功するはずであり、次とその次URLは失敗するはずである。

最初の結果は下記のようになり、リクエストに成功している(TCP_MISSはキャッシュミスしたということ、キャッシュは無効にしているので常にミス)

GET http://ifconfig.me/ HTTP/1.1" 200 517 "-" "-" TCP_MISS:HIER_DIRECT

次のリクエストは、下記のようになり失敗している。ステータスコード 403 が脆弱性のあるPHPアプリに返っているはずである。

GET http://169.254.170.2/ HTTP/1.1" 403 323 "-" "-" TCP_DENIED:HIER_NONE

最後のリクエストは、下記のように失敗している。10進数のIPv4アドレスが解釈され、通常の形式で表示されていることがわかる。

GET http://169.254.170.2/ HTTP/1.1" 403 323 "-" "-" TCP_DENIED:HIER_NONE

まとめ

なんかうまくいきそう。

ディスクフルになる日を予測するスクリプトを書いた

ディスクフルになる日を予測するスクリプトを書いた

ある日、ディスク使用量が閾値に達し、アラートが発生しました。 アラートによって、ディスクの使用率が80%に達した、ということはわかりますが、さて、このアラートにはどのくらい緊急で対応する必要があるのでしょうか?。

ぼくはいつも、この問に答えるためにグラフを眺めて傾きが緩やかだからまだ大丈夫、傾きが急だから急いだほうが良いといった判断をしていました。 これをもうすこし定量的に判断できるようにしたいとおもい、ディスクフルになる日を予測するスクリプトを作成しました。

ディスク使用量の増え方を線形回帰しディスクフルになる日をみつけます。 線形回帰の手法には最小二乗法を利用しました。

どのように実装したかを簡単に説明します。 最初に、ぼくが面倒を見ているサービスでは監視サービスとして mackerel が導入されているので、API でメトリクスを持ってきます。

api_key = os.getenv("MACKEREL_API_KEY")
headers = {"X-Api-Key": api_key}
params = {"name": f"filesystem.{args.partition_name}.used", "from": start, "to": end}

r = requests.get(
    "https://mackerel.io/api/v0/hosts/3d2zGEvSwif/metrics",
    headers=headers,
    params=params
)

次に、最小二乗法で利用できるようにデータを整形します。 x は日付をunixtimeで表現したもの (mackrelからのレスポンスのデフォルトです) とし、y をディスク使用量をGBで表したものにします。

data = r.json()
x = [x["time"]  for x in data["metrics"]]
y = [y["value"]/ 1024 / 1024 /1024 for y in data["metrics"]]

そして、numpy で最小二乗法を計算します。今回は f(x)=ax+b という1次関数で近似します。下記のようなコードになります。

np.polyfit(x, y, 1)

(plofit については ここ を参照してください)

これを関数化し、ディスクフルになる日を、一年後までで検索します。 poly1d という多項式Python の関数として演算可能にするクラスを用います。 下記のようにになります。

val = np.poly1d(np.polyfit(x, y, 1))(x)

さて、以上がうまく動いているかどうかを確認するためグラフにしてみましょう。下記のようなコードです。

plt.figure(num=None, figsize=(10, 4), dpi=80, facecolor="w", edgecolor="k")
plt.scatter(x , y, alpha=0.5, color="blue", s=5)
plt.plot(x, val, label="d=1", color="red")
plt.show

すると、下記のようなグラフとなります。

f:id:happy_siro:20180726190111p:plain

うまく動いていそうですね。

ぼくが知りたいのはディスクフルになる日です。 なので、一年以内にディスクフルになってしまうのかどうかを調べてみましょう。

d = int(time.mktime(time.strptime("2018/7/20", "%Y/%m/%d")))
for i in range(365):
    d = d + 86400
    if np.poly1d(np.polyfit(x, y, 1))(d) > 1024:
        print(np.poly1d(np.polyfit(x, y, 1))(d))
        print(time.strftime('%Y/%m/%d', time.gmtime(d)))
        break;

こうすると、結果は例えば 2018/09/01 などと出ます。 出力された日付により、緊急度高めで頑張るか、のんびりと対応するかが定量的に判断できます。

以上です。

Goでシグナルハンドリングする

自己紹介

GMOペパボでインフラエンジニアをしているmickeyです。 この記事はGMOペパボ Advent Calendar 2017 の12/21の記事です。 最近、Golangに入門しました。ということで、今日は Golang のことを書いていきます。

Golang でシグナルハンドリングする

今、作っているツールでは、シグナルハンドラを実装する必要がありました。C などではシグナルハンドラを書いたことがあるのですが、その経験と比べて Golang は特殊だな、と感じたので、その点についてまとめていきます。

まず、コードを見ましょう。

func main() {
    fin := make(chan bool)

    go func() {

        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT,
            syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)

        t := time.NewTicker(3 * time.Second)

        for {
            select {
            case <-sigCh:
                fin <- true
                return
            case <-t.C:
                log.Println("A")
                // 何かとっても長時間かかる処理
                // シグナルによる割り込みで途中で止めたくない
                time.Sleep(10 * time.Second)
                log.Println("B")
            }
        }
    }()

    <-fin
    log.Print("Interrupted")
    os.Exit(0)
}

特徴的だな、と感じたことを端的に書くと、Golang ではシグナルはチャンネルからやってきます。

sigCh := make(chan os.Signal, 1)

のところで、シグナルを受け取るためのチャンネルを作っています。

signal.Notify(sigCh, syscall.SIGINT,
            syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)

で受け取りたいシグナルを登録し、

select {
            case <-sigCh:
                fin <- true
                return

でシグナルの受信を待ち、受信したらfinをtrueにして終了しています selectしているのはシグナルの受信と、timerからの通知の両方を待ちたかったからです。

selectしないと、sigChからメッセージが来るまで、ブロックしてしまい、timerからのメッセージを待てません。

C なんかだと、シグナルハンドラは登録する、というイメージだったのでGolangの、チャンネルからシグナルがやってくるというところが特徴的に感じました。

まとめ

というような感じで、Golang殆ど書いていないんですが、頑張って実装を進めています。

作っているものが形になってきたら、そちらについてもまとめていきたいなと思います。