デプロイ今昔

こんにちは。はてなのアプリケーションエンジニアの id:onk です。

最近、若手エンジニアを中心に、いろいろな技術を見つめ直すワーキンググループをやっています。今回は、その中から「デプロイ」の会で発表されたことをまとめました(なお、私は会のとりまとめをやっている非若手です)。

デプロイのライフサイクルの違い

デプロイはコンテナ以前・以降でかなり様相を変えたが、そのライフサイクルは今も昔も次の 3 つのレイヤーで考えられる。

  • Infrastructure Platform
  • Application Runtime Platform
  • Applications

例えば Mac に開発環境を入れると考えるなら、それぞれのレイヤーは

  • Infrastructure Platform
    • OS, ミドルウェアとその設定
    • 一度入れたら brew upgrade ぐらいでしか変わらない
  • Application Runtime Platform
    • 言語のライブラリ
    • 開発が進むと追加されたり更新されたりする
  • Applications
    • アプリケーションのソースコード
    • ずっと変わり続ける

このようになり、変更頻度がかなり違う。

Infrastructure Platformでのデプロイ

コンテナ以前の世界では、Puppet, Chef, Ansible, Packer といったツールが主な登場人物だった。

Ops がサーバを調達し、これらのインフラ自動化ツール群で諸々を設定して、Dev に渡す。という姿がよく観測されていたと思う。

このレイヤーのデプロイツールは、今の開発ツールで言うなら CloudFormation, CDK, Terraform が対応している。

参考 URL:

Application Runtime Platformでのデプロイ

OS があって、ネットワークが繋がっていて、ミドルウェア(例えば Perl の最新バージョン)が入っていたらアプリケーションが動作するかというと、そんなことはなく、CPAN モジュールを入れないと動かない。

これを Infra として置くのか、Runtime として置くのか? つまり、Ops と Dev の境界線は会社によって違うし、開発環境と本番環境で違ったりもするだろう(CPAN モジュールを rpm にして Ops が管理している、といった体制を想像しています)。

ここを何でデプロイするかは曖昧で、はてなでは Capistrano および Chef の実行時の両方で走るようになっていた。

Applicationsのデプロイ

月次デプロイになると毎回おっかなびっくりになってしまうので、それ以上の頻度でデプロイされるのを期待している。

ここに使われている/いたのが Capistrano で、モダンなコンテナ環境だと……?(後で述べます)

デプロイ方式はどのように変化してきたか

In place から Blue/Green へ

詳しくは riywo さんのめっちゃ良い記事「Blue/Greenデプロイとは?」を見てほしいんだけど、この変化は次のように考えられる。

  • In place
    • インスタンスはそのままに、新しいリビジョンのアプリのみをその場で反映させること
    • 具体的なツールとしては EC2 と capistrano
  • Blue/Green
    • 新しいリビジョンのアプリ用に、新しいインスタンスを構築して入れ替えること
    • オンプレ時代でも実現することはできて、そのためにロードバランサが存在している
    • クラウド時代になると調達コストが限りなく低くなるので一世を風靡した

Immutable Infrastructure という考え方

  • デプロイのたびに全てを新しくする
  • 大枠で言うと In Place が Mutable、Blue/Green が Immutable(なんだけど、Blue/Green は切り換え方式を表す言葉なので正しく使い分けましょう)
  • デメリットとしては、ライフサイクルの違いを無視していること
    • ちょっとした修正(例えば超微細な文言修正)でも、常に AMI から作り直しになるため、時間がかかる

オートスケールへの対応

  • 全自動で Applications がデプロイされた状態の EC2 が追加されてほしい
  • cap で、デプロイ先サーバを指定して人間がデプロイを実行している状況だと無理
  • ゴールデンイメージ を焼くようになる
    • 起動するだけで良い感じになる AMI
    • デプロイ毎に AMI を焼くのではなく、Runtime までを焼いておいて、インスタンス起動時に Applications を取得する形にすることもある

push 型デプロイと pull 型デプロイ

  • push 型
    • cap で、デプロイ先サーバを指定してデプロイする
    • デプロイタイミングで、何にデプロイするかを指定する必要がある(=push)
  • pull 型
    • インスタンス自身が最新バージョンを取得する
    • オートスケールできる!

この push 型デプロイ、pull 型デプロイというのは 2016年〜2017 年ぐらいの言葉で、それ以降は production 環境もコンテナで提供するようになったことから「Runtime と Applications を合わせたものを配備する」ことがデプロイになり、コンテナ化により Runtime 以上は絶対に Immutable になった。

コンテナによるデプロイの現況

コントロールプレーンの普及が、エポックメイキングな出来事(主に ECS や k8s を指しています)。

例えば、次の ECS のコンポーネントを見てみよう。

Manager と Scheduler がコントロールプレーン、Cluster と Agent がデータプレーンと考えればだいたい合ってる。

この後、データプレーンとして Fargate が登場して、フルマネージドな ECS Cluster の上で、コントロールプレーンである ECS が良い感じに管理してくれるようになる(on EC2 とは「EC2 を束ねてデータプレーンにし、EC2 上で docker が死んだりしたら自分でなんとかするし、apt upgrade も自分でやる」です)。

コントロールプレーンとしての概要は、上記資料の 21 ページ から 36 ページを見ると分かるはず!

コントロールプレーンによって何が変わったか

コントロールプレーンがあることによって、我々の作業は以下のように変わる。

  • サーバを増やしたいとき
    • DesiredCount を増やすと ECS が勝手にコンテナを立てて LB に登録してくれる
  • 新バージョンをデプロイしたいとき
    • 新しい TaskDefinition を作ると、それに合うように ECS が新コンテナを立てて旧コンテナを落としてくれる

アプリケーションエンジニアがメンテナンスできるのは TaskDefinition と DesiredCount だけで、「指定通りに保ち続ける」作業をコントロールプレーンが行う。この保ち続ける作業を Reconciliation loop と言う。

参考URL: Infrastructure as Dataとは何か | SOTA

ECS におけるデプロイ手順

ECS におけるデプロイは次の手順になる。

  1. コンテナイメージを作り、ECR に登録する
  2. タスク定義を更新する
  3. ECS が、サービス内のタスクを入れ替える

ここで、3 は ECS に任せるしかできない(デプロイという概念が「宣言的なもの」に変化した)。また、1 と 2 は CI/CD, IaC と同じ文脈になる(CFn, CDK で管理できるので)。

ところで別の話題だけど、宣言したら勝手に動くので、デプロイの各イベントが追えるような環境を用意する必要があるね。

デプロイを分解して考える

コンテナ以前のデプロイツールでは、以下の 3 フェーズに分解する考え方があった。

  • fetch
    • source を push すること
  • prepare
    • cpm install
  • switch
    • symlink の張り替え

コンテナ時代では、Immutable な Blue/Green が前提なので fetch と prepare はあまり意識する必要がなくなり、switch だけを考えれば良くなった(その代わりに Build, Ship, Run という分解がされるようになった。後述)。

この switch には、いくつかの戦略がある。

Switch 戦略

デプロイは、大枠で以下の流れに沿う。カナリア(canary)リリースして、良かったら本リリース、そして Blue を落とす。

Pipeline - Application deployment - Concepts - Spinnaker

本リリース時の切り換え方にいくつかパターンがあり、「デプロイ戦略の定義」という翻訳記事にまとまっている。

一言で言うと次のようになる。

  • 全部をまるっと切り換えるのを Red/Black と言う
  • 1 台だけ出してみるのを canary Deploy と言う
    • もっと言うと canary には「よさそうだったので全台切り換えます!」「ダメだったので rollback します」を全自動でやってくれるのを期待している

AWS CodeDeploy による自動化

CodeDeploy も「コントローラ」と言える(Reconciliation loop で保ち続ける存在を「コントローラ」と呼びます)。

Build, Ship, Run

2017 年ぐらいには、Docker 社のモットーとしてこれが掲げられていたんだけど。

少なくとも「Build と Release (switch) をそれぞれ独立して実行できるようにしておく」というのが、まず最初にやること。そして「Release および Run は任せる」というのが、何度も言うけど、一番大きな変化。

複数バージョンがほぼ絶対に共存する世界

いずれにせよ「2 バージョンをバツンと切り換える」というのはとてもやりづらくなっている(コントロールプレーン任せ)ので、2 バージョンが共存しても大丈夫な状態を維持しながらデプロイするという考え方により強くシフトする必要がある。

例えば nullable で add_column しておくとか、暗号化キーを変えたときに、暗号文字列を新旧両方のキーで復号を試すとか。前方互換性と後方互換性の両方が必要。

バージョン競合の話はクライアントにコードを配っている場合に特に問題が起きがちで、ネイティブアプリでは、古いクライアントでアクセスしてきたら強制アップデートでストアに飛ばす、みたいな手段がよく採られている。SPA も、ブラウザに JS を既に配っているので、何らかのタイミングでブラウザをリロードさせる必要がある。

まとめ

  • コントロールプレーンが Reconciliation loop で保ち続ける世界に変わった
  • Switch 戦略は ssh で流し込むコマンドを工夫すればなんとかなる世界ではなくなり、コントロールプレーン上で何らかのツール(CodeDeploy 等)を用いて実現する
  • ローリングデプロイ任せは「コントロールできている」感じが少ないので、canary デプロイができる状態までは作っておくとよい