TOYOKUMO Tech Blog

トヨクモ株式会社の開発本部のブログです。

Zipperでコードフォーマッターを実装する (greglook/cljstyle に学ぶ)

北川です。

Clojure 始めました。 Clojure のお勉強をするにあたり何か良い題材はないかと探していたところ greglook/cljstyle というツールが目につきました。 以前からフォーマッターや linter の実装に興味があったということもあり、ちょうど良いのでこれを題材にお勉強を始めていきます。

コンテキスト不明なものをいきなり読んでも眠くなってしまいそうなので greglook/cljstyle のソースコードをそのまま抜粋するなどはしません。 しかし、この記事を読み終える頃には自分好みのコードフォーマッターを再実装できそうな気持ちになれます。*1

実際にcljstyleを使ってフォーマットをかけていく方法については、こちらの記事をご覧ください。

tech.toyokumo.co.jp

フォーマット処理の流れ

例えば cljstyle check コマンドは以下の通りです。

  1. Clojure コードの文字列を取得
  2. 文字列をパースしてツリーを取得
  3. ツリーをトラバースしてフォーマットルールに反した箇所を探す
  4. 変更する
  5. オリジナルソースと変更後ソースの diff をとる
  6. 結果を整形して表示する

1, 5, 6 についてはやるだけなので 2, 3, 4, について見ていきます。

Zipper

本題に入る前に Zipper についてほんのり触れておきます。Zipper とは1996年に Gérard Huet という人が発明したデータ構造です。 Clojure のような immutable なデータ構造を変更する際 Zipper を適用すると比較的簡単な操作で効率的に再構築できるというものです。Zipper の特徴は現在の注目点(loc)と、そこに至るまでのパスを保持している点です。あまり Zipper について思いを馳せると長くなってしまうので、今回は便利なAPIだなあと思うことにします。さっそく使い方を見ていきます。

(require [clojure.zip :as zip])

(-> (zip/vector-zip [1 2 [3 4]])
    (zip/down))

;;=> [1 {:l [], :pnodes [[1 2 [3 4]]], :ppath nil, :r (2 [3 4])}]

vector-zip を使うと vector をトラバースするための Zipper が得られます。zip/down でルートからひとつ下に降り、注目点が 1, right-node が [2 [3 4]] という状態になりました。

(require [clojure.zip :as zip])

(-> (zip/vector-zip [1 2 [3 4]])
    (zip/down)
    (zip/right))

;;=> [2 {:l [1], :pnodes [[1 2 [3 4]]], :ppath nil, :r ([3 4])}]

さらに right-node へ進むと、注目点が 2, right-node が [3 4]、left-node が [1] という状態になりました。

(require [clojure.zip :as zip])

(-> (zip/vector-zip [1 2 [3 4]])
    (zip/down)
    (zip/right)
    (zip/edit inc)
    (zip/root))

;;=> [1 3 [3 4]]

注目点の値を1増やし zip/root でデータを再構築すると変更後の vector を取得することができました。このように Zipper はデータを歩き回り変更したい時に便利ですね。

rewrite-clj で文字列をパース

greglook/cljstyle では文字列をパースしてツリーを取得する工程で、xsc/rewrite-clj が使われています。rewrite-clj の返す Zipper は clojure.zip の上に実装されているので rewrite-clj の提供するAPIはもちろん使えますし先ほどの zip/right のような clojure.zip が提供する操作も問題なく使うことができます。動きを見ていきます。

(require [rewrite-clj.zip :as z])

(z/of-string "(defn add [a b] (+ a b)) (add 1 2)")

;;=> [<list: (defn add [a b] (+ a b)) >
;;  {:l [], :pnodes [<forms: (defn add [a b] (+ a b)) (add 1 2) >], :ppath nil,
;;  :r (<whitespace: " " > <list: (add 1 2) >)}]

z/of-string は文字列から Clojure コードツリーの Zipper を生成します。現在の注目点が (defn add [a b] (+ a b)) フォームとなり、right-node が残りの (add 1 2) という部分です。 ここでうれしいのが、ホワイトスペース等のフォームの評価時には不要なトークンを保持しているという点です。これはコードフォーマッターを実装する時にはとても都合が良さそうです。

実際にフォーマットしてみる - surrounding-whitespace の除去

greglook/cljstyle でもフォーマットルールとして定義されている surrounding-whitespace の除去をやってみます。

(   * a 3 ) このようなフォームを (* a 3) このようにフォーマットします。

(require
  [clojure.zip :as zip]
  [rewrite-clj.zip :as z])

(defn surrounding-whitespace?
  [zloc]
  (and (z/whitespace? zloc)
       (or (nil? (zip/left zloc))
           (nil? (z/skip zip/right z/whitespace? zloc)))))

(defn remove-surrounding-whitespace
  [form]
  (loop [zloc (z/of-string form)]
    (if-let [zloc (z/find-next zloc zip/next surrounding-whitespace?)]
      (recur (zip/remove zloc))
      zloc)))

remove-surrounding-whitespace では最初に (z/of-string form) で受け取った Clojure コードをトラバースするための Zipper を作ります。

(z/find-next zloc zip/next surrounding-whitespace?) では、述語 surrounding-whitespace? にマッチするノードを探します。

zip/nextz/find-next が使用する次の注目点への移動関数です。rewrite-clj.zip が提供する z/next はホワイトスペースやコメントをスキップするため普通に Zipper を歩く分には便利なのですが、コードフォーマッターとしてはスキップして欲しくないです。そこで clojure.zip が提供する zip/next を使います。zip/next はホワイトスペース等を無視せず Zipper を深さ優先的に探索するので今回はこれが使えます。

surrounding-whitespace? は注目点がホワイトスペースなノードであることと、フォーム左端または右端であることを調べています。

(z/skip zip/right z/whitespace? zloc) では、Zipper 全体ではなくトップレベルの1フォーム内をホワイトスペース等は無視せず探索したいため z/skipzip/right を使用していることがポイントです。

(require
  [clojure.zip :as zip]
  [rewrite-clj.zip :as z])

;; ...

(z/root-string (remove-surrounding-whitespace "(    * a 3  )"))
;;=> "(* a 3)"

(z/root-string (remove-surrounding-whitespace "(  + 2 3 (    * 4 5    )  )   (   + d 3    )"))
;;=> "(+ 2 3 (* 4 5))   (+ d 3)"

動いているように見えます。

一般化する

先ほどの remove-surrounding-whitespace から、フォーマットルールに反しているか調べる述語と、ルール違反を検知したら何をするか、を分離すると一般化することができます。

(require
  [clojure.zip :as zip]
  [rewrite-clj.zip :as z])

(defn edit-all
  [form p? f]
  (loop [zloc (z/of-string form)]
    (if-let [zloc (z/find-next zloc zip/next p?)]
      (recur (f zloc))
      zloc)))

さらにコードフォーマッター用の設定ファイル等からマップを生成し、設定が有効ならば何をするかを定義することができます。

(require
  [clojure.zip :as zip]
  [rewrite-clj.zip :as z])

(defn remove-surrounding-whitespace
  "Transform this form by removing any surrounding whitespace nodes."
  [form]
  (z/root-string (edit-all form surrounding-whitespace? zip/remove)))

(defn reformat-form
  "Transform this form by applying formatting rules to it."
  [form config]
  (cond-> form
    ;; config は
    ;; { :remove-surrounding-whitespace? true, :another-rule? false, ... }
    ;; のようなマップ
    (:remove-surrounding-whitespace? config true)
    (remove-surrounding-whitespace)
    ;; ...
    ))

あとは、フォーマットルールに反しているか調べる述語と、ルール違反を検知したら何をするか、をひとつひとつ実装していけばコードフォーマッターが完成しそうですね。

ここまでくると greglook/cljstyle のソースコードにかなり近くなってきました。実際にはもっと細やかな気配りのもと書かれていますが、要旨は押さえられていると思います。

(上記の実装だと設定項目の数だけソースコード全体をパースして走査することになるのが少し気になりますね。)

最後に

自分でもコードフォーマッターが実装できそうな気持ちになれました。トークン列からソースコード上の位置などを取得できれば linter も同じように実装できそうです。

また、rewrite-clj といったツールを作るためのツールの存在は偉大ですね。ぼくも rewrite-clj を使って何かやりたいなあと思えてきました。

*1:気持ちになれます

会社の金で買った HHKB Professional HYBRID Type-S (2台) の打鍵感はいかほどか

こんにちは。このブログでまだ技術的なことを1つも書いていない開発本部の飯塚です。

自分の好きなキーボードを買ってくれる会社も最近では珍しくなくなってきたかと思います。 ご多分に漏れず弊社トヨクモでもPCとキーボードは自分で好きなものを選び買ってもらうことができます。

しかし「左右の手で1台ずつ使いたいから」と言うバカみたいな理由で 2 台買ってくれる会社はどれだけあるでしょうか? それもつい最近発売された HHKB Professional HYBRID Type-S を!

f:id:uotan:20191217074420j:plain
『会社の金で買ってもらった』 HYBRID Type-S 2 台 (上の Pro2 Type-S 2台は今まで使っていた個人的に買ったもの)

経緯

私は元々使っていた HHKB Professional2 Type-S があったので、キーボードに関しては買ってもらっていなかったのですが、 Type-S 好きとして HYBRID Type-S は見逃せず買ってもらうことは確定事項でした。

ただ懸念が1つあり、私は普段 HHKB を 2 台並べて左右の手で1台ずつ使う「なんちゃって分離HHKBスタイル」を採用しているので、 同じことをやろうとすると、この高級キーボードを2つ買う必要があります。 1台くらい自腹でいいかと思いつつ、ダメ元で2台買ってもらえたりするんですか?と聞いてみたところ、

「2台買えるルールがあるわけじゃないけれど、飯塚さんこだわり強いから特例で」

おぉ、普段から2台体制でこだわってますアピールしていて良かった。。

到着

f:id:uotan:20191217074940j:plain
産まれたての HYBRID Type-S

買ってもらったのは英語配列の色は墨です。 配列はもう日本語配列を使えない体になってしまっているので英字一択でしたが、色は Type-S として今回初めて墨が選べるようになったので折角なので墨にしました。

刻印はキーマップの変更もあるので無刻印も少し考えましたが、個人的に何となく好みじゃないので刻印ありです。

f:id:uotan:20191217080304j:plain
左右に仲良くならぶ双子の HYBRID Type-S

諸事情で今は有線接続していますが、無線接続でも2台体制は問題ないそうです。 打鍵音は同じ Type-S なので Pro2 Type-S 同様に「トコトコ」と軽い音ですが、心なしか HYBRID の方がより軽い音な気がします。 しかし打鍵感がないわけではなく、しっかりと打てている感はありとても気持ちいいです。

キーマップ変更

さて今回の目玉機能の1つであるキーマップの変更ですが、結論から言うと今はしていません。

私は普段 Vim 上で Clojure を書いているので、以下のようなキーマップを Vim 上で行っています。

  • コロンとセミコロンの入れ替え
  • 数字と記号の入れ替え(e.g. "9" と "(" や "5" と "%" の入れ替え)

しかし現状の Happy Hacking Keyboard キーマップ変更ツールではどうもこれらの入れ替えができないようです。 今後の更新で対応可能な範囲かはわからないですが、ここまで変更できると個人的にはかなり嬉しいです。

f:id:uotan:20191217081631p:plain
キーの場所は変えられても同じキーで入力内容が入れ替えられない

最後に

会社の金で買った HHKB Professional HYBRID Type-S (2台) の打鍵感は最高です!

どうも一部のモデルがすでに在庫切れになっているようですが、Type-S を使っていない人はもちろん、使っている人も触ってみるのをおすすめします。


トヨクモでは一緒に働いてくれるエンジニアを募集しております。

採用に関する情報を公開しております。 気になった方はこちらからご応募ください。

nil punningが難しい(seq vs empty?)

これはClojure Advent Calendar 2019 - Qiitaの二日めの記事です。nil punningが難しいなあと個人的に感じているので、なぜリッチヒッキーさんはnil punningを選択したのだろうか、nil punningが優れている点は何か、ということを調べて書こうと思います。調べていくうちに、いろんな考えがあるなあ、というような気持になることはできました。nil punningにも一長一短があり、実際リッチヒッキーさんも「確かに複雑ではある」とは言っていることはわかりました。オチのない記事になってしまいましたが大目に見てください。

nil punningとは何か

"pun"とは日本語では「 (同音意義語を利用した)だじゃれ」と訳される ようで、Lispでいえば、nilが異なるコンテキストでは別の意味を持って用いられること(=同音異義語)、のことを指します。特にClojureでは、ifの条件の部分ではnilは偽として扱われること、nilは空のリストや空のhash map、空のvectorとして多相的に扱われること、これらのことを指してnil punningといいます。

参考: https://lispcast.com/what-is-nil-punning/

調べようと思ったきっかけ

Clojureのスタイルガイド を見ていたら、nil punningの節で違和感を感じました。

;; good
(defn print-seq [s]
  (when (seq s)
    (prn (first s))
    (recur (rest s))))

;; bad
(defn print-seq [s]
  (when-not (empty? s)
    (prn (first s))
    (recur (rest s))))

「goodとbadが逆では?」という気持ちになりました。私が初めて触れたLispはSchemeであったこと(参考: Schemeではnilはfalsyな値ではない。(if () 1 2) => 1 になる)も要因の1つではあるかもしれないですが、文芸的プログラミングの観点から見てもempty?のほうが意思が感じられてよさそうだなと思いました。実際、私と同じように感じる人も少なからず存在するようで、nil punning confusing? · Issue #44 · bbatsov/clojure-style-guide · GitHubのようなissueがstyle guideのリポジトリに立てられていました。これに対して、リポジトリのオーナーのbbatsovさんは 「リッチヒッキーがnil punningを推奨している」というようなコメントをしています。このseq vs empty?に関しては、nil punningの問題が背後に潜んでいるとみることができます。seq派の人は、nil punningを推進しているので、whenなどの条件分岐にnilを利用することを好みます。一方empty?派の人(私もその一人)は、nilを多義的に扱うことを好まないので、whenであったりifの条件部にnilの値が紛れ込まないよう、empty?でbooleanにわざわざ変換します。empty?は(not (seq coll))として実装されているからなのでは 、という意見もあるようですが、ちょっとくらい効率が落ちても分かりやすく書いたほうが嬉しいのではと思っているので、あまり納得がいきませんでした。

以後、google groupsにあるnil punningの推進派、反対派の意見を整理していきたいと思います。

nil punning反対派のpuzzlerさん

https://groups.google.com/d/msg/clojure/gWvXoHa7-t4/rPY-nWpt4zQJ

puzzlerさんはnilが存在しないこと、偽、空を表す3つの意味で使われていることに一貫性が感じられないと言っています。「多くのケースでは動くが、nilを要素に含むlist/map/vectorを渡すと動かない」関数を多く見てきたそうです。具体的にどういうことなのか別のコメントを見てみましょう。

https://groups.google.com/d/msg/clojure/gWvXoHa7-t4/6Qw3yIq0BEAJ

(filter identity s)(keep identity s) を例として挙げています。前者はnilとfalseを消すのに対し、keepはnilだけを削除するのがconfusingだと言っていますね。filterとkeepの使い分けが生じて、バグを生む原因になってしまったということなのでしょう。 参考

user=> (keep identity [true false 1 nil])
(true false 1)
user=> (filter identity [true false 1 nil])
(true 1)

nil punning推進派のリッチヒッキーさん

google groupでnil punningについて議論が盛り上がっている中、リッチヒッキー本人がコメントを残しています。

https://groups.google.com/d/msg/clojure/gWvXoHa7-t4/HjdiQu3dqbwJ

The is no doubt nil punning is a form of complecting.

nil punningが複雑さを生むことは認めていますね。(文頭のTheはThereのtypoだと思われる)

But you don't completely remove all issues merely by using empty collections and empty?, you need something like Maybe and then things get really gross (IMO, for a concise dynamic language)

empty?を使ってもすべての問題が解決するわけではない、と言っていますね。Maybe(Optional)のようなものは動的言語には似つかわしくないという考えのようです。

I like nil punning, and find it to be a great source of generalization and reduction of edge cases overall, while admitting the introduction of edges in specific cases. I am with Tim in preferring CL's approach over Scheme's, and will admit to personal bias and a certain comfort level with its (albeit small) complexity.

慣れは必要だが、nil punningはより一般的なアプローチにつながると言っています。

However, it couldn't be retained everywhere. In particular, two things conspire against it. One is laziness. You can't actually return nil on rest without forcing ahead. Clojure old timers will remember when this was different and the problems it caused. I disagree with Mark that this is remains significantly complected, nil is not an empty collection, nil is nothing.

このコメントの文脈について補足します。 https://groups.google.com/d/msg/clojure/gWvXoHa7-t4/Ndf2EHynZdAJ puzzlerさんが (when s ...)と書いていたため、sが空のlazy-seqの時に(空のlazy-seqはtrueとして扱われる)変な動作になってしまった、というような体験を語っています。これは、nilがfalsyな値であるため、空のlazy-seqのもfalsyなのでは、と思った結果バグらせてしまった、これはnil punningが原因だ、というような趣旨のようです。

https://groups.google.com/d/msg/clojure/gWvXoHa7-t4/DLxx_dCoRkUJ それに対して、daly(=リッチヒッキーのコメントに出てくるTim)が、昔のClojureは空のlazy-seqもfalsyだった、すなわち、(when s ...) が透過的に (when (seq s) ...) と扱われていた、このアプローチのままだったらpuzzlerの言う問題は生じなかった、という指摘をしています。これ踏まえてコメントを見直してみましょう。

You can't actually return nil on rest without forcing ahead. Clojure old timers will remember when this was different and the problems it caused. I disagree with Mark that this is remains significantly complected, nil is not an empty collection, nil is nothing.

おそらく、透過的にseqを呼ぶと遅延リストのまま引きまわすことができないため問題が生じたことがあったのではないか、と解釈できそうです。nilと空のコレクションを混同するなと言っていますね。dalyのアプローチを採用しても依然として複雑さは残るだろうという考えのようです。

Second, unlike in CL where the only 'type' of empty collection is nil and cons is not polymorphic, in Clojure conj is polymorphic and there can only be one data type created for (conj nil ...), thus we have [], {}, and empty?. Were data structures to collapse to nil on emptying, they could not be refilled and retain type.

Common Lispと違ってmap, set, vector, listなど複数の種類のemptyな値があるということを言っています。nilを用意することで、map, set, vector, listなどに多相的に使うことのできる関数が書きやすくなったりするのでは、と思いましたが、具体的な例は思いつきませんでした。

At this point, this discussion is academic as nothing could possibly change in this area.

academicという言葉の解釈が難しくてわかりませんでした。pragmaticと対比する意味で使っているのかと思いましたが、リッチヒッキーがnil punningのメリットを感じる点があるならその解釈はおかしい気がしました。"nothing could possibly change in this area" は、nil punningかそうでないか、という議論に決着がつくことはないだろう、ということを言っているのでしょう。

The easiest way to think about is is that nil means nothing, and an empty collection is not nothing. The sequence functions are functions of collection to (possibly lazy) collection, and seq/next is forcing out of laziness. No one is stopping you from using rest and empty?, nor your friend from using next and conditionals. Peace!

最後に、nil punningに親しむための考えを紹介してくれています。nilとempty collectionは違うんだぞー、ということを意識しろと言ってます。でもempty?を使うのは君の自由だ!らしいです。

empty?の存在意義

最後に、上記のことを踏まえて、初めの seq vs empty?の問題に戻りたいと思います。nil punningを採用している以上、empty?という関数の存在意義は何だろうか、という疑問が浮かびます。これはclojureがJavaのライブラリを呼んだりする状況を考えれば腑に落ちるのではと考えます。clojureだけで記述している限り、nil punningの思想を徹底していればbooleanを表す値は不要ですが、Javaのライブラリがbooleanを期待しているところにnilを渡すわけにはいきません。そういう場合にsequentialな値をbooleanに変換する関数empty?が必要になってくるということなのでしょう。個人的にはjavaとの連携の部分にempty?を使い、それ以外はseqでnilかどうかを判定する方針で書いていこうかなと思いました。

書きたかったが書けなかったもの

  • coreの関数でbooleanを返す関数 contains? などの設計について書きたかったが、疲れたのでやめた。要素を含んでいるか調べる系の関数ではnilを返すことで「nilが要素として発見されたのか、それとも(条件を満たす)要素がなかったのか」が分かりづらくなるので、booleanを返すようになっていたりしないかなと適当に仮説を立てているが、調べてないので嘘かもしれない。

参考にしたもの

https://github.com/bbatsov/clojure-style-guide/issues/44

https://lispcast.com/what-is-nil-punning/

https://groups.google.com/forum/?fromgroups=#!topic/clojure/gWvXoHa7-t4

https://ejje.weblio.jp/content/pun

kintone REST API の query の BNF を書いてみました

北川です。

弊社では安否確認サービス2の他 kintone連携サービス の開発もしているということもあり、kintone REST API を頻繁に利用します。

中でも query という独自SQLのような機能をよく使うのですが、記法や SQL との差異をよく忘れるので BNF として記述しておこうと思い至りました。

また、BNF があると構文解析器も書きやすいですし文法チェックなど製品で役立つ機能開発に使うことができます。

W3S's EBNF での記述となります。文法のチェックには Railroad Diagram Generator を使用しました。

BNF

query           ::= conditions? order-by-clause? limit-clause? offset-clause?

or-conditions   ::= and-conditions ('or' and-conditions)*
and-conditions  ::= parenethesized ('and' parenethesized)*
parenethesized  ::= condition | '(' or-conditions ')'
condition       ::= comp-condition | in-condition | like-condition
comp-condition  ::= field comp-operator value
in-condition    ::= field in-operator values
like-condition  ::= field like-operator '"' string '"'
field           ::= string ('.' string)?
comp-operator   ::= '!'? '=' | '>' | '<' | '>=' | '<='
in-operator     ::= 'not'? 'in'
like-operator   ::= 'not'? 'like'
values          ::= '(' value (',' value)* ')'
value           ::= num | '"' string '"' | function

order-by-clause ::= 'order by' sortor (',' sortor)*
sortor          ::= field ('asc' | 'desc')?

limit-clause    ::= 'limit' num

offset-clause   ::= 'offset' num

num             ::= "任意の数値"
string          ::= "任意の文字列"
function        ::= "LOGINUSER()..."

注意点

  • もちろん非公式である
  • 細かい点で実際の仕様との差異はあるはず

補足

kintone REST API には「フィールド、システム識別子ごとの利用可能な演算子と関数」という制約がありますが、こういった意味的な正しさはBNFでは検査しないことにしています。

andor では優先度を同じにしてしまうと結果が決定的にならないため、優先度を明示する必要があります。

再帰下降型の構文解析器を前提としているので左再帰にならないように意識しています。


トヨクモでは一緒に働いてくれるエンジニアを募集しております。

採用に関する情報を公開しております。

気になった方はこちらからご応募ください。

Symfony アプリを Elastic Beanstalk Multi-Container Docker で運用する

北川です。

Symfony アプリを AWS Elastic Beanstalk Multi-Container Docker を用いて構築・運用する事例の紹介です。

弊社が提供するフォームブリッジというサービスは実際にこの記事の内容に基づき作られています。

なおベストプラクティスを模索中だったり、生々しい妥協案も出てくるので暖かい目で読んでいただきますと幸いです。

構成

  • Nginx
  • Symfony, Composer
  • Vue, Yarn

フロントエンドとバックエンドがある程度分離されたSPA

Symfonyアプリでの事前準備

EB での運用を快適にするためにはアプリ側にもいくつか要件があります。

環境変数の設定

アプリの振る舞いを変える方法を環境変数のみにしておきます。 Symfony では parameters.yaml を配置して振る舞いを変えますが、parameters.yaml から環境変数を読み込ませる機能がありますのでこれを利用します。Symfony 3系でも使えます。

# parameters.yaml
parameters:
    database_host: '%env(DATABASE_HOST)%'
    database_port: '%env(int:DATABASE_PORT)%'
    database_name: '%env(DATABASE_NAME)%'
    database_user: '%env(DATABASE_USER)%'
    database_password: '%env(DATABASE_PASSWORD)%'

注意点として、環境変数にセットした値がデフォルトでは string でパースされるのでポート番号など型を指定したい場合は '%env(int:DATABASE_PORT)% のように記述する必要があります。 環境変数は EB の環境プロパティでセットします。

ログを標準出力に出す

必要なログを標準出力に出し CloudWatch にストリーミングするようしておくと ssh で接続してファイルを取得といったことをする必要が無くなります(後述)。 また Docker でコンテナ内のログファイルを永続化するのは面倒です。 このためアプリケーションログはファイルでは無く標準出力に出しておくと便利です。

monolog を使用した例です。

# config.yaml
monolog:
    handlers:
        main:
            type: fingers_crossed
            action_level: error
            handler: nested
        nested:
            type: stream
            path: "php://stdout"
            level: debug
        console:
            type: console

CI やテストがやかましくなるので test env ではファイルに出力するようにしておくと良いでしょう。

ジョブのAPI化

バッチサーバーなどを EB で運用する場合、Worker環境を使うことになります。 詳細はドキュメントを読めば分かりますが、Woker環境の仕組み上、HTTPリクエストを受けジョブを実行する流れになります。 逆に言えば、Web環境とまったく同じ構成でWorker環境を作ることもできます。FormBridge でも Web と Worker は同じ構成です。

例えば、バッチサーバーで以下の cron を実行している場合、Symfony の Controller にアクションを追加し処理を移行すれば良いです。スケジューリングは cron.yaml という仕組みが用意されています(後述)。

0 9 * * * php /var/www/html/app/bin/console app:hoge

非同期ジョブなどはWorker環境では以下のように処理します。

  • SQS にメッセージを送信
  • aws-sqsd がメッセージを取得し指定のpathにHTTPリクエスト
  • ジョブを実行

この場合、SQS にメッセージを送信する部分とジョブのAPI化の2箇所を対応する必要があります。

Docker のビルド

ECR

イメージのリポジトリには ECR を利用しています。タグ運用をする場合はイメージタグの変更可能性を変更不可にしておくと事故が減ります。

PHP Dockerfile

FROM composer:version as composer
WORKDIR /app

COPY . /app/

RUN composer install --no-dev

FROM php:7.2-fpm
WORKDIR /app

RUN apt-get update && apt-get install -y ...
RUN docker-php-ext-install ...
RUN pecl install ...

ADD ./path/to/ini/*.ini $PHP_INI_DIR/conf.d/

COPY . /app/

COPY --from=composer /app/vendor /app/vendor

RUN cp app/config/parameters_env.yaml app/config/parameters.yaml
RUN chown -R www-data:www-data /app

ところどころぼかしていますが、これが Symfony アプリのイメージビルドに使用している Dockerfile です。ソースコードを丸ごとコピーしているように見えますが .dockerignore で不要なディレクトリをコピーしないようにしています。parameters_env.yaml は、先ほどの環境変数を読み込むようにした parameters.yaml のことです。最後にデフォルトの PHP-FPM 実行ユーザーの www-data にパーミッションを変更しています。

Nginx Dockerfile

FROM node:version as node
WORKDIR /app

ADD ./package.json /app/
ADD ./yarn.lock /app/

RUN yarn install
RUN yarn build

FROM nginx:version

ADD ./path/to/nginx.conf /etc/nginx/nginx.conf
ADD ./path/to/default.conf /etc/nginx/conf.d/default.conf

COPY ./document-root /app/document-root
COPY --from=node /app/document-root/dist /app/document-root/dist

ところどころぼかしていますが、これが Nginx での最も単純な Dockerfile です。Nginx には静的コンテンツを管理している document-root と webpack でのビルドの成果物のみを配置します。

ところが、使用しているライブラリによってはこう単純にはいかず、実際には以下のような Dockerfile を使っています。

FROM composer:version as composer
WORKDIR /app

ADD . /app

RUN composer install

FROM node:version as node
WORKDIR /app

ADD . /app/

RUN yarn install

COPY --from=composer /app/document-root/js /app/document-root/js

RUN yarn build

FROM nginx:version

ADD ./path/to/nginx.conf /etc/nginx/nginx.conf
ADD ./path/to/default.conf /etc/nginx/conf.d/default.conf

ADD ./document-root /app/document-root
COPY --from=composer /app/vendor /app/vendor
COPY --from=composer /app/document-root/bundles /app/document-root/bundles
COPY --from=node /app/document-root/dist /app/document-root/dist

node のマルチステージビルドでは、 composer でインストールしたライブラリに同梱された js/css が document-root/js に落ち、その中から必要な js/css を yarn build の成果物に含めるようにしています。どんな状況だよという感じですね。

下の方では composer でインストールしたライブラリに同梱された js/css が document-root/bundles に落ちてくるパターンに対応しています。document-root/bundles からさらにシンボリックリンクが vendor 以下に貼られる場合もあるので vendor もイメージに含めています。vendor は js/css のみを目当てに配置しているということです。つらい。

Elastic Beanstalk 設定ファイルの管理

ディレクトリ構成

EB 管理用リポジトリは下記のようなディレクトリ構成を取ります。

.gitignore
eb
├── production
│   ├── worker
│   │   ├── .ebextensions
│   │   │   ├── ...
│   │   │   └── cache_clear.config
│   │   ├── .elasticbeanstalk
│   │   │   └── config.yaml
│   │   ├── Dockerrun.aws.json
│   │   └── cron.yaml
│   └── web
│       ├── .ebextensions
│       │   ├── cache_clear.config
│       │   ├── ...
│       │   └── migrate_db.config
│       ├── .elasticbeanstalk
│       │   └── config.yaml
│       └── Dockerrun.aws.json
└── staging
    ├── worker
    │   ├── .ebextensions
    │   │   ├── ...
    │   │   └── cache_clear.config
    │   ├── .elasticbeanstalk
    │   │   └── config.yaml
    │   ├── Dockerrun.aws.json
    │   └── cron.yaml
    └── web
        ├── .ebextensions
        │   ├── cache_clear.config
        │   ├── ...
        │   └── migrate_db.config
        ├── .elasticbeanstalk
        │   └── config.yaml
        └── Dockerrun.aws.json

production と staging の2種類のEBアプリケーションがあり、それぞれのEBアプリケーションはworker環境とweb環境を持ちます。 各環境には .ebextensions/, .elasticbeanstalk/, Dockerrun.aws.json が配置されています。

config.yaml

デプロイの利便性のため .elasticbeanstalk/config.yaml をソース管理に含めています。EB CLI コマンドeb init を実行した場合、デフォルトではソース管理から外されますがあえてソース管理しています。

# .gitignore
# Elastic Beanstalk Files
eb/**/.elasticbeanstalk/*
!eb/**/.elasticbeanstalk/config.yaml #追加

config.yaml にはEBアプリケーション名および環境名を記述します。

# config.yaml
branch-defaults:
  default:
    environment: web
    ...
global:
  application_name: formbridge-production
  ...

各環境ディレクトリに対して eb deploy を実行することでデプロイを行います。

弊社では完全な自動化はしていませんが、さらに .ebextensions 以下に config を追加したりJSONドキュメントを活用することで eb deploy だけでなく eb initeb create による自動化も達成できるはずです。

参考

Dockerrun.aws.json

各環境毎の Dockerrun.aws.json では ECR リポジトリとイメージタグの指定やログの設定を行なっています。

# Dockerrun.aws.json
{
  "AWSEBDockerrunVersion": 2,
  "containerDefinitions": [
    {
      "name": "php",
      "essential": true,
      "memory": 1024,
      "image": "xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/php-repository-name:tag",
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-region": "ap-northeast-1",
          "awslogs-group": "/aws/elasticbeanstalk/containers/php"
        }
      }
    },
    {
      "name": "nginx",
      "essential": true,
      "memory": 256,
      "image": "xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/nginx-repository-name:tag",
      "links": [
        "php"
      ],
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-region": "ap-northeast-1",
          "awslogs-group": "/aws/elasticbeanstalk/containers/nginx"
        }
      }
    }
  ]
}

Dockerrun.aws.json のコンテナ定義セクションとボリュームセクションは、Amazon ECS タスク定義ファイルの対応するセクションと同じ形式を使用できますので、logConfiguration を設定し CloudWatch へログをストリーミングしています。awslogs-group に対応するロググループを CloudWatch 上で手で作成する必要があります。

cron ジョブ

Worker環境に cron.yaml を配置すると cron のように定期的なタスクを実行することができます。

# cron.yaml
version: 1
cron:
  - name: "hoge"
    url: "/hoge"
    schedule: "0 0 * * *"

注意点としては、スケジューリングをUTCで指定する必要があることと、FIFOキューとの併用ができないことです(2019.8 時点)。実際に動かしてみると cron.yaml を元にキューにメッセージを詰める aws-sqsd がエラーを出すので単に対応していないようです。このためFIFOキューを使うWorker環境が欲しい場合、cronジョブを捌く環境とは別の環境を用意すると良さそうです。

.ebextensions

Symfony アプリの運用では cach_clear.configmigrate_db.config があると便利なので紹介します。それぞれDockerコンテナ起動後に実行されるコマンドです。

上記のディレクトリ構成ではこれらのファイルが重複し冗長な構成となっていますが、別にいいんじゃないかなという気持ちです。

# cache_clear.config
files:
  "/opt/elasticbeanstalk/hooks/appdeploy/post/99_cache_clear.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      docker exec `docker ps --no-trunc | grep container-name | awk '{print $1}'` php bin/console cache:clear -e prod
      docker exec `docker ps --no-trunc | grep container-name | awk '{print $1}'` chown -R www-data:www-data /app/var/cache

Symfony は Cache まわりがよく問題となるので、どこかのタイミングで php bin/console cache:clear を実行し Cache を削除しておくと安心です。この例では root ユーザーで実行しているので PHP-FPM の実行ユーザー www-data にパーミッションを戻しています。

# migrate_db.config
files:
  "/opt/elasticbeanstalk/hooks/appdeploy/post/10_post_migrate.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      if [ -f /tmp/leader_only ]
      then
        rm /tmp/leader_only
        docker exec `docker ps --no-trunc | grep container-name | awk '{print $1}'` php bin/console doctrine:database:create --if-not-exists
        docker exec `docker ps --no-trunc | grep container-name | awk '{print $1}'` php bin/console doctrine:migrations:migrate --allow-no-migration
      fi

container_commands:
  01_migrate:
    command: "touch /tmp/leader_only"
    leader_only: true

migrate_db.config のポイントは冗長化されていても1回だけ実行するためにリーダーインスタンスのみで実行していることです。

デプロイ

ローリングデプロイ

EB コンソールで次のような設定をしておくとデプロイ時にコンテナ起動中の状態で公開されてしまい 502 Bad GateWay といった事態を無くすことができます。

f:id:kitagawasyunta:20190815144316p:plain
ヘルスにもとづくローリングデプロイ

手順

最後にこれまでのまとめとしてデプロイの手順を紹介します。例として eb/production/web の環境にデプロイします。

# イメージタグを更新する
vi eb/production/web/Dockerrun.aws.json
# 本番用イメージのビルド
docker build -t nginx-image /path/to/nginx/Dockerfile
docker build -t php-image /path/to/php/Dockerfile
# ECRにログイン
aws ecr get-login --no-include-email --region ap-northeast-1
 # イメージを ECR にプッシュ
docker tag nginx-image:latest xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/nginx-repository:tag
docker tag php-image:latest xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/php-repository:tag
docker push xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/nginx-repository:tag
docker push xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/php-repository:tag
# eb へデプロイ
cd eb/production/web
eb deploy

あとはこれと似たようなものをそれぞれの環境についても記述し、お好みのツールとビルド環境で自動化すればおしまいです。


トヨクモでは一緒に働いてくれるエンジニアを募集しております。

採用に関する情報を公開しております。 気になった方はこちらからご応募ください。

堅牢な VPC を CloudFormation でサクッと構築する方法

こんにちは。トヨクモ開発本部インフラエンジニアの井上です。

AWS には CloudFormation というリソースの構築を自動化できるサービスがあります。 今回は CloudFormation を使って、堅牢な RDS ネットワークを構築してみたいと思います。

背景

日々データストアを運用していると、以下のようなことをやりたい場面が出てくるのではないでしょうか。

  • 本番の RDS に対して、マイグレートやスクリプトの実行時間を検証したい
  • 稼働中のEC2インスタンスからアクセスさせたくない
  • 恒久的な構成ではなく一時的に作成、削除したい

やり方は色々あると思いますが弊社では次のような方法で解決しています。

方法

アクセス元が限定された検証用の堅牢なネットワークを CloudFormation で作成し、RDS のインスタンスをその VPC ネットワーク内に構築する。

RDS は本番のリードレプリカからマスターに昇格させたものを使います。これで本番のデータが壊れることはなくなります。 さらにアクセス元を弊社IPからのみに限定することで、セキュリティーを保ちデータを外部に流出させないようにします。

構成図は以下の通り。

f:id:iriya_ufo:20190808120527p:plain
fig

ネットワークリソースの要件

  • 検証用 VPC
  • ルートテーブル
  • インターネットゲートウェイ
  • AvailabilityZone の異なるサブネット2つ
  • RDS サブネットグループ
  • 特定 IP 接続のみ許可したセキュリティグループ

RDS インスタンスの設置

  • RDS インスタンスのリードレプリカを作成する
  • 作成したリードレプリカをマスターに昇格させる
  • 検証用 VPC のネットワークに移動させる

コード

少し長いですが CloudFormation コードの全体像です。 PostgreSQL と MySQL に対応しています。

---
AWSTemplateFormatVersion: '2010-09-09'
Description: Robust RDS Network for Testing

# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - PJPrefix
      - Label:
          default: "Network Configuration"
        Parameters:
          - VPCCIDR
          - AvailabilityZone1
          - AvailabilityZone2
          - RDSPublicSubnet1CIDR
          - RDSPublicSubnet2CIDR
      - Label:
          default: "Connection Source IP"
        Parameters:
          - ConnectionSourceIP
    ParameterLabels:
      VPCCIDR:
        default: "VPC CIDR"
      AvailabilityZone1:
        default: "AvailabilityZone 1"
      AvailabilityZone2:
        default: "AvailabilityZone 2"
      RDSPublicSubnet1CIDR:
        default: "RDS PublicSubnet 1 CIDR"
      RDSPublicSubnet2CIDR:
        default: "RDS PublicSubnet 2 CIDR"
      ConnectionSourceIP:
        default: "Source IP"

# ------------------------------------------------------------#
#  Input Parameters
# ------------------------------------------------------------#
Parameters:
  PJPrefix:
    Type: String
  VPCCIDR:
    Type: String
    Default: "14.100.0.0/16"
  AvailabilityZone1:
    Type: String
    Default: "ap-northeast-1b"
  AvailabilityZone2:
    Type: String
    Default: "ap-northeast-1c"
  RDSPublicSubnet1CIDR:
    Type: String
    Default: "14.100.21.0/24"
  RDSPublicSubnet2CIDR:
    Type: String
    Default: "14.100.22.0/24"
  ConnectionSourceIP:
    Type: String
    Default: "Set Source IP xxx.xxx.xxx.xxx/32"

Resources:
  # ------------------------------------------------------------#
  #  VPC
  # ------------------------------------------------------------#
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      InstanceTenancy: default
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-vpc"

  # ------------------------------------------------------------#
  #  Internet Gateway
  # ------------------------------------------------------------#
  IGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-igw"
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref IGW

  # ------------------------------------------------------------#
  #  RDS PublicSubnet
  # ------------------------------------------------------------#
  RDSPublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref RDSPublicSubnet1CIDR
      AvailabilityZone: !Ref AvailabilityZone1
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-rds-public-subnet-1"
  RDSPublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref RDSPublicSubnet2CIDR
      AvailabilityZone: !Ref AvailabilityZone2
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-rds-public-subnet-2"

  # ------------------------------------------------------------#
  #  RDS Subnet Group and Security Group
  # ------------------------------------------------------------#
  RDSSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Robust Subnet Group
      SubnetIds:
      - Ref: RDSPublicSubnet1
      - Ref: RDSPublicSubnet2
  RDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId:
        Ref: VPC
      GroupDescription: Robust DB SecurityGroup
      SecurityGroupIngress:
      # MySQL
      - IpProtocol: tcp
        FromPort: '3306'
        ToPort: '3306'
        CidrIp: !Ref ConnectionSourceIP
      # PostgreSQL
      - IpProtocol: tcp
        FromPort: '5432'
        ToPort: '5432'
        CidrIp: !Ref ConnectionSourceIP

  # ------------------------------------------------------------#
  #  Public RouteTable
  # ------------------------------------------------------------#
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-public-rt"

  # ------------------------------------------------------------#
  #  Public Routing
  # ------------------------------------------------------------#
  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref IGW

  # ------------------------------------------------------------#
  #  Public RouteTable Association
  # ------------------------------------------------------------#
  RDSPublicSubnetAttach1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref RDSPublicSubnet1
  RDSPublicSubnetAttach2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref RDSPublicSubnet2

# ------------------------------------------------------------#
#  Output Parameters
# ------------------------------------------------------------#
Outputs:
  VPCID:
    Value: !Ref VPC
    Export:
      Name: !Sub "${PJPrefix}-vpc-id"
  VPCCIDR:
    Value: !Ref VPCCIDR
    Export:
      Name: !Sub "${PJPrefix}-vpc-cidr"
  #
  # RDS
  #
  RDSPublicSubnet1ID:
    Value: !Ref RDSPublicSubnet1
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-1-id"
  RDSPublicSubnet1CIDR:
    Value: !Ref RDSPublicSubnet1CIDR
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-1-cidr"
  RDSPublicSubnet2ID:
    Value: !Ref RDSPublicSubnet2
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-2-id"
  RDSPublicSubnet2CIDR:
    Value: !Ref RDSPublicSubnet2CIDR
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-2-cidr"
  #
  # Route
  #
  PublicRouteTableID:
    Value: !Ref PublicRouteTable
    Export:
      Name: !Sub "${PJPrefix}-public-route-id"

使い方

コードの解説をする前に、上記 yml ファイルの使い方を説明していきます。

スタックの作成

f:id:iriya_ufo:20190807174215p:plain
cf-1

最初に AWS コンソールから CloudFormation リソースにアクセスし、スタックの作成を選択します。

テンプレートのアップロード

f:id:iriya_ufo:20190807174243p:plain
cf-2

先ほどのコードをテンプレートとしてアップロードします。ファイル名はなんでもいいですが、拡張子は .yml にします。

パラメータの入力

f:id:iriya_ufo:20190807174246p:plain
cf-3

パラメータに適当な値を入れていきます。

AvailabilityZone は各リージョン毎に適したものをいれます。

東京なら ap-northeast-1b などです。ここでは例としてロンドンリージョンに作成する場合を書きました。

Source IP はアクセス元となる IP アドレスを CIDR 表記(ex. 12.34.56.78/32)で書きます。

スタックオプションの設定

f:id:iriya_ufo:20190807174250p:plain
cf-4

スタックオプションの設定は特に何も指定せずに 次へ を選択します。

ステータス

f:id:iriya_ufo:20190807174254p:plain
cf-5

数分待ってから CREATE_COMPLETE という表示が出れば完了です。

これでアクセスが限定された堅牢 VPC の出来上がりです。あとは作成された環境に RDS インスタンスのリードレプリカを作ってマスターに昇格させてから、適宜ツールなどを用いてエンドポイントに接続します。

リソースの一括削除

f:id:iriya_ufo:20190807174258p:plain
cf-6

最後に検証が終わったら、RDS のリソースを削除した上で、CloudFormation の削除を選択すると、作成されたリソースを一括で削除することができます。RDS が残った状態だとエラーになって削除できません。

説明

ポイントとなる部分だけかいつまんでコードの解説をしていきます。

メタデータとパラメータ

# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - PJPrefix
      - Label:
          default: "Network Configuration"
        Parameters:
          - VPCCIDR
          - AvailabilityZone1
          - AvailabilityZone2
          - RDSPublicSubnet1CIDR
          - RDSPublicSubnet2CIDR
      - Label:
          default: "Connection Source IP"
        Parameters:
          - ConnectionSourceIP
    ParameterLabels:
      VPCCIDR:
        default: "VPC CIDR"
      AvailabilityZone1:
        default: "AvailabilityZone 1"
      AvailabilityZone2:
        default: "AvailabilityZone 2"
      RDSPublicSubnet1CIDR:
        default: "RDS PublicSubnet 1 CIDR"
      RDSPublicSubnet2CIDR:
        default: "RDS PublicSubnet 2 CIDR"
      ConnectionSourceIP:
        default: "Source IP"

# ------------------------------------------------------------#
#  Input Parameters
# ------------------------------------------------------------#
Parameters:
  PJPrefix:
    Type: String
  VPCCIDR:
    Type: String
    Default: "14.100.0.0/16"
  AvailabilityZone1:
    Type: String
    Default: "ap-northeast-1b"
  AvailabilityZone2:
    Type: String
    Default: "ap-northeast-1c"
  RDSPublicSubnet1CIDR:
    Type: String
    Default: "14.100.21.0/24"
  RDSPublicSubnet2CIDR:
    Type: String
    Default: "14.100.22.0/24"
  ConnectionSourceIP:
    Type: String
    Default: "Set Source IP xxx.xxx.xxx.xxx/32"

まずはメタデータとパラメータの部分です。ここで書いた内容はスタック作成の際にパラメータとして入力できるようになります。

CloudFormation を書く中でマジックナンバー的な要素を書かなくてはならないときはパラメータとして追い出してやるといいです。

ここでは VPC の範囲 複数リージョンの対応のための AZ アクセス元 IP をパラメータとして分離しました。

RDS サブネットとサブネットグループ

  # ------------------------------------------------------------#
  #  RDS PublicSubnet
  # ------------------------------------------------------------#
  RDSPublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref RDSPublicSubnet1CIDR
      AvailabilityZone: !Ref AvailabilityZone1
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-rds-public-subnet-1"
  RDSPublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref RDSPublicSubnet2CIDR
      AvailabilityZone: !Ref AvailabilityZone2
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-rds-public-subnet-2"

  # ------------------------------------------------------------#
  #  RDS Subnet Group and Security Group
  # ------------------------------------------------------------#
  RDSSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Robust Subnet Group
      SubnetIds:
      - Ref: RDSPublicSubnet1
      - Ref: RDSPublicSubnet2
  RDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId:
        Ref: VPC
      GroupDescription: Robust DB SecurityGroup
      SecurityGroupIngress:
      # MySQL
      - IpProtocol: tcp
        FromPort: '3306'
        ToPort: '3306'
        CidrIp: !Ref ConnectionSourceIP
      # PostgreSQL
      - IpProtocol: tcp
        FromPort: '5432'
        ToPort: '5432'
        CidrIp: !Ref ConnectionSourceIP

サブネットグループ ( Type: AWS::RDS::DBSubnetGroup )

次に RDS のサブネットについて見ていきます。 RDS インスタンスの配置するネットワークは最初は少々戸惑います。

RDS は RDS 専用のサブネットグループという場所に設置するのですが、これは RDS リソースから作成します。

サブネットグループとは通常の複数のサブネットを1つにまとめたものです。

Multi AZ にするには2つ以上のサブネットを選択して、サブネットグループを作成する必要があります。

セキュリティグループ ( Type: AWS::EC2::SecurityGroup )

そしてセキュリティグループの作成です。

RDS リソースに対するセキュリティグループなのに AWS::EC2::SecurityGroup でいいのかという感じがしますが、これで大丈夫です。

DB セキュリティグループと勘違いして作成しがちですが、DB セキュリティグループは VPC 内にない EC2-Classic DB インスタンスへのアクセスを制御するためのものなので今回は関係ありません。

セキュリティグループはインバウンドで MySQL と PostgreSQL のポートをアクセス元を限定して解放しています。アクセス元は CidrIp: !Ref ConnectionSourceIP で関連づけます。

参考

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/Overview.RDSSecurityGroups.htmldocs.aws.amazon.com

出力パラメータ

# ------------------------------------------------------------#
#  Output Parameters
# ------------------------------------------------------------#
Outputs:
  VPCID:
    Value: !Ref VPC
    Export:
      Name: !Sub "${PJPrefix}-vpc-id"
  VPCCIDR:
    Value: !Ref VPCCIDR
    Export:
      Name: !Sub "${PJPrefix}-vpc-cidr"
  #
  # RDS
  #
  RDSPublicSubnet1ID:
    Value: !Ref RDSPublicSubnet1
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-1-id"
  RDSPublicSubnet1CIDR:
    Value: !Ref RDSPublicSubnet1CIDR
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-1-cidr"
  RDSPublicSubnet2ID:
    Value: !Ref RDSPublicSubnet2
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-2-id"
  RDSPublicSubnet2CIDR:
    Value: !Ref RDSPublicSubnet2CIDR
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-2-cidr"
  #
  # Route
  #
  PublicRouteTableID:
    Value: !Ref PublicRouteTable
    Export:
      Name: !Sub "${PJPrefix}-public-route-id"

最後にアウトプットパラメータについてです。

これは今回の作業においては必要ない部分ですが、設定しておくと別のテンプレートからリソースを呼び出せたり、AWS コンソールて手軽に id などが確認できるので便利です。

まとめ

CloudFormation の利用例は Web サービスインフラの構築の例とかが多いと思いますが、拡張性がある形で設計するのは結構たいへんです。今回のように限定的な使われ方をするけど何回か使い回すような利用シーンだと手軽で便利に使えるのではないかと思います。


トヨクモでは一緒に働いてくれるエンジニアを募集しております。

採用に関する情報を公開しております。 気になった方はこちらからご応募ください。

Clojureエンジニアが育休を4ヶ月とりました

こんにちは。開発本部の飯塚です。 4月に第2子が生まれ、4ヶ月の育児休暇を取得しました。

育休の入るまでにしたこと

開発本部では毎週目標を決めて翌週頭に何ができたか/できなかったなどを振り返るのですが、出産予定日がある程度確定した時点で、育児休暇に入るまでの計画を作成して共有・レビューしました。

この計画作成でありがたかったのが、育休に入る予定の2週間前から待機期間として雑務を中心とした計画とし、いつでも育休に入れる状態にさせていただいたことです。 これにより予定日が近づいてきても途中の業務や引き継ぎに関する不安を抱えなくて済み、精神衛生上とても良かったです。 なお実際に生まれたのが予定日の5日前だったので(私目線では)スムーズに育休に入れたと思っています。

育休を取得して良かったこと

この手の話はよくある話になりがちで、すぐ大きくなってしまって貴重な赤ちゃん時期を、短いながら夫婦2人で育児できたことはとても素敵なことでした。

それ以外だと上の子の幼稚園入園が出産とちょうど重なったことで、入園後3ヶ月ほど幼稚園イベントは私1人で参加していたのですが、以下のようなことができたのは貴重だったかなと思います。

  • 母の日イベントに唯一父1人だけで参加する
  • 顔を覚えられているので幼稚園近くの公園に1人でいても通報されない

理想と現実

元々、上の子が幼稚園に行っている間の1時間程度を使って少しだけ業務を行う予定だった(そういう命令があったわけではなく妻とも相談して了承済みの話)のですが、幼稚園の送迎・買い物・家事・下の子の面倒を見るなどしていると全然時間が取れず、また体力の消耗も激しかったので結局業務は全くできませんでした。 これは完全に2人目の育児の大変さを甘く見ていた結果です。

育休の長さについて

制度的には1年取得しても問題なかったのですが、私は妻とも相談して4ヶ月で復帰しました。 ただこの4ヶ月という期間も、元々暫定で2ヶ月という申請をした上で2回延長した結果になります。 下の子の成長具合や上の子の幼稚園の様子、また妻の体力などを考慮しつつ夫婦納得の上で期間が決められたのは良かったなぁと思います。

復帰に際しての不安

上の子の時は転職前かつ出産時の諸々のトラブル関連で育児休暇を取らずに終わってしまったので、長期間の育児休暇をとるのは初めてでした。 休暇中に取り残される・ちゃんと復帰できるだろうかといった不安が無いわけでは無かったのですが、それ以上に家事・育児が大変だったので考える余裕もあまり無かったのが正直なところです。

実際に復帰してみて(まだ数日しか経っていませんが)ですが、仕事っぽいことはできているつもりであることと、4ヶ月いなくても温かく迎えてくれる職場・メンバーに恵まれたので杞憂だったなという感想です。

最後に

上の子の幼稚園入園に伴って生活リズムが早くなったので、復帰後は 09:00-18:00 勤務から 07:00-16:00 の時差勤務に変更しました。 これまでだと朝に数分顔を合わせて、家に着いたら夕食が終わっているという状態になってしまいますが、時差勤務により子供と顔を合わせる時間が増え、帰宅後に家事も手伝えて、満員電車も(多少)回避できてとみんなハッピーになりました。

育児休暇に入るまでの待機期間に始まり、育児休暇期間の延長、復帰後の勤務形態の変更などいろいろと父として育児しやすい環境が整いつつあり助かったので、育休をとったからと言って幼稚園でママさんたちに「すごーい!」と言われないようになると良いですね。


トヨクモでは一緒に働いてくれるエンジニアを募集しております。

採用に関する情報を公開しております。 気になった方はこちらからご応募ください。