概要
Infrastructure as Code などの文脈で、定義ファイルをリポジトリ管理下におき、継続的にデプロイしている場合、
定常的な変更においては、リポジトリ管理のオーバヘッドが大きくなりがちである。
ここでは、ChatBot を使って簡単に回せるようにする解決策の構想と実装例の一部を示す。
背景
インフラ等の構成をコードで記述し、それら定義ファイルを Git リポジトリ管理するとき、
理想的には環境Aの設定=ブランチAのコードであるべきなので、
なるべくならこの関係が自動的に保たれるようにしたい。
たとえば master にマージされると本番環境へのデプロイが開始されるように継続的デプロイしたい。
そうすると、勝手に設定変更されないよう、更新フローを明確化したくなり、
GitHub や GitLab で、プルリクエスト/マージリクエストを使ってレビューするような運用にしたくなる。
課題
しかし、いちいちそのようなフローを回すのが面倒に感じるかもしれない。
運用者が Git に明るいとは限らない(むしろ失敗が増えるかもしれない)し、
リポジトリ管理していなかったときと比べて、次のように余計な作業(★)が増える。
- ブランチを切る。★
- 設定を変更する。
- 変更をコミットする。★
- プルリクエスト/マージリクエストを送る。★
- レビューを依頼する。
- 承認を得られたら適用(マージ)する。
- 設定が適用されたことを確認する。
解決策
- ChatBot を使うことで、インターフェイスを限定し、習熟不要にする。
- 作業(★)を前後の作業にまとめてしまうことで、リポジトリ管理が業務を極力邪魔しないようにする。
次のようなフローになる:
- ChatBot に設定変更を告げると、ブランチを切ってコミットする。
- ChatBot にデプロイを告げると、プルリクエスト/マージリクエストを作成する。
- レビューを依頼する。
- 承認が得られたら、適用(マージ)する。
- 設定が適用されたことを確認する。
具体例
なんらかのリストをリポジトリ管理していて、リストへの追加、リストからの削除を行う業務があるとする。
たとえば、ユーザのリストや、DNS のゾーンファイル、DHCP の設定ファイルなどの変更作業が考えられる。
リストへの追加は、たとえば add foo foo@example.com
のように送り、
リストからの削除は、たとえば remove foo
のように送ると、
ChatBot が dev ブランチを切って、リストを更新するコミットを作成する。
(もし dev ブランチがすでに存在すれば、そこにコミットを作成する。)
更新コミットが作成出来たら、たとえば deploy
のようにデプロイを指示すると、
ChatBot がプルリクエスト/マージリクエストを作成し、URLを返す。
あとは、承認者にメッセージを送り、承認されたらマージすればよい。
承認・マージを ChatBot に代行させることもできる。
実装例
Hubot や GitLab での実装例の一部を示す。
リストへの追加は、次のように反応する:
robot.respond(/add\s+(.+)\s+(.+@.+)/i, res => {
let username = res.match[1];
let emailAddress = res.match[2];
//...
});
コミットの作成は発言者の名義で行うため、
発言者の名前(res.message.user.name
)から GitHub/GitLab 上のユーザを取得する。
GitLab であれば、次のように GitLab API を叩いて取得する:
http.get('/api/v4/users?username=' + username, res => {
//...
});
コミットの作成には、更新元のファイルの中身が必要である。
更新するブランチから、最新のファイルを取得する。
GitLab であれば、次のように GitLab API を叩いて取得する:
http.get('/api/v4/projects/' + PROJECT_ID + '/repository/files/path/to/list?ref=' + branch, res => {
//
});
ファイルの中身を更新する方針は2通り考えられる。
- デコード(Base64、UTF-8など)後、改行で
split
して、 単純に文字列の配列として処理する。 柔軟性が高いが、煩雑になると思われる。 - 何らかのクラスに変換する。 保守性が高いが、ファイルの中身の書き方に制限がかかると思われる。
前者の場合、削除は、たとえば次のように処理する:
// content はファイルの中身を1行ずつ配列に入れたもの。
// username を含む行番号 index を取得する。
let index = content.findIndex(x => new RegExp(username).test(x));
if (index < 0) throw username + "が見つかりませんでした。";
content.splice(index, 1); // index 行目の要素を除去する。
後者の場合、add/remove
メソッドや toFileContent
メソッドなどを実装しておき、
これらを呼び出して更新処理やコミット作成時の文字列の書き出しを行う。
注意点
- 変更作業を定型化できない場合、ChatBot への実装パターンが増えてしまうので、 結局人手で柔軟に対応したほうが早いかもしれない。