コンテナイメージができたら、実際にどこで動かすのか

イメージをビルドし、脆弱性スキャンを実施し、レジストリにプッシュしました。ここからが、パイプラインを実際のデプロイに変える瞬間です。つまり、ユーザーがアクセスできる場所でそのコンテナを実際に動かすことです。

コンテナの実行方法は、どこで動かすかに完全に依存します。シングルサーバーとKubernetesクラスターは、どちらもコンテナを動かすという点では似ていますが、運用体験はまったく異なります。この選択は、アップデートの方法、障害からの復旧方法、そして新しいバージョンがリリースされるたびにチームが手動で調整する必要がある作業量に影響します。

シングルサーバーでのコンテナ実行

シングルサーバーへのデプロイは単純に見えます。サーバーにSSHで接続し、プロモートしたイメージタグでdocker runを実行すれば、アプリケーションが起動します。デモ環境であれば、これで話は終わりです。

実際には、シングルサーバーで1つのコンテナだけが動いていることはほとんどありません。通常は、アプリケーションコンテナ、データベースコンテナ、キャッシュ、キューイングワーカーなどが動いています。これらのコンテナは正しい順序で起動し、適切なネットワークで相互に通信し、いずれかがクラッシュした場合の処理を行う必要があります。ここでdocker-composeが役立ちます。すべてのサービス、その依存関係、ポート、再起動ポリシーを1つのファイルに定義します。1つのコマンドで、正しい順序ですべてを起動できます。

本当の課題は、アプリケーションバージョンを更新する必要があるときに現れます。シングルサーバーでは、古いコンテナを停止し、新しいコンテナを起動します。その間、アプリケーションはリクエストを処理できません。アプリケーションが実際のユーザーに使用されている場合、そのダウンタイムは問題になります。

ダウンタイムを減らす最も簡単な方法は、2つのコンテナを並行して実行することです。新しいバージョンが起動している間、古いバージョンを稼働させ続けます。新しいコンテナが接続を受け入れられるようになったら、トラフィックを新しいコンテナに切り替え、古いコンテナを停止します。これが最も基本的なローリングアップデートです。スクリプトを使って手動で行うことも、NginxやTraefikのようなリバースプロキシを使ってトラフィックの切り替えを処理することもできます。

しかし、ローリングアップデートパターンを使っても、シングルサーバーにはハードリミットがあります。サーバー自体がダウンすると、アプリケーションもダウンします。ホストOSにセキュリティパッチを適用する必要がある場合、ダウンタイムをスケジュールする必要があります。小規模チームが使う内部ツールであれば、このトレードオフは許容できることがよくあります。顧客向けアプリケーションでは、通常は許容できません。

Kubernetesでのコンテナ実行

Kubernetesは、シングルサーバーデプロイの問題を解決済みの問題として扱い、その上に構築します。コンテナを直接管理するのではありません。望ましい状態を記述したDeploymentオブジェクトを定義します。どのイメージを実行するか、レプリカ数、使用するヘルスチェック、アップデートの実行方法などです。

Deploymentのイメージタグを更新すると、Kubernetesはすべてを停止して再起動するわけではありません。新しいイメージで新しいPodを作成し、それらがヘルスチェックに合格するのを待ってから、古いPodを徐々に終了します。このプロセス全体を通じて、常に少なくとも1つのPodがトラフィックを処理しています。ユーザーはサービス中断を認識しません。

PodはKubernetesの最小単位です。1つ以上のコンテナを実行できますが、重要な考え方は、Podは一時的であるということです。Kubernetesは必要に応じてPodを作成、破棄、異なるノードに移動します。Podがどの特定のサーバーで実行されているかを考える必要はありません。クラスターがその処理を行います。

シングルサーバーとKubernetesの違いは、単により多くのトラフィックにスケーリングすることだけではありません。それは、誰が調整を担当するかです。シングルサーバーでは、起動順序、再起動ポリシー、障害処理を自分で決定します。スクリプトを作成するか、docker-composeを使用してそれらの決定を強制します。Kubernetesでは、オーケストレーターがその調整を担当します。Podのヘルスを定期的にチェックし、失敗したPodを再起動し、ノードがダウンしたときにPodを正常なノードに再分散します。

この責任の移行は、チームの運用方法を変えます。コンテナのライフサイクルを処理するスクリプトを作成するのをやめ、望ましい状態を記述したDeploymentマニフェストを作成し、クラスターにその状態に到達する方法を任せるようになります。

以下は、実際の最小限のDeploymentマニフェストです。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: app
        image: my-registry/my-app:v1.2.3
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10

このマニフェストは、Kubernetesに対して3つのレプリカを実行し、一度に1つずつ更新し、Podの/healthエンドポイントが正常に応答した後にのみトラフィックをルーティングするように指示します。

2つの選択肢の選び方

シングルサーバーとKubernetesの選択は、技術的な純粋性のテストではありません。運用要件に基づく判断です。

以下のフローチャートは、どのパスが状況に適しているかを判断するのに役立ちます。

flowchart TD A[開始] --> B{高トラフィックまたは ゼロダウンタイムが必要?} B -- はい --> C{管理する 複数サービスあり?} B -- いいえ --> D{小規模チーム, シンプルなアプリ?} C -- はい --> E[Kubernetesを使用] C -- いいえ --> F[K3sまたは MicroK8sを検討] D -- はい --> G[シングルサーバー docker-composeを使用] D -- いいえ --> H{クラスター運用の 成熟度はある?} H -- はい --> E H -- いいえ --> G

以下の場合、シングルサーバーとdocker-composeを使用します。

  • アプリケーションが小規模な内部チームによって使用されている。
  • アップデートやメンテナンスのためのダウンタイムが許容される。
  • 管理するサービスが1つか2つである。
  • 水平スケーリングの必要がない。
  • チームサイズが小さく、インフラストラクチャの複雑さを最小限に抑えたい。

以下の場合、Kubernetesを使用します。

  • アップデート中でもアプリケーションを利用可能にしなければならない。
  • トラフィックに基づいてサービスを個別にスケーリングする必要がある。
  • 複数のサービスを実行しており、それらを個別にデプロイおよび更新する必要がある。
  • ノード障害からの自動復旧を望む。
  • チームにクラスターを管理する運用の成熟度がある。

中間の選択肢もあります。一部のチームは、K3sやMicroK8sなどのツールを使用して、単一ノードの小さなKubernetesクラスターを実行しています。これにより、マルチノードクラスターの完全な複雑さなしに、Kubernetesのローリングアップデートとヘルスチェック機能を利用できます。デプロイパターンは欲しいが、まだスケールは必要ない場合に検討する価値があります。

決して変わらない1つのルール

どこにデプロイするかに関係なく、1つのルールは変わりません。本番環境で実行されるイメージは、パイプラインですべてのテストとスキャンに合格したイメージと完全に同一でなければなりません。

サーバー上でイメージを再ビルドしてはいけません。「同じはず」だからといって、異なるタグをプルしてはいけません。誰もサーバーにSSHで接続し、ローカルで変更したイメージでコンテナを実行させてはいけません。レジストリ内のイメージが実行中のイメージでなければ、再現、監査、ロールバックの能力を失います。

これが、イメージのタグ付けとプロモーションが重要である理由です。イメージをステージングから本番にプロモートするとき、何かを再ビルドしているわけではありません。特定のタグをプルできる環境を変更しているだけです。バイトは同一です。

コンテナデプロイの実践的チェックリスト

コンテナを任意の環境にデプロイする前に、以下の点を確認してください。

  • デプロイメント内のイメージタグが、パイプラインに合格したタグと一致している。
  • コンテナに、オーケストレーターに準備完了を伝えるヘルスチェックエンドポイントがある。
  • 環境変数とシークレットがターゲット環境用に正しく設定されている。
  • 更新戦略が定義されている:ゼロダウンタイムの場合はローリングアップデート、単純なケースの場合は再作成。
  • 現在実行中のイメージのバージョンを確認する方法がある。
  • ロールバック計画がある:以前のイメージタグまたは以前のデプロイメントマニフェストのいずれか。

次に来ること

コンテナを実行することは、作業の半分に過ぎません。実行されたら、実際にトラフィックを処理しているバージョン、それが正常かどうか、新しいバージョンに問題がある場合の対処方法を知る必要があります。ここで、イメージバージョンの追跡とロールバックが重要になります。これらは、この議論の次のパートのトピックです。

今のところ重要なのは、運用の現実に合ったデプロイターゲットを選択し、実行するイメージがテストしたイメージであることを確認することです。他のすべてはそれに従います。