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殆ど書いていないんですが、頑張って実装を進めています。

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

Packer で OpenStack のイメージを作ってみた

Packer で OpenStack のイメージを作ってみた

Packer で OpenStack のイメージを作ってみたので,その知見を共有します。

Packer とは

かんたんに, Packer について説明します。 Packer とは, Json で記述された template を元に,クラウドなどで動作するイメージを作ってくれる子です。

Packer のために,記述する Template には Builder と Provisioner というセクションがあります。 それぞれについて,説明していきます。

Builder

これは,イメージを作成するために,どんな環境を,どのように利用するのかを指定します。 環境には, OpenStack や Amazon EC2, Docker などがあります。 そして,それぞれの環境にどのようにアクセスするのかを指定します。

Provisioner

これは,作成したイメージにプロビジョニングするための設定を書くセクションです Puppet や Chef 等を利用できます。

作ってみるぞ!

それでは,上で説明した事を利用して,イメージを実際に作っていきます。 利用したコードは,下記のようになります。

{
  "builders": [
    {
      "type": "openstack",
      "flavor": "m1.small",
      "networks": "xxxxxxxxxxxxxxxxx",
      "ssh_interface": "eth0",
      "floating_ip" : "xxxxxxxxxxxxxxxxx",
      "security_groups": ["xxxxxxxxxxxxxxxxx", 
                          "xxxxxxxxxxxxxxxxx"],
      "availability_zone": "xxxx",
      "ssh_username": "centos",
      "image_name": "{{user `role`}}_centos7_{{user `date`}}",
      "source_image": "xxxxxxxxxxxxxxxxx",
      "user_data_file": "user_data"
    }

  ],

  "provisioners": [
    {
      "type": "puppet-server",
      "puppet_bin_dir": "/opt/puppetlabs/bin/",
      "puppet_server": "puppetserver.xxx.com",
      "options": "--test --certname={{uuid}} --environment={{user `environment`}}",
      "facter": {
         "role": "{{user `role`}}"
      }
    }
  ]
}

Builder セクションの type に OpenStack を指定すると,source_image に指定したイメージで,OpenStackにインスタンスが起動します。 その後,起動したインスタンスに対して, SSH でログインし, Provisioner セクションで指定した方法でプロビジョニングしていきます。

ですので, Builder セクションでは, Packer を利用した場所から SSH ができるような設定が必用で, ssh_username や ssh_interface , security_groups などがその設定に当たります。

Provisioner セクションでは, puppet を利用するために必用な設定を行います。

注意事項として, Provisioner ではプロビジョニングに利用するツールをインストールしてくれるわけではないので,もととなるイメージに Puppet が含まれているか, cloud-init などでインストールする必要があります。

今回は cloud-init でインストールしました。

#cloud-config
bootcmd:
    - rpm -Uvh https://yum.puppetlabs.com/puppetlabs-release-pc1-el-7.noarch.rpm
    - yum install -y puppet-agent

また, Packer には, User Variable という機能があって,コマンドライン引数や,環境変数から Packer templateに情報を渡すことができます。

今回は,コマンドライン引数から渡したいので

 "options": "--test --certname={{uuid}} --environment={{user `environment`}}",

の用に書いています。

packer build -var 'role=xxx' -var 'environment=xxx' -var \
       'date=`date +"%Y-%m-%d-%H-%M-%S"`' template.json

などのように実行すると,コマンドライン引数で指定した値が {{user environment}} などのようにかれた箇所で利用されます。

{{uuid}}

にはランダムな値が入ります。 こうしておかないと, 同じ証明書が Puppet で使いまわされることになり,Puppet で怒られます。


幾つか, Packer Template の記法で悩むことはありましたが,割とシュッとできたな,という所感です。 今は,最低限のプロビジョニングが施された,基本となるイメージしか作成できていませんが,Role ごとにプロビジョニングされたイメージを作ることで スケールアウトのために時間が短縮できたらな,と考えています。

以上です。

mruby-maxminddb という mgem を作った話

このブログは mruby Advent Calendar 2016 の 21 日めの記事です。

最近,と言っても少し前ですが,mgem を作りました.

作った mgem の名前は mruby-maxminddb って言います.

GioIP の DB である maxminddb を読み書きする Gem です.

MaxMindDbDat = "/tmp/GeoLite2-City.mmdb"
IPAddr = '8.8.8.8'

maxminddb = MaxMindDB.new MaxMindDbDat

maxminddb.lookup_string IPAddr

maxminddb.country_code       #=> US
maxminddb.region             #=> CA
maxminddb.region_name        #=> California
maxminddb.city               #=> Mountain View
maxminddb.postal_code        #=> 94035
maxminddb.latitude           #=> 37.386
maxminddb.longitude.round(4) #=> -122.0838
maxminddb.metro_code         #=> 807
maxminddb.time_zone          #=> America/Los_Angeles

などのように利用します。

例えば,Webサーバーやメールサーバーのスパム対策に利用できるかと思います。

幸いにも maxminddb を利用するための C 言語のライブラリがあったので,こちらをラップする形で実装していきました。 libmaxmind と言います。

libmaxmind はドキュメントが充実しており,それらを参考に実装していきました。

また, maxminddb をダンプするための mmdblookup というツールも同梱されており,そちらのコードはとても参考になりました。 ほぼ,このツールの動作を模倣する形で実装を進めていきました。

static mrb_value mrb_maxminddb_lookup_string(mrb_state *mrb, mrb_value self) {
  mrb_maxminddb_data *data = DATA_PTR(self);
  char *ipaddr = NULL;

  mrb_get_args(mrb, "z", &ipaddr);
  data->host = ipaddr;

  data->lookup_result_s =
      MMDB_lookup_string(&(data->mmdb), data->host, &(data->gai_error),
                         &(data->mmdb_lookup_error));

  if (0 != data->gai_error) {
    mrb_raise(mrb, E_RUNTIME_ERROR, gai_strerror(data->gai_error));
  }

  if (MMDB_SUCCESS != data->mmdb_lookup_error) {
    mrb_raise(mrb, E_RUNTIME_ERROR, "Lookup Error");
  }

  return self;
}

例えば,上記のようなコードは, mmdblookup.c の似た関数を模倣して実装しています。

mgem として実装するので,エラーが発生したときに mrb_raise を返すところや, 構造体に MMDB_lookup_string の戻り値を格納しているところが違うぐらいです。

その構造体は mruby で C 言語の構造体をラップしたオブジェクトを作る正しい方法 を参考にして mruby のオブジェクトとしてラップしています。

mruby-maxminddb は初めて C 言語で mgem を作るには手頃なお題でした。 libmaxminddb というライブラリもあり,ドキュメントも充実しており,参考にできるコードも同梱されていました。

mgem を作るにあたって参考になるブログもありました。

来年も,幾つか mgem を開発していきたいと思います。

以上です。

障害対応 ~ あるいはポケットに手を入れて走ることの危険性について ~

こんにちは, GMO ペパボの @mickey と申します。 この記事は,Pepabo Advent Calendar 2016 の 5 日目の記事になります。

さて,僕は GMO ペパボでインフラエンジニアをやっています。 インフラエンジニアの重要な仕事の1つに障害対応があります。

そこで,先日の障害対応について書いていこうと思います。

障害が起きたのは自分です。

チームでの楽しい飲み会の後,僕は終電に向けて走っておりました。 原因については,定かではありませんが,気がついたら僕の顔は地面に衝突していました。 ポケットに手を突っ込んでいたため,顔から地面に突っ込んでしまいました。

この記事では,自分に起きた障害,怪我,に対して僕がどのように対処したのかを記述していこうと思います。

これからの忘年会シーズンで同じような事象に遭遇する方々もいらっしゃるかと思います。

体験を記す事で,似たような体験をした方々にスムーズに治療を受けてもらうことができればと思います。

さて,顔から地面に突っ込んだ後ですが,もちろん,終電は逃しました。 タクシーを拾い,なんとか自宅へ帰宅しました。

帰宅後,顔を点検したところ,大きな擦過傷と,そして,腫れがありました。

酔いも冷め,段々痛みが強くなってきます。

できれば,病院で治療をしてもらいたい,しかし,夜中なので空いている病院もないですし,救急病院に心当たりもありません。

未経験の障害に遭遇したときにどうするか,まずは,検索です。

Web は広いので自分しか経験したことがないと思うようなことでも,対処方法が乗っていたりするものです。

google 先生にお伺いを立てると,こういった相談を受け付けてくれる電話番号があるとのことでした。

7119 番 (携帯電話だと 23 区なら 03-3212-2323) です。 この電話では,

  • 症状に基づく緊急性の有無のアドバイ
  • 受診の必要性に関するアドバイ
  • 医療機関案内

等をしてくれます。

電話をし,受付の看護師さんと相談しました。

基本的な個人情報,年齢や住所,を聞かれ,症状について説明し助言をいただきました。

  • 顔というか頭を打っているので脳に異常がないかどうか CT を撮ってもらったほうが良い
  • 左腕に痛みがあるそうだが,そちらも,骨折の有無について検査したほうが良い

とのことでした。

また,頭部への打撲ということもあるので,このまま救急車を手配することもできるがどうするか,と聞かれました。

救急車を呼ぶかどうか,割りと真剣に悩みましたが,さすがにそこまでするのはどうだろうと思い,病院を紹介していただき,自力で病院を探しました。

混み具合などで 2 件ほど断られた後,治療してくれる病院が見つかりました。

病院では 20 分ほど待たされた後,CT と左腕のレントゲンを撮影していただき,骨折や脳の出血が無いことが確認されました。

検査の合間に,看護師さんに「こういった怪我で受診する人はどれくらい居るのか」と聞いてみたところ,割といるとのことでした。

さて,治療も済み,大きな怪我も無いことが判明したので,会計して帰宅します。 しかし,夜間では病院の精算業務が停止しているため,何円か預けて,後日精算する流れになるとのことでした。

僕の体験は以上になります。

最後に知見をまとめます。

  • 急な怪我や病気で緊急性の有無の判断や医療機関案内をしてほしいときは 7119 番に電話すると良い
  • 夜間に病院にかかるときは,少し多めの現金があると良い
  • ポケットに手を入れない
  • 飲みすぎない

これからの忘年会シーズン,怪我に気をつけて無事に今年を乗り切りましょう。

以上です。

(技術的な話も準備してあるのですが,ちょっと,上記の経験のインパクトが大きすぎてこちらを書き残すことにしました)