環境依存情報をコードから分離する

この記事は [Recruit Engineers Advent Calendar 2017](https://adventar.org/calendars/2324) の4日目の記事です。

# 1.はじめに
RTCのy_kabutoyaです。
リクルートが提供しているサービスに対してSREとして構築や運用改善を担当しています。

本記事では日々発生する運用業務のうちシステムリリース[^※1]について日々の取組からエッセンスを紹介していきたいと思います。

### ■ The Twelve-Factor App
システムリリースを行う上で運用を見据えたアプリケーション設計/構築のバイブルである[Twelve-Factor App](https://12factor.net/ja/)の思想を取り入れています。
とはいえ、全ての要素を取り込むことは現実的に難しく、主に以下項目を意識しながらシステムリリースに取り組んでいます。

* [I. コードベース](https://12factor.net/ja/codebase)  
コード自体は1つのリポジトリで管理し、配布先(実行先)は複数存在するという考え方。  
アプリケーションのコードについてはこの方針が(利用可能なツールも含めて)浸透していますが、その他のシステムの構成要素(DBやディレクトリ等)に関しては「実機にしか資源がない」状況に陥りやすいと考えています。
* [V. ビルド、リリース、実行](https://12factor.net/ja/build-release-run)  
ビルド(コードから実行可能形式の配置)、リリース(ビルド結果と設定の結合)を経て対象となる環境で実行されるという考え方。  
特にリリース時に適用される設定は後述の「開発/本番一致」で実現しきれていない部分を補うために、設定自体と環境依存情報を分離する必要があると考えています。
* [X. 開発/本番一致](https://12factor.net/ja/dev-prod-parity)  
開発/ステージング/本番といった環境間の構成差異とそれぞれの環境の差異がある時間を極力短くし続けるという考え方。  
しかしシステム規模が大規模化するほど*時間*のギャップは大きくなり、逆にシステム規模が小さいほど環境間の構成差異が大きくなる[^※2]と考えています。

上記を踏まえてシステムの構成要素のコード化と、コードと環境依存情報の分離を行っています。

### ■ 想定しているシステム構成
シンプルにするために一般的なWeb/DB構成を想定構成としています。
ただ、環境間の構成差異が複数ある環境と見立てています。

<img width="600" alt="想定環境.png" src="https://qiita-image-store.s3.amazonaws.com/0/104757/241fb8ad-8985-36ab-abbf-1749ce8e491a.png">

### ■ コード化と環境依存情報分離の対象
システムを構成する要素のうち、コード化可能な項目は無数にあます。そして全てに対応するのは非現実的です。
そのため、日々のシステムリリースにおいて頻度が高く、かつ環境依存情報が含まれやすい項目を対象としています。

# 2.コード化と環境依存情報の分離
ここでは環境依存情報が含まれやすい項目を「ミドルウェア」「ディレクトリ」「DBオブジェクト」の3つをピックアップしました。

### ■ ミドルウェア
ミドルウェアはRPMに代表されるパッケージ管理の仕組みが整っておりビルドに対する問題は発生しにくいかもしれません。
しかし、コンフィグ情報については機能の増加やサービスの特性(アクセスの増加やピーク時間帯の変更等)に伴って随時でチューニングが発生します。
そのため、[Ansible](https://www.ansible.com/)を利用してコード化と環境依存情報の分離を行っています[^※3]。

Ansibleの利用においては環境依存情報を適切に分離するために[alternative-directory-layout](http://docs.ansible.com/ansible/latest/playbooks_best_practices.html#alternative-directory-layout)に準拠した構成を取っています。
varsをパッケージ化して定義し、configに[Jinja2テンプレート](http://jinja.pocoo.org/docs/2.10/)を適用することで環境依存情報の分離とコード管理を行っています。

また、Playbookの存在するトップディレクトリをサービス単位で、roles配下のディレクトリをMW単位でそれぞれリポジトリとして管理しています。
roles配下(MWのタスク/コンフィグ)はサイトに依存しない構成がほとんどだからです。
逆にサイトに依存する情報はinventories配下のvarsに配置し、実行時にMWのタスク/コンフィグにバインドすることで環境依存を適用する作りとしています。

<img width="600" alt="プレゼンテーション1.png" src="https://qiita-image-store.s3.amazonaws.com/0/104757/9dd68e78-e688-132a-b317-454e38950f9c.png">

### ■ ディレクトリ
JavaのWARのようにパッケージされるアプリケーションは以下のディレクトリはアプリケーション用のリポジトリの中で管理されていますが、静的コンテンツや一時的なユーザファイルアップロード先としてアプリケーションリポジトリの範囲外のディレクトリが必要となる場合があります。
そして前述の想定環境のように1つの環境に複数の(同一)アプリケーションを配置する必要がある場合、ディレクトリ自体をリポジトリにて管理することが困難です。

そのため、ディレクトリ自体をYAMLで表現[^※4]し差分を環境に反映するツールをPythonで独自に実装しています。
以下のサンプルでは`my_toplevel`を別の環境設定YAMLファイルで(dev1,2,3のように)定義することで実環境適用時に環境依存情報を適用する作りとしています。

```yaml
- rootPath: my_toplevel
- test: {owner: siteuser, group: mygroup, permission: 777}
- #Lv2
    - test1: {owner: siteuser, group: mygroup, permission: 777, description: XXX用ディレクトリ}
    - test2: {owner: siteuser, group: mygroup, permission: 777, description: XXX用ディレクトリ}
    - #Lv3
        - dir118: {owner: siteuser, group: mygroup, permission: 777, symlink: {src: /my/path/from}}
        - dir119: {owner: siteuser, group: mygroup, permission: 777}
        - dir120: {owner: siteuser, group: mygroup, permission: 777}
        - #Lv4
            - dir121: {owner: siteuser, group: mygroup, permission: 777}
        - dir130: {owner: root, group: mygroup, permission: 777}
        - #Lv4
            - dir131: {owner: siteuser, group: mygroup, permission: 777}
        - relationPath1: {owner: root, group: mygroup, permission: 777, symlink: {src: dir120/dir121}}
        - relationPath2: {owner: siteuser, group: mygroup, permission: 777, symlink: {src: dir130/dir131}}}
```

### ■ DBオブジェクト
DBのではデータベース(またはスキーマ)相当を1つのインスタンス内に複数配置する場合があります。
その中に作成されるDBオブジェクト(テーブルやインデックス等)は構成は同じでもテーブルスペース等は環境に応じて異なる場合があります。

そのため、[Liquibase](http://www.liquibase.org/)にてDDLとDMLをコード化し管理しています。
Liqubaseにはplugin形式で[拡張](https://liquibase.jira.com/wiki/spaces/CONTRIB/overview)することが可能です。独自に拡張機能を作成し、その中で特有の処理(テスト機能の追加など)を実装し、適用対象のDDL(SQL)[^※5]を管理＆テストする施策も追加しています。
また、Liquibaseでは`${name}`形式で環境に適用するDDLの値を置換できるため、環境依存情報はproperties fileとして別管理することで環境依存を適用する作りとしています。

<img width="900" alt="運用改善.png" src="https://qiita-image-store.s3.amazonaws.com/0/104757/52610f9a-3cc5-69fa-7d79-d358c4738ed0.png">


# 3.統合的なリリース
上記までで示したコード(に加えてアプリケーションコード)をビルドするための方式は実行環境も含めて、全て異なっています。
そのため、各環境へのリリースには[Jenkins 2.0](https://jenkins.io/2.0/)を利用しています。
Jenkinsで定義するビルドジョブも[Jenkins Pileline](https://jenkins.io/doc/book/pipeline/)(Scripted Pipeline)を利用してコード化と環境依存情報の分離を行っています。

Jenkinsサーバ(上記のAnsibleサーバを兼ねる)にリポジトリ情報を集約。必要に応じてBuild Agentに資源を配置(とリリース処理)を行う仕組みにしています。

<img width="550" alt="プレゼンテーション1.png" src="https://qiita-image-store.s3.amazonaws.com/0/104757/9dc461db-784c-9da9-f7bd-1eabd5236128.png">

ただ、Jenkins Pilelineを利用したコード化は行っていますが、人が操作を行う必要が有るため、パラメータ定義を行っています。このパラメータについてリポジトリ管理の方法(≒ビルドパラメータ自体のコード化)の方法が調べきれておらず。。。
誰か良い方法があれば指摘いただけると嬉しいです。

# 4.最後に
実際に稼働しているシステム装着を行う際の苦労や気をつけた点について書くつもりが、テーマを大きく取りすぎてしまったため、長文の割には全体的にぼやっとした話になってしまいました。。。
各項目のより踏み込んだ内容について記載する機会を作って改めて記事に出来ればと思います。

最後までお読みいただいた皆さん、ありがとうございました！

## (注釈)
[^※1]: 世の中に機能を提供する際の、M/W〜アプリケーションまでの変更を反映させる作業を「システムリリース」としています。
[^※2]: システム規模が小さくなるほど予算の都合等から開発環境に複数の案件を並行で実行可能な状態作らなければならなくなると考えています。
[^※3]: 実際にはスケールしながらインフラ構築を自動化することを主目的としてAnsibleを導入しているためOSのコンフィグやMW導入も行っています。
[^※4]: ディレクトリの階層表現とYAMLの配列の表現に親和性(可読性)の高さがあると考えYAMLを採用しています。
[^※5]: LiquibaseはDBマイグレーションを可能とするため、Liquibase固有のDSLを利用してDBオブジェクトを管理することになっていますが、独自DSLのキャッチアップや適用される際に構築されるSQL形式が直接見ることが出来ない点をリスクと捉え、Liqubaseで直接SQL分を記述する形式に振り切っています。