GoogleCloudの継続的デプロイへのより良いアプローチ
公開: 2022-03-11継続的デプロイ(CD)は、新しいコードを本番環境に自動的にデプロイする方法です。 ほとんどの継続的デプロイメントシステムは、ユニットテストと機能テストを実行することにより、デプロイされるコードが実行可能であることを検証し、すべてが正常に見える場合は、デプロイメントがロールアウトされます。 通常、ロールアウト自体は、コードが期待どおりに動作しない場合にロールバックできるようにするために段階的に行われます。
AWSスタック、Google Cloudスタック、Bitbucketパイプラインなど、さまざまなツールを使用して独自のCDパイプラインを実装する方法についてのブログ投稿は少なくありません。しかし、それらのほとんどは、優れたCDパイプラインについての私の考えに合わないことがわかりました。次のようになります。最初にビルドし、ビルドされた単一のファイルのみをテストしてデプロイします。
この記事では、イベント駆動型の継続的デプロイパイプラインを構築します。このパイプラインは、最初にビルドしてから、デプロイの最終アーティファクトでテストを実行します。 これにより、テスト結果の信頼性が高まるだけでなく、CDパイプラインを簡単に拡張できるようになります。 次のようになります。
- ソースリポジトリに対してコミットが行われます。
- これにより、関連付けられたイメージのビルドがトリガーされます。
- テストは、ビルドされたアーティファクトに対して実行されます。
- すべてが良好に見える場合、イメージは本番環境にデプロイされます。
この記事は、Kubernetesとコンテナテクノロジーに少なくともある程度精通していることを前提としていますが、慣れていない場合や復習を使用できる場合は、「Kubernetesとは」を参照してください。 コンテナ化と展開のガイド。
ほとんどのCDセットアップの問題
ほとんどのCDパイプラインに関する私の問題は次のとおりです。通常、ビルドファイルですべてを実行します。 私がこれについて読んだほとんどのブログ投稿には、ビルドファイルに次のシーケンスのバリエーションがあります(Google Cloud Buildの場合はcloudbuild.yaml
、Bitbucketの場合はbitbucket-pipeline.yaml
)。
- テストを実行する
- イメージを作成する
- 画像をコンテナリポジトリにプッシュ
- 新しいイメージで環境を更新します
最終的なアーティファクトに対してテストを実行していません。
この順序で実行することにより、テストを実行します。 それらが成功した場合は、イメージを作成し、パイプラインの残りの部分に進みます。 ビルドプロセスによって、テストに合格しなくなるような方法でイメージが変更された場合はどうなりますか? 私の意見では、アーティファクト(最終的なコンテナーイメージ)を作成することから始めるべきであり、このアーティファクトはビルドと本番環境にデプロイされる時間の間で変更されるべきではありません。 これにより、アーティファクトに関するデータ(テスト結果、サイズなど)が常に有効になります。
ビルド環境には「王国への鍵」があります。
ビルド環境を使用してイメージを本番スタックにデプロイすることにより、本番環境を効果的に変更できるようになります。 ソースリポジトリへの書き込みアクセス権を持つ誰もが本番環境に対してやりたいことを何でもできるようになったので、これは非常に悪いことだと思います。
最後のステップが失敗した場合は、パイプライン全体を再実行する必要があります。
最後のステップが失敗した場合(たとえば、資格情報の問題が原因で)、パイプライン全体を再実行する必要があり、他のことを行うために費やすことができる時間やその他のリソースを消費します。
これが私の最後のポイントにつながります。
あなたのステップは独立していません。
より一般的な意味では、独立したステップを持つことで、パイプラインの柔軟性を高めることができます。 パイプラインに機能テストを追加したいとします。 1つのビルドファイルにステップを含めることにより、ビルド環境で機能テスト環境を起動し、その中でテストを実行する必要があります(ほとんどの場合は順次)。 手順が独立している場合は、「イメージ構築」イベントによって単体テストと機能テストの両方を開始できます。 その後、それらは独自の環境で並行して実行されます。
私の理想的なCDセットアップ
私の意見では、この問題に取り組むためのより良い方法は、一連の独立したステップをすべてイベントメカニズムによってリンクさせることです。
これには、以前の方法と比較していくつかの利点があります。
さまざまなイベントに対して、いくつかの独立したアクションを実行できます。
上記のように、新しいイメージのビルドが成功すると、「成功したビルド」イベントが公開されます。 次に、このイベントがトリガーされたときに、いくつかのことを実行させることができます。 この場合、ユニットテストと機能テストを開始します。 また、ビルド失敗イベントがトリガーされたとき、またはテストに合格しなかった場合に開発者に警告するなどのことも考えられます。
各環境には、独自の一連の権限があります。
各ステップを独自の環境で実行することにより、単一の環境ですべての権限を取得する必要がなくなります。 これで、ビルド環境はビルドのみが可能になり、テスト環境はテストのみが可能になり、デプロイメント環境はデプロイのみが可能になります。 これにより、イメージが作成された後は変更されないことを確信できます。 生成されたアーティファクトは、最終的に本番スタックに配置されるアーティファクトです。 また、1セットのクレデンシャルを1つのステップにリンクできるため、パイプラインのどのステップが何を実行しているかを簡単に監査できます。
より柔軟性があります。
ビルドが成功するたびに誰かにメールを送信したいですか? そのイベントに反応してメールを送信するものを追加するだけです。 簡単です。ビルドコードを変更したり、ソースリポジトリに誰かのメールをハードコードしたりする必要はありません。
再試行は簡単です。
独立したステップがあるということは、1つのステップが失敗した場合にパイプライン全体を再起動する必要がないことも意味します。 失敗状態が一時的であるか、手動で修正されている場合は、失敗したステップを再試行できます。 これにより、より効率的なパイプラインが可能になります。 ビルドステップに数分かかる場合は、デプロイ環境にクラスターへの書き込みアクセス権を付与するのを忘れたという理由だけで、イメージを再構築する必要がないのは良いことです。
GoogleCloudの継続的デプロイの実装
Google Cloud Platformには、このようなシステムを短時間で、ごくわずかなコードで構築するために必要なすべてのツールがあります。
私たちのテストアプリケーションは、静的なテキストの一部を提供するだけの単純なFlaskアプリケーションです。 このアプリケーションは、より広範なインターネットにサービスを提供するKubernetesクラスターにデプロイされます。
以前に紹介したパイプラインの簡略化されたバージョンを実装します。 基本的にテスト手順を削除したので、次のようになります。
- ソースリポジトリに対して新しいコミットが行われます
- これにより、イメージのビルドがトリガーされます。 成功すると、コンテナリポジトリにプッシュされ、イベントがPub/Subトピックに公開されます。
- 小さなスクリプトがそのサブジェクトにサブスクライブされ、画像のパラメーターをチェックします。要求されたものと一致する場合は、Kubernetesクラスターにデプロイされます。
これがパイプラインのグラフィック表現です。
フローは次のとおりです。
- 誰かが私たちのリポジトリにコミットします。
- これにより、ソースリポジトリに基づいてDockerイメージをビルドするクラウドビルドがトリガーされます。
- クラウドビルドはイメージをコンテナリポジトリにプッシュし、メッセージをクラウドpub/subに公開します。
- これにより、公開されたメッセージのパラメーター(ビルドのステータス、ビルドされたイメージの名前など)をチェックするクラウド機能がトリガーされます。
- パラメータが適切な場合、クラウド関数はKubernetesデプロイを新しいイメージで更新します。
- Kubernetesは、新しいイメージを使用して新しいコンテナをデプロイします。
ソースコード
私たちのソースコードは、静的なテキストを提供するだけの非常にシンプルなFlaskアプリです。 プロジェクトの構造は次のとおりです。
├── docker │ ├── Dockerfile │ └── uwsgi.ini ├── k8s │ ├── deployment.yaml │ └── service.yaml ├── LICENSE ├── Pipfile ├── Pipfile.lock └── src └── main.py
Dockerディレクトリには、Dockerイメージの構築に必要なすべてのものが含まれています。 このイメージはuWSGIとNginxイメージに基づいており、依存関係をインストールしてアプリを正しいパスにコピーするだけです。
k8sディレクトリにはKubernetes設定が含まれています。 これは、1つのサービスと1つのデプロイメントで構成されます。 デプロイメントは、 Dockerfileから構築されたイメージに基づいて1つのコンテナーを開始します。 次に、サービスはパブリックIPアドレスを持つロードバランサーを開始し、アプリコンテナにリダイレクトします。

クラウドビルド
クラウドビルドの構成自体は、クラウドコンソールまたはGoogleCloudコマンドラインから実行できます。 クラウドコンソールを使用することにしました。
ここでは、任意のブランチで任意のコミット用のイメージを作成しますが、たとえば、開発用と本番用で異なるイメージを使用できます。
ビルドが成功すると、クラウドビルドはイメージを独自にコンテナレジストリに公開します。 次に、cloud-builds pub/subトピックにメッセージを公開します。
クラウドビルドは、ビルドの進行中と失敗したときにもメッセージを公開するため、これらのメッセージに反応させることもできます。
クラウドビルドのpub/sub通知のドキュメントはここにあり、メッセージの形式はここにあります。
クラウドパブ/サブ
クラウドコンソールのクラウドパブ/サブタブを見ると、クラウドビルドがクラウドビルドと呼ばれるトピックを作成していることがわかります。 これは、クラウドビルドがステータスの更新を公開する場所です。
クラウド機能
ここで行うことは、cloud-buildsトピックに公開されたメッセージでトリガーされるクラウド関数を作成することです。 ここでも、クラウドコンソールまたはGoogleCloudコマンドラインユーティリティのいずれかを使用できます。 私の場合は、変更があるたびにクラウドビルドを使用してクラウド機能をデプロイしました。
クラウド機能のソースコードはこちらです。
まず、このクラウド機能をデプロイするコードを見てみましょう。
steps: - name: 'gcr.io/cloud-builders/gcloud' id: 'test' args: ['functions', 'deploy', 'new-image-trigger', '--runtime=python37', '--trigger-topic=cloud-builds', '--entry-point=onNewImage', '--region=us-east1', '--source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME']
ここでは、GoogleCloudDockerイメージを使用します。 これにより、GCcloudコマンドを簡単に実行できます。 実行しているのは、端末から直接次のコマンドを実行するのと同じです。
gcloud functions deploy new-image-trigger --runtime=python37 --trigger-topic=cloud-builds --entry-point=onNewImage --region=us-east1 --source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME
Python 3.7ランタイムを使用し、cloud-buildsトピックの新しいメッセージによってトリガーされる新しいクラウド関数をデプロイするようにGoogle Cloudに依頼しています(または、そのリージョンにその名前の関数が既に存在する場合は置き換えます)。 また、その関数のソースコードの場所をGoogleに通知します(ここで、PROJECT_IDとREPO_NAMEは、ビルドプロセスによって設定される環境変数です)。 また、エントリポイントとして呼び出す関数も指定します。
ちなみに、これを機能させるには、cloudbuildサービスアカウントに「クラウド機能開発者」と「サービスアカウントユーザー」の両方を指定して、クラウド機能をデプロイできるようにする必要があります。
クラウド関数コードのコメント付きスニペットを次に示します
エントリポイントデータには、pub/subトピックで受信したメッセージが含まれます。
def onNewImage(data, context):
最初のステップは、その特定のデプロイメントの変数を環境から取得することです(クラウドコンソールでクラウド機能を変更することで変数を定義しました。
project = os.environ.get('PROJECT') zone = os.environ.get('ZONE') cluster = os.environ.get('CLUSTER') deployment = os.environ.get('DEPLOYMENT') deploy_image = os.environ.get('IMAGE') target_container = os.environ.get('CONTAINER')
メッセージの構造が期待どおりであることを確認する部分はスキップし、ビルドが成功して1つのイメージアーティファクトが生成されたことを検証します。
次のステップは、ビルドされたイメージがデプロイしたいイメージであることを確認することです。
image = decoded_data['results']['images'][0]['name'] image_basename = image.split('/')[-1].split(':')[0] if image_basename != deploy_image: logging.error(f'{image_basename} is different from {deploy_image}') return
ここで、Kubernetesクライアントを取得し、変更するデプロイを取得します
v1 = get_kube_client(project, zone, cluster) dep = v1.read_namespaced_deployment(deployment, 'default') if dep is None: logging.error(f'There was no deployment named {deployment}') return
最後に、デプロイメントに新しいイメージをパッチします。 Kubernetesがロールアウトを担当します。
for i, container in enumerate(dep.spec.template.spec.containers): if container.name == target_container: dep.spec.template.spec.containers[i].image = image logging.info(f'Updating to {image}') v1.patch_namespaced_deployment(deployment, 'default', dep)
結論
これは、CDパイプラインで設計することが好きな方法の非常に基本的な例です。 どのpub/subイベントが何をトリガーするかを変更するだけで、より多くのステップを実行できます。
たとえば、イメージ内でテストを実行し、成功時にイベントを公開し、失敗時に別のイベントを公開するコンテナーを実行し、結果に応じてデプロイメントを更新するかアラートを送信することでそれらに対応できます。
私たちが構築したパイプラインは非常に単純ですが、他の部分のために他のクラウド関数を書くことができます(たとえば、単体テストを破ったコードをコミットした開発者に電子メールを送信するクラウド関数)。
ご覧のとおり、ビルド環境はKubernetesクラスター内で何も変更できず、デプロイコード(クラウド関数)はビルドされたイメージを変更できません。 私たちの特権の分離は良さそうです、そして私たちは不正な開発者が私たちの本番クラスターをダウンさせないことを知ってしっかりと眠ることができます。 また、より運用指向の開発者にクラウド関数コードへのアクセスを許可して、修正または改善できるようにすることもできます。
ご質問、ご意見、または改善点がございましたら、以下のコメントでお気軽にお問い合わせください。