続 カッコの付け方

AWSを始めとしたクラウドコンピューティング全般と、唯一神emacsにおける()の付け方についてだらだら書きます

Google Cloud SQL 2nd Generation Proxy接続する

8月下旬、 Google Cloud SQL 2nd Generation がGAとなりました。すでに検証はしていますが、今回は Cloud SQL Proxy を中心に書きます。

iga-ninja.hatenablog.com

Cloud SQL Proxy とは?

前回の記事でも記載しましたが、Cloud SQL への接続は全てグローバルIPアドレス となります。これに嫌悪するひとは多いと思いますが、この点はどうすることも出来ません。で、GCEのVMからアクセスする場合、は2つの方法どちらかをとることになります。

  1. 特定IPアドレスからのみアクセスを受け付ける(グローバルIPのみローカルIPは意味ない)
  2. Cloud SQL Proxy を使う

1を採用したでも、GCEの特定VMからのみだときまっていたり、スケールアウトは考える必要がなく、固定台数でのみサービスする場合は、あまり問題はありませんが、スケールアウトするならば新規VM追加の度にIPアドレスを追加していく必要があります。他の方法としては、NATやHAProxy, MaxScale などで、どこかのIPアドレスに束ねてしまう方法ですが、新たなspofを産んでしまうので、あまりよい手とは言えません。

そこで、2の Cloud SQL Proxyです ちなみに、 MySQL Proxy とは全く関係ありません

動作について

Cloud SQL Proxy のやることは、Cloud SQLへの接続(or 認証だけ?)を、一般的な IPアドレス(TCP)経由ではなく、Cloud SQL APIベースで行うものだと推測しています。
だから、IPアドレスにより許可は要らない。
では、プログラム側からどのように参照するかですが、これは UNIX Socket となります。後で手順を書きますが、
Proxy の役目は Cloud SQL への接続を API ベースから UNIX Socket に変換する と言って良いはずです。

構成

上記 Cloud SQL Proxy の役割を理解すると、自ずと構成は Cloud SQL へアクセスする全VMインスタンスで Cloud SQL Proxyを動かす となります。どのみち UNIX Socket なんで、必然的にこうなりますよね。また Docker用の Cloud SQL Proxy もあるようです。これはDockerイメージかな?多分他のコンテナは、 Proxy コンテナへリンクをはって、Proxyコンテナ経由で接続するではなかろうかと

また、各VMインスタンスには、Cloud SQL のスコープ(AWSでいうEC2ロール)を付けておくことを激しく推奨します。スコープ無しでも出来ると思いますが、面倒だと思います。

手順

簡単です。GCSからダウンロードして実行するだけです。多分 Go製かな? https://cloud.google.com/sql/docs/compute-engine-access

wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64
mv cloud_sql_proxy.linux.amd64 cloud_sql_proxy
chmod +x cloud_sql_proxy
sudo mkdir /cloudsql; sudo chmod 777 /cloudsql
sudo ./cloud_sql_proxy -dir=/cloudsql &

この実行により、 /cloudsql/ ディレクトリの下に、各Cloud SQL への接続用 Unix Socketができ、 mysqlでアクセスするなら

mysql -uroot -p -S /cloudsql/[INSTANCE_CONNECTION_NAME]

で接続できます。 [INSTANCE_CONNECTION_NAME] ってなんじゃい?知いますが、大して難しい話ではなくて、-S で指しているのは UNIX Socket の場所です。 Cloud SQL Proxyが INSTAANCE_CONNECTION_NAME(Cloud SQLのね!) で UNIX Socketを作るってだけの話です。

自動起動

ただのアプリーケションなので、自動起動設定が必要です。勝手にデーモン化はするみたいですが。
systemd が使えるなら systemdで
ないなら upstart で設定すればよろし。

HAProxyで RR負荷分散

backend の指定は UNIX Socket でも出来るらしいです。(> 1.5)
動くことは確認出来ました。レプリラグがひどいものを排除する場合は、xinetd などで httpから確認になると思います。

inokara.hateblo.jp

まとめ

Cloud SQL 2nd gen 使うなら Cloud SQL Proxy 一択

メリットが圧倒的に多いので、上記のみとなります。

jsonの戻り値とか、複雑なオブジェクトからスマートにデータを抽出[JMESPath 最強説]

「パイセン、JSONで帰ってくる複雑な構造のオブジェクトをいい感じにするやつしらないっすか?」
「ああ、複雑な構造ね。しらん、専用の関数用意するとか?」
「やっぱ、そっすよねー」
全国津々浦々でこんな会話が繰り広げられてると思います。私も2回ぐらいこのやり取りをしました。この長年の問題に終止符、JMESPath最強じゃね?

JMESPathとは

以前、一回書いてます iga-ninja.hatenablog.com

この時私は勘違いしておりました、JMESPathはJSONだけを捌くライブラリではなく、JSONから素直にコンバート出来る各言語のオブジェクト を捌くことが出来ます。pythonで言うなら list と dictで出来てるものなら JMESPathでクエリ出来ます。# setとかは知りません。

似たようなのに jq コマンドがありますが、JMESPathのほうがいいです、多分。jq 詳しく知らないので、機能・性能面では評価出来ないですが、どう考えても動作環境の広さから言えばこっちが上です。能書きは最後の方に書きます。

JMESPathでこんなことができる!

前回も書きましたが

JMESPath Tutorial — JMESPath

ここ見てもらえばだいたい分かります。しかも試せます。そして私も勘違いしてましたが、このXPathぽい表記が JSON文字列ではなく、各言語のオブジェクト(JSONに変換出来る)に対して通用します。READMEの抜粋ですがこれは、 pythonのdictに対して問い合わせしています。

>>> import jmespath
>>> path = jmespath.search('foo.bar', {'foo': {'bar': 'baz'}})
'baz'

チュートリアルは、ナナメ読みせず、きちんと読んだほうがいいです。

もっと難しい問い合わせ

もっと複雑な問い合わせについて、ちょっと解説します。
http://jmespath.org/examples.html#working-with-nested-data

ここ見てください。awsの ec2_describe_instances の戻りっぽいJSONですが

{
  "reservations": [
    {
      "instances": [
        {"type": "small",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "large",
         "state": {"name": "stopped"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }, {
      "instances": [
        {"type": "medium",
         "state": {"name": "terminated"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "xlarge",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["DB"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }
  ]
}

reservations[].instances[].[tags[?Key=='Name'].Values[] | [0], type, state.name]

[
  [
    "Web",
    "small",
    "running"
  ],
  [
    "Web",
    "large",
    "stopped"
  ],
  [
    "Web",
    "medium",
    "terminated"
  ],
  [
    "DB",
    "xlarge",
    "running"
  ]
]

が帰ってきます。最後の [tags[?Key==`Name`].Values[] | [0], type, state.name] が特にヤヴァイ。select name, type, state from .. みたいに、欲しい項目を複数抜き出してます。プロジェクションってやつです。昔、情報処理とかで勉強した 射影 ですな。 ちょっと気おつけて欲しいのは このサンプルでは ?Key=='Name' と シングルクオートでNameを囲っているが、pythonとかで使うときは

?Key==`Name`

バッククオートで区切ること

pythonから使ってみる

import jmespath

dt = {
  "reservations": [
    {
      "instances": [
        {"type": "small",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "large",
         "state": {"name": "stopped"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }, {
      "instances": [
        {"type": "medium",
         "state": {"name": "terminated"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "xlarge",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["DB"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }
  ]
}


jmespath.search(
    'reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name]',
    dt
)

>>[['Web', 'small', 'running'],
 ['Web', 'large', 'stopped'],
 ['Web', 'medium', 'terminated'],
 ['DB', 'xlarge', 'running']]

pythonでのサンプルの解説

まず、

reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name]

について解説します。(後でアレンジも試します)

最初の部分

reservations[].instances[]

は特に問題無いと思います。実行したらこんな感じ

In [33]: jmespath.search('reservations[].instances[]', dt)
Out[33]:
[{'state': {'name': 'running'},
  'tags': [{'Key': 'Name', 'Values': ['Web']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'small'},
 {'state': {'name': 'stopped'},
  'tags': [{'Key': 'Name', 'Values': ['Web']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'large'},
 {'state': {'name': 'terminated'},
  'tags': [{'Key': 'Name', 'Values': ['Web']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'medium'},
 {'state': {'name': 'running'},
  'tags': [{'Key': 'Name', 'Values': ['DB']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'xlarge'}]

次の構文ですが、最初にこのクエリのやりたいことは、 instances配下にある、

  • tagsのNameタグのValue(配列なので Key=Nameのもの)
  • type
  • stateのname(dict なので、pythonで書くと state['name']の値)

だけを抜き出したいということです。チュートリアルを終わった段階で、いきなりこのクエリを書ける人はまず、いないと思います。
問題は、複数の要素を抜き出したいが、すべての要素がフラットに並んでいないという点です。フラットに並んでいるとは、わかりやすい例だとこんな感じ

{
  "people": [
    {"first": "James", "last": "d"},
    {"first": "Jacob", "last": "e"},
    {"first": "Jayden", "last": "f"},
  ]
}

こういう例だと、そもそもJMESPath要らなくね? という話ですが、 (people 以外にもう一個ゴミ要素があるとかなら意味ある) people[*].[first,last] とでもすれば複数のキーの値を抜くことは出来ます。

では、まず失敗例から。tagsの下の Key=name のうちの Values (配列) に着目し、これを狙い打ちます。

In [39]: jmespath.search('reservations[].instances[].tags[?Key==`Name`].Values[]', dt)
Out[39]: [['Web'], ['Web'], ['Web'], ['DB']]

これだと tags.Key='Name'のValuesは引けますが、 reservations[].instances[].tags まで掘り下げているので、どうやっても typeやstateは出てきません。

ちなみに.Valuesのケツの[] は flattenです。無いとこうなります。

In [40]: jmespath.search('reservations[].instances[].tags[?Key==`Name`].Values', dt)
Out[40]: [[['Web']], [['Web']], [['Web']], [['DB']]]

じゃあ、どうすればいいか。reservations[].instances[].tags まで掘り下げないで reservations[].instances[...] でマルチセレクト処理すれば良いということです! お手本の
[tags[?Key=='Name'].Values[] | [0], type, state.name] に着目、 これは

  • tags は tags[?Key==`Name`].Values[] | [0]
  • type は 素直に type
  • state は state.name

抜いています。更に解説を進めますと。
tags[?Key==`Name`].Values[] | [0] のパイプよりも前はもう説明いらないとおもいますが、なんでわざわざ | [0] としているかに注目。Valuesはlist型なので、 ['Web','01'] みたいな値が帰ってくることもありえます、その場合先頭を抜くという意味になります。(これはサンプルなので、参考程度に) あとの type, stateは解説不要ですね!

ちょっとアレンジ

In [46]: jmespath.search('reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name ]', dt)
Out[46]:
[['Web', 'small', 'running'],
 ['Web', 'large', 'stopped'],
 ['Web', 'medium', 'terminated'],
 ['DB', 'xlarge', 'running']]

これでも十分なんですが、どうせなら dict の listがかえってくるようにしたいです。どうやればよいのか、、って簡単ですよね!今までの解説を読んでたら、linstance[]の下からマルチセレクトしているだけなので、ここで{}で囲って適当にkeyをつければ終わりです。

In [48]: jmespath.search('reservations[].instances[].{_name: tags[?Key==`Name`].Values[] | [0], _type: type, _state: state.name }', dt)
Out[48]:
[{'_name': 'Web', '_state': 'running', '_type': 'small'},
 {'_name': 'Web', '_state': 'stopped', '_type': 'large'},
 {'_name': 'Web', '_state': 'terminated', '_type': 'medium'},
 {'_name': 'DB', '_state': 'running', '_type': 'xlarge'}]

このサンプルの落とし穴

instances[] からの マルチセレクトであるため、 Nameタグが存在しない場合、除外する という動きになっていません。試しにデータの一部の tag.Key Nameを NNameとかにいじってみてください。多分こうなるはずです。

In [54]: dt2
Out[54]:
{'reservations': [{'instances': [{'state': {'name': 'running'},
     'tags': [{'Key': 'Name', 'Values': ['Web']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'small'},
    {'state': {'name': 'stopped'},
     'tags': [{'Key': 'Name', 'Values': ['Web']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'large'}]},
  {'instances': [{'state': {'name': 'terminated'},
     'tags': [{'Key': 'Name', 'Values': ['Web']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'medium'},
    {'state': {'name': 'running'},
     'tags': [{'Key': 'NName', 'Values': ['DB']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'xlarge'}]}]}

In [55]: jmespath.search('reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name ]', dt2)Out[55]:
[['Web', 'small', 'running'],
 ['Web', 'large', 'stopped'],
 ['Web', 'medium', 'terminated'],
 [None, 'xlarge', 'running']]

最後がNone で抜けてきてます。これを避けるには、更にパイプを使って listの最初の要素がNoneで無いものを抽出するようにするか、これぐらいならpython側でやっちゃうかの2択かなと思います。ココらへんを選べるのがプログラム言語から使える利点ですね。

能書き

対応言語

github.com

python, ruby, php, js, go, lua LWの主要言語ほぼ全部やないか! 言語オタクもここまで来たら凄いわ! しかも jp というコマンドまであるし、goでね。去年の記事では rubyは対応してなかったけど、対応してる。

あとは c/c++, C#, java, swift とかの heavy系
Rust, Elixir とかの 変態関数型言語

ここまで来たら言語オタクから言語変態紳士へクラスチェンジ。

動作環境の優位性

AWSを使っている人なら、少なくとも pythonのJMESPathは絶対に入っているといえる。なぜなら aws-cliが依存しているから。また、Rubist曰くもAWS-SDK2が依存しているらしく、AWSに携わっている人なら、まずJMESPathは入ってる、はず。

ただ、非AWSな人にとっても、各言語でネイティブ対応できていて、バイナリ依存とかも無いので、インストール自体はさくっと簡単に出来る、はず。

なんでこんなにAWSなの?と思ったけど、作者がAWSの中の人?(オーガには入ってる)なので、AWSとは切っても切れない。けどAWS関係なしにこのライブラリはイケてる。

jqとの比較

jq のほうが歴史は古いと思いますが、JMESPathは各言語におそらく言語ネイティブ(俗にゆう pure-javaみたいな)で対応している点が優れていると思います。

あと、作者が一人であるという点も、私はプラスに考えています。なぜならこのJMESPathの仕様自体は、彼がこの世で一番知っているのだから。もちろんバグもどこかにあるでしょうが、JMESPathの仕様に対する実装レベルが(どの言語でも)一定であると想像します。

まとめ & 上達のコツ

  • AWS人ならJMESPath 最高、嫌でも入ってるし!
  • プログラマなら jq より jp のほうが明るい未来(と思う)
  • 学習は、とにかく叩いて試してみる。遅くてもチュートリアルは完全に理解するほうが、結果お得
  • 正規表現と同じ、1手ですべてを解決しようとしない。ちょっとずつ書き足す
  • パイプは素直にパイプ、わからなければパイプがなんとかしてくれる!
  • 今回触れなかったが、UDFも作れるらしい!

個人的な感想ですが、今更 jq を使う理由が見つからないです。過去の遺産(資産)とか関係なしのはなしですよ、もちろん。

GCE Autoscaling in 2016夏

Google Cloud Platform Japan 公式ブログ: GCE ユーザーに高可用性を提供する Regional Managed Instance Groups がベータに 最近 Regional Managed Instance Groups という機能がβになりました。Regional とはなんぞや?ということですが、

この機能のもとでマルチゾーンの設定を行うと、Compute Engine が自動的に VM インスタンスを同一リージョン内の 3 つのゾーンに均等に分散させます。こうしておけば、1 つのゾーンの機能停止といった稀な障害が発生した場合でも、残る 2 つのゾーンに配置された VM インスタンスはサービスの提供を続けることができます。

ということです。逆に言うと今まではゾーン毎にManaged Instance Group (長いので MIGと略)を作る必要があったのですが、本機能でだいぶ楽になります。この機能とAutoscalingについて書きます。

f:id:iga-ninja:20160807111047j:plain

Autoscaling

GCEのAutoscalingは、AWSのそれとはかなり異なります。他のパブリッククラウドの知識は理解の妨げになるので、真っさらな頭でやりましょう。

ASの構造

  1. インスタンステンプレート
  2. インスタンスグループ
  3. (Cloud Monitoring)
  4. AutoScaler

この順番で成り立っている感じです。(Cloud MonitoringはCustom Metricsを使う場合)AWSの場合は

  1. AutoScaling Group launch config
  2. AutoScaling Group
  3. CloudWatch

です。GCEのAutoScalerはそれ自身が閾値基準によるScale-out/in判定を行っている感じです。一個ずつ軽く説明

インスタンステンプレート

AWS launch config 相当です。ようは Scale-out時に立ち上がってくるインスタンスの設定 (Network, Tag, Image, MetaData等) を指定しておく物です。インスタンステンプレートでは リージョンは指定しません。

また、Image (OSイメージ)ですが、これはかなり楽できます。まず、AWSと異なり、Imageは全リージョン共通ですので、1イメージを全世界に撒く必要すらありません。さらにImageには family という属性があり、これを一致させてImage作成すると、 familyでイメージを指定した場合、そのfamilyの中の常に最新版のImageを指す 事ができます。
つまり、AWSだと、AMI作成してから Launch Config のイメージ差し替えが必須でしたが、その1手が全く不要になります。というより、API的にImageだけ差し替えとか出来ないので、これを使わないとかなり面倒な話になります。

なお、family で Imageを指定する方法は、残念ながら Developers Consoleでは存在しないようです。gcloud 必須です。下記の --image-family を指定します。

gcloud compute instance-templates create  |  Cloud SDK  |  Google Cloud Platform

インスタンスグループ

タイトルに出てきた Regional Managed Instance Groupsの話です。インスタンスグループは、インスタンステンプレートを参照します。また AotoScalerも参照?しているはずです。Developers Consoleからみるとこんな感じです。

f:id:iga-ninja:20160807111156p:plain

今回の変更で マルチゾーンが可能になりました。GCEのゾーンはAWSと異なり、2msのペナが無いので、使わない理由がありません。

f:id:iga-ninja:20160807111221p:plain

ヘルスチェックについて

自分も誤解していたので、ヘルスチェックについて。まずAWS識者の為の一言ですが
ELBベースのヘルスチェックは、GCEには存在しません。またヘルスチェックを行うのはLBではありません。

GCEのヘルスチェックはそれ単体で独立しています。HTTPの場合は、ローカルリンクアドレス (169.254,169.254) から飛んで来ます。 よって、ヘルスチェックを使い回すことが出来ます。

AutoScaler

驚くほど少ないパラメータしか指定出来ません。まず現状では出来ないこと

gcloud compute instance-groups managed set-autoscaling  |  Cloud SDK  |  Google Cloud Platform

  • スケール Out/In のルールを分離出来ない
  • スケール Inしないという戦略はとれない
  • 閾値超過したとき、何台追加するかを指定できない (step)

出来ることは

  • インスタンス最大/最小台数指定
  • CoolDown期間の指定 (AS発動間隔)
  • CPUベースのAS発動
  • カスタムメトリックスベースの発動
  • LB利用率ベースの..

次に、それらをGCEはどうさばいているか。

  • スケール Out/In のターゲットとして CPU 60% とだけ指定する
  • CPU 60% を超えた場合、上昇速度に応じて Setpを決める ( 緩やかな上昇なら1台、急激に上昇したら一気に5台とか多分そんな実装っぽい)
  • CPU 60% を下回ったら即座に スケール Inとか、そんなにバカじゃない。ある程度余裕をもって勝手にスケールIn

一言でいうと こまけぇこたぁいいんだよ!! いい感じにやっとくから

デプロイのやり方

Image差し替えの方法です。

新規Image作成

Image作成は結構面倒です。GCEは動いているDisk -> Imageが出来ないので、

  1. Snapshot
  2. Snapshot -> Disk作成
  3. Disk -> Image作成

という結構な面倒なことになります。この時、前述の Family を指定していれば、インスタンステンプレートの差し替え不要。

デプロイ前の AutoScaling Stop

他所でも言及されていますが、 Stopは出来ますが、再開は出来ません。set-autoscaling で再設定が必要です。

まとめ

  • GCEのAutoScalingは細かくユーザーが指定出来ないが、動かしてみたところかなり賢い。正に こまけぇこたぁいいんだよ!!
  • Regional Managed Instance GroupsでZone毎にいちいち作らなくても良くなった。
  • Image の Familyは使うべき。でもgcloudでのみ指定可能。

今回のアップデートでかなり使いやすくなりました。また、AutoScaling自体、かなり大きな変更が入っているはずなので、他所の古い記事とは整合しない部分が多数あります。あとは、AWSのようにLifeCycle-Hookなどが現状存在せす、Shutdown-ScriptというOS外で指定できる機能があります(SLAは無い、走る保証は無い)。細かい色々不足している機能はありますが、ユーザーがいじれる部分を極限まで減らし、本当に必要なものだけを高いレベルで実現出来ているのは、さすがGoogleだと思います。

nginx + php-fpm の組み合わせのErrorログ出力を理解する

ngix + php-fpm の組み合わせで特に Errorログについて整理しときます。

初心者向け補足

プール向け設定ファイル = /etc/php-fpm.d/www.conf
全体の設定ファイル = /etc/php-fpm.conf

php-fpmのログまとめ

error.log

php-fpm 全体で出す Errorログです。これは global ディレクティブでしか設定出来ません。 このファイルは、デフォルトでは各プールの標準エラーなどを出力されることはありません。各プールのエラーを出力したいならば catch_workers_output = yes を、プールの設定ファイルに記載します。 ただし、このパラーメータには注釈があります。

; Redirect worker stdout and stderr into main error log. If not set, stdout and
; stderr will be redirected to /dev/null according to FastCGI specs.
; Note: on highloaded environement, this can cause some delay in the page
; process time (several ms).
; Default Value: no

パフォーマンスに若干影響するということなので、気になる人はデフォルトの noが良いでしょう。

このファイルはphp-fpm マスタープロセスがつかみます。ので、logrotate時にシグナルを打つ必要があります。

$pool.access.log

プール毎 に設定できる access.logです。 このファイルはphp-fpm ワーカープロセスがつかみます。ので、logrotate時にシグナルを打つ必要があります。

xxx-slow.log

プール毎 に設定できる slow-logです。 php-fpm ワーカープロセスが出しているはずですが、ファイルはつかんでいません。

www-error.log

rpmデフォルトの設定を参考に書きます。プール毎 に設定できるのですが、これは php-fpm が出力しているのではなく、言語としての php が出力しています。 プール毎に設定できるので、これを使うのがベストプラクティスだと思います。

ファイルの出力は、エラーが発生したタイミングで発生します。また、php-fpmが出力していないので、出力するユーザーはワーカプロセスの実行ユーザー = プール向け設定ファイルで設定するユーザー&グループとなります。例えば nginx + php-fpm の組み合わせ centosなどの場合、 /var/log/php-fpm の所有権が apache となっているため、 www.confで 実行ユーザーをnginxなどとしていると ファイル出力出来ません。

nginx側のログ

ワーカープロセスの標準エラーは、実は nginxのerror.logにそのまま出力されます。しかし、日付等が記載されず、そのままなので、その点不便です。

おまけ php-fpm のログ出力パーミッションを変更したい

/var/log/php-fpm/xxx.log のうち、php-fpm自身が出力するものはすべて 600で吐き出され、しかも rootがオーナーです。これを設定ファイルでかえることは残念ながら出来ないようです。パッチは出ています。

https://github.com/sandyskies/php-filemode-patch

JSON中にDot(.)が含まれたキー名を変換する

最近なんでもかんでもJSONですが、MongoDB/ElasticsearchにJSONを突っ込もうとして問題にぶつかりました。具体的には表題の通りで、 キーにDot(.)を含むものが上手く動かない という点です。現状は キーにDotを使うな、もしもデータファイルに入っていたらコンバートしろ ということなので、コンバートフィルタを書きました。

どうして困るのか?

{
  "id": "hogehoge",
  "members": [
    "hisatoshi",
    "toshihisa"
  ],
  "detail": [
    {
      "name": "toshihisa"
    },
    {
      "name": "imaoka",
      "org": {
        "addr.name": "hogehoge",
        "tel.num": "fugafuga"
      }
    }
  ]
}

こういうJSONの場合、MongoDBやElasticsearchに突っ込んだ後、検索する場合は addr.name というキーは detail[1].org.addr.name と表現します。この場合 キーに含まれるDotが、階層を表すDotと競合してしまい、まともに動かないという現象が表れます。

例がよくなかった、途中で配列入っているの気にしないで。要は Object in Object な JSON

  • MongoDBの場合 Dotが含まれていようが、JSONインポートが出来ます。が、検索が出来ないので意味なし。

  • Elasticsearchの場合 インポートの段階で明確に蹴られます。

JSONキー置換フィルタの構想

最初は、正規表現の置換で良いかと思いましたが。JSONの中に JSON-textを含むような JSON-in-JSONのような構成をとられるとムダに破壊するかもと思い止めました。が、よく考えたら JSONのテキスト要素として JSONを表現する場合は \ でエスケープするので、正規表現だけでもいけますね、多分。。。

JSON吸って、pythonのオブジェクトにして、JSONで出す

多分正規表現のみでも行けそうと今、書いていて感じてますが、上記の方針で実装しました。 要は泥臭く実装するってことです。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import json


def conv_key(obj):
    if isinstance(obj, dict):
        for org_key in obj.keys():
            new_key = org_key
            if '.' in org_key:
                new_key = org_key.translate({ord("."): ord("_")})
                obj[new_key] = obj.pop(org_key)

            conv_key(obj[new_key])

    elif isinstance(obj, list):
        for val in obj:
            conv_key(val)
    else:
        pass


def main():
    for line in sys.stdin:
        my_json = json.loads(line)
        conv_key(my_json)
        print json.dumps(my_json)


if __name__ == '__main__':
    main()

使い方

./conv_json_invalid_keys.py < hoge.json

実装の説明

  • 再起です。むちゃくちゃ深い JSONだとスタック足らないかもしれません。(そんなJSONで変換が必要とかどうかしている!)
  • JSON->Pythonの対応として、受けうる型は list , dict, 普通の値の3種類のはず
  • list は dictの listかもしれないので、そのままもう一回解析

気づき

  • CSVのように使うJSONは、1行1JSONで書く必要がある。醜いからといってjqで整形してしまうとインポート出来ない
  • type(obj) とかやって比較しようとしていたけど、そんなの時代遅れらしい、 isinstance 使え!
  • tr相当の処理 string.translateunicodeかstrかで違うらしい、この実装は unicode向け
  • json vs simplejson はほとんど変わらんらしいが、 enc/dec がそれぞれ対照的に速い・遅いらしい

GCE HTTP(S) 負荷分散と Cloud CDNを使う

すでに何度が触れていますが、GCE HTTP(S) ロードバランサー が更にアップデートされました。同じく、 HTTP(S) ロードバランサー限定で Cloud CDN というサービスも有効となりました。

GCEでHTTPS負荷分散 SSL Termination - 続 カッコの付け方

GCP リージョン跨ぎロードバランサーの破壊力 - 続 カッコの付け方

HTTP(S) ロードバランサー追加機能

以前の記事で

と書きましたがこれは解消?されているようです。この場合、エフェメラルではなく、静的なIPを1つ取得して、 80にも443にも指定すればOKです。これでよくある(AWS ELBのように) 1つのELBで 80/443 を両方通す設定が可能です。

f:id:iga-ninja:20160626102416p:plain

また、この変更?により、http/httpsの違いによる x-forwarded-for の差は無くなりました(私の記憶が確かであれば)。かならず
[HTTP(S)バランサのIP] -> [多分SSLを解くナニカ] -> [自分でたてたinstance]
となります。この[SSLを解くナニカ]ですが、毎度アクセスするたびにIPアドレスが変わります。これから推測するに、かなりの数のインスタンスSSL解きをやっているのだと思います。イメージはこんな感じ。

f:id:iga-ninja:20160626115810p:plain

ネットワークロードバランサはDSRであるため、技術的に暖気不要なことは説明がつきます。
対して HTTP(S)はリバースプロキシ型となったため、HTTP/HTTPSいずれもターミネートする存在が必要です。ここかショボイと詰まってしまいますが、圧倒的な物量でカバーしていると推測します。

Cloud CDN

Cloud CDNとは、HTTP(S) ロードバランサー専用のCDN機能です。が、これは Google Cloud Storage (GCS)単体でも実現出来る、CDN機能をGCEにも応用したようなものらしいです。なので、

  • 静的コンテンツ(ccs,js,image)はGCSから配信(キャッシュのメタデータもちゃんとセットしている)
  • 動的コンテンツのみGCE + LBで配信

しているような健全なサイトであれば、正直微妙な機能です。GCS使ってれば、静的コンテンツは自動的にCDN配信ですから。

GCE側の実装

GCE側の設定としては超簡単です。

f:id:iga-ninja:20160626105256p:plain

これで終了

キャッシュの有効期限とか

設定できません。オリジンがはき出すヘッダを厳守します。逆に言うと CloudFrontのように、オリジンヘッダを無視して勝手にキャッシュ保持期間を設定したり出来ません。

サーバ側の設定

問題はこれからです。下記はドキュメントの抜粋
https://cloud.google.com/cdn/docs/caching#cacheability

  • 下記条件を満たしていること
    • backend service が caching有効になってること、(上記のGCE側設定)
    • GET だけ
    • HTTPレスポンスコードは 200, 203, 300, 301, 302, 307, 410 のどれか
    • レスポンスヘッダに Cache-Control: public
    • レスポンスヘッダに Cache-Control: s-maxage, Cache-Control: max-ageExpires
    • まともな Date ヘッダ 未来の時間とかダメ
    • content-Length か、chunkなら Transfer-Encoding
  • 下記条件に当てはまるなら、キャッシュしない
    • Set-Cookie
    • データ部分が 4 MB超え
    • VaryAccept, Accept-Encoding, Origin 以外
    • Cache-Control:no-store とか no-cache とか private
    • リクエストヘッダの時点で Cache-Control: no-store?

これらを満たす動的コンテンツってある?
ですがとりあえずやってみます。簡単なので一気に書くと apacheの設定で無理やり Cache-Controlヘッダを付けてやりました。

Header set Cache-Control "public, max-age=600"

apache再起動でOK。ものがphpだろうがなんだろうが、 Cache-Controlを付けます。検証用なので超雑。

動作確認

curl -i で試します。私の試したところ、エッジサーバ毎にAgeを個別に持っているように思います。ですので、数回叩かないとAgeが帰ってこないかもしれません。

$ curl -i http://xxx.yyyy.xxx.zzz/index.html
HTTP/1.1 200 OK
Date: Sun, 26 Jun 2016 02:15:35 GMT
Server: Apache/2.2.15 (CentOS)
Last-Modified: Sat, 25 Jun 2016 20:51:47 GMT
ETag: "21549-9-5362074a6e070"
Accept-Ranges: bytes
Content-Length: 9
Content-Type: text/html; charset=UTF-8
Via: 1.1 google
Cache-Control: public, max-age=600

hello

$ curl -i http://xxx.yyyy.xxx.zzz/index.html
HTTP/1.1 200 OK
Date: Sun, 26 Jun 2016 02:15:33 GMT
Server: Apache/2.2.15 (CentOS)
Last-Modified: Sat, 25 Jun 2016 20:51:47 GMT
ETag: "21549-9-5362074a6e070"
Accept-Ranges: bytes
Content-Length: 9
Content-Type: text/html; charset=UTF-8
Via: 1.1 google
Age: 5           <---- Ageきたー
Cache-Control: public, max-age=600

hello

invalidation

CDNといえば invalidationですが、Cloud CDNの場合はどうやるのでしょうか? GCSでは、GCS上のファイルを削除する・更新すると、CDN側のキャッシュも消えるらしい(あくまでも伝聞)のですが、GCEというかサーバではそんなこと出来ません。ので、ちゃんと invalidationの機能はあります。

f:id:iga-ninja:20160626114650p:plain

パスに http://とか打つと

パスの先頭は「/」にしてください。「*」が使用できるのは、末尾の「/」の後のみです。また、パスには「?」と「#」を含めることはできません。

と教えてくれます。ワイルドカードもいけるんだ、ふーん。

まとめ

GCEのSubNetwork対応による、変更点とAWSとの違い

昨年の12月頃らしいですが、GCEでもSubnetがサポートされました。最初、これを知った時 なんて無駄な機能をつけたんだ!Googleのパワーをこんなしょうもないことに使うな と思いましたが、調べてみるとGoogle流の考慮は入っていました。
ひとまず、このエントリに引っかかった人は、最後まで読んでください。tl;dr とかでは表せませんが、無理やり概要をまとめると、 GCEもsubnetという太古のダサい技術をサポートしてしまったが、他のパブリック・クラウドとは一味ちがうから安心して! となります。

f:id:iga-ninja:20160625192154j:plain

今までのGCE:Network

GCPドキュメント上では、Legacy mode と書いていますが、こちらのほうが よっぽど先鋭的 です。簡単にまとめると

  • ローカルIPアドレスの CIDRだけ指定する
  • そのCIDRは全リージョンにまたがる

たったこれだけです。AWSで言い換えると

  • 全リージョンをまたぐVPCが1つ
  • VPCのなかにsubnetは存在しない。言い換えるならVPC全体が1つのsubnet

これでほぼすべてを賄っていました。これで十分なんです。

Subnet対応でできるようになったこと

一応メリットはあります

  • ローカルIPアドレスを手動固定できる
  • SubnetのCIDR単位でFW ルールを指定できる (ただし AWSのような ルートテーブル - サブネットのひも付けはない)

正直、どうでもいい。。

Legacy Network におけるローカルIPアドレスは、すべてDHCPで、ユーザーで指定出来ませんでした。嘘でした、今確認したらLegacyでもできるようになってました。できると嬉しい!と思うひともいるかもしれませんが、GCEはAWSと異なり、起動時に決めるホスト名で内部DNSが自然に登録されるため、ローカルIP同士をIPアドレスで叩く必要はほぼ皆無です。AWSのような、IPアドレスベースの使いにくいFQDNではなく、ホスト名のみで、/etc/hostsとか登録無しでOKです。

次に SubnetのCIDR単位でFW ルールを指定できる点ですが、これもAWSとは考え方が全く違います。手っ取り早く説明するため、AWSの Public-Subnet / Private-subnet で説明します。
どこかのブログのエントリでもありましたが、このGCE:subnet をつかったとしても Public-subnet / Private-subnet は実現出来ません、が、GCEは AWS流の考え方ではなく、別の方法でこれを実現しています。私の感覚では、GCEの実現方法の方がよりスマート です。

AWSの考え方

旧来のネットワークに近い考え方
Public-Subnet: グローバルIPアドレスをもち、インターネットから直接アクセスできる subnet
Private-Subnet: ローカルIPアドレスしか持たず、インターネットから直接アクセスできない subnet

GCEの考え方

インスタンスタグで、FWのみならず、ルートテーブルも管理すればいい (下記名称は私の造語)
Internet-faced-Instance: グローバルIPアドレスをもち、インターネットから直接アクセスできる instance
Local-only-Instance: グローバルIPアドレスをもたず、インターネットから直接アクセスできない instance

双方の違いとメリット

つまり、ネットワーク経路に関することは subnet単位でやるのがAWS流。 subnetなんて元々存在しないので、インスタンスタグでインスタンス単位管理するのがGCE流。 この考え方は Legacy でも Subnetでも変わっていません

また、VPNを張った場合ですが、この場合はSubnetで切れたほうが(全リージョンにアクセス可能な状態がNGならば)メリットはあります。ただし、必須ではないですね、そこもインスタンスタグで制御すればいいので、所詮気分の問題でどうでもいいです。

新しいのGCE:subnet

厳密には subnet対応にも二種類存在します
1. autoモード
2. customモード

autoモード

自動設定するウィザードではありません customとは全然違います。

  • 各リージョンに1つのSbunetが自動生成
  • Subnetの追加・削除は一切できない
  • 既存SubnetのCIDRの変更できない
  • (多分) 新リージョンが増えたら勝手に新しいCIDRが追加される

f:id:iga-ninja:20160625191119p:plain

サブネットは存在こそしますが、ユーザーはメンテナンス・オペレーション出来ません。 これにより、おそらくですが、新リージョンがリリースされると、勝手にSubnetは追加されていくと思います。

customモード

完全に Subnetをユーザーがコントロール出来ます。つまり

  • 各リージョンに Subnetを1つずつ作るか否かはユーザー次第
  • 1リージョンに Sbunetが2つとかもユーザー次第
  • CIDRも自由自在、ユーザー次第
  • (多分) 新リージョンが増えても勝手に新しいCIDRは作られない

f:id:iga-ninja:20160625191209p:plain

どうしてもSubnetを弄りたい人は custom一択です。

customモードの説明

customの説明を詳しく書きます。

CIDRの考え方

ドキュメントの絵を見るのが早いですが、1つのネットワークの中に 10.1.0.0/16 と 192.168.0.0/16 が混在出来ます。

Using Subnetworks  |  Compute Engine Documentation  |  Google Cloud Platform

AWSの場合、Subnetの前にVPCが存在し、VPCのCIDRの範囲内にSubnetを指定する必要があります。(この成約があるので、最初にネットワーク設計をしないと、手戻りが発生してしまいます。)
しかしGCEには VPCに相当するものはNetworkで、Network自体はCIDRを持ちません。よってこんなことが出来ます。

各Subnet間の通信は?

AWSのNetworkACLのように、Subnetに対してのみ効果を発揮するACLは存在しませんAWS的に言うなれば、SGしか存在しません。
各Subnet間の通信も、FW Ruleで、IPアドレス単位での指定に過ぎません。また、デフォルトでは各Subnet間での疎通は出来ません。同Subnet内でも同じく、デフォルトでは別マシンと疎通出来ません
FW ルールとしては下記のようなものを設定すれば、内部通信は全疎通となります。

f:id:iga-ninja:20160625191310p:plain

f:id:iga-ninja:20160625191319p:plain

Subnet間は疎通ルールを足していないが、インスタンスタグで疎通させている場合はどっちが適応?

  • Subnet A: 192.168.1.0/24
  • Subnet B: 192.168.2.0/24

Subnetレベルで疎通はNGとしているが、各サブネットにインスタンスを立てて、そこに疎通させるためのタグをつけたらどうなるのか?試してみました。

f:id:iga-ninja:20160625191344p:plain

ここで作成した internal タグを、互いのインスタンスにつけて、pingを打ちます。  結果は予想通り疎通OKとなります。デフォルトが暗示Deny、明示Denyできないので、Allowが1つでもあるとそれが適応されます。

全リージョンにSubnetを作ることは必須ではない

作りたければ使いたい時、使いたいだけ作れ!というスタンス 私は大好きこの思想。

あとからCIDRは変更できる?

できない。Subnetは新規作成か削除のみ操作可能

Zoneについて

SubnetはZoneには縛られません。 ただし、リージョンを跨ぐことは出来ません。

一先ずここだけでも安心しました。AWS VPCの最大の設計ミス、Subnetは AZを跨げない という悪手は踏襲していません。

まとめ

  • 基本的なACLの考え方は昔から変わっていない。
  • 今までのNetworkは Legacyとか言われるようになって(気に入らない)、作成するには gcloud コマンド経由となった。
  • Auto の Subnet は今までのNetworkとほとんど変わらない。ただ意味もなくリージョン毎にSubnetを切るだけで、追加も削除もできない。
  • Custom の Subnet は自由にSubnetを切れる
    • Classをあわせる必要すらない
    • 大きなアドレス空間を細切れにしていく設計も不要
    • SubnetはRegionを跨げない
    • SubnetはZoneを跨ぐ、絞りたければインスタンスを建てなきゃいいだけ
    • Subnet毎にルートテーブルとか、ダサい仕様もない
  • Subnet内にインスタンスを構築するとき、ローカルIPアドレスを指定しなくとも、連番で若番から使う

なんでこんなにSubnetを嫌うのか

AWS-VPCを触ったあとに、初めてGCEを触った時に、ネットワークの簡素さに不安を覚えました。
「え、こんなに簡素でいいの?これしかできないの?」
しかし、インスタンスタグレベルでサーバ間通信、ルート、セグメントを分割できると知ったとき
「なんて賢いんだ!さすがGoolge!」
と思いました。私はネットワーク屋ではありませんが、オンプレ時代、というかクラウドコンピューティングが存在しなかった頃のインフラ屋を知っています。その概念から考えると

  • 各セグメントはSubnetで切るものだ!
  • セグメント毎にFWでコントロールするものだ!

という固定観念が多くの人にあるはずです。しかし、ほとんどのパブリック・クラウドはブロードキャストはおろか、ARPすら無いネットワークで、Subnetでセグメントを割る必要すら本当は無いのです。
ACLをサブネットに求める必要すらなく、他の選択肢が存在するので、そちらを選べば良いのです。はっきり行って、Subnetにこだわるのは時代遅れです。
GCEは私が知る限り、最後発だと思いますので、先人(AWS達)が継承した 古臭い固定概念 をバッサリ捨てました。正しい判断だと思います。

だいぶネガティブな論調で書きましたが、今回のSubnet対応により、Subnetを切らないと気がすまない という 意味のないこだわり は実現出来ます。ただしGCEのNetworkの本質は今のところ変わっていないので安心しました。