📝

Platform Engineering:CICD パイプラインの構築

2024/06/25に公開

はじめに

SRE 部の岸本です。
前回に引き続き、Platform Engineering についてです。
テーマは「GKE で始める Platform Engineering~実践編~」です。

Platform Engineering とは

Platform Engineering とは、組織において有用な抽象化を行い、セルフサービス インフラストラクチャを構築するアプローチです。
ポイントとしては、以下の 2 つが挙げられます。

  • インフラの有用な抽象化
  • デベロッパーの生産性の向上

詳細については、前回の記事をご参照ください。

今回のテーマ

前回は Platform Engineering の概要について紹介し、実際に Cloud Workstations を使用してゴールデンパスの提供を行いました。
今回も、Platform Engineering の実践として、CI/CD パイプラインを構築しゴールデンパスの提供を行いたいと思います。

効率的なデプロイ環境の構築

効率的なデプロイ環境とは

変化が激しい現代のビジネス環境において、アプリケーションの開発と運用は、迅速かつ効率的に行うことが求められています。
例えば、生成 AI の登場により、アプリケーション開発においても、生成 AI をすぐに試し、その有用性を検証した上で、迅速にサービスとしてデプロイできるかが重要です。
ただ迅速にデプロイするだけでなく、効率的にデプロイできる環境も必要です。では効率的にデプロイするためには何を指標にすればよいのでしょうか?

DORA (DevOps Research and Assessment)が行った研究から、ソフトウェア開発チームのパフォーマンスを示す 4 つの指標が確立されました。

デプロイの頻度 - 組織による正常な本番環境へのリリースの頻度
変更のリードタイム - commit から本番環境稼働までの所要時間
変更障害率 - デプロイが原因で本番環境で障害が発生する割合(%)
サービス復元時間 - 組織が本番環境での障害から回復するのにかかる時間

上記の指標と一緒に実際の開発者調査を行った結果が以下の記事の「指標の計算」に記載されています。

調査結果からも分かる通り、高頻度でデプロイした方が変更障害率は下がります。
つまり、開発速度とサービスの安定性は両立できることが示されています。

上記のように高頻度でデプロイする環境を提供することがサービスの品質向上、開発者の効率アップ、運用の安定化、そして会社全体の利益につながることがわかります。
これらの Platform Engineering が実現できれば、皆が喜ぶシステムができるのではないでしょうか。
開発者が迅速にアプリケーションをテストできる環境を作り、リリースの管理者が効率的に運用を行えるプラットフォームを提供することで、変化の激しいビジネス環境においても、迅速かつ効率的にアプリケーションを提供できるようになると私は考えています。

効率的なデプロイを支えるサービス

では実際構築していく前に、今回使用する Google Cloud のサービス概要を説明します。

  • Cloud Build
    CI/CD デリバリーパイプラインを構築するためのサービスです。ビルド、テスト、デプロイを自動化することができます。トリガーを作成して、GitHub などのリポジトリに Push されたときにビルドを実行することができます。今回は CI の構築に使用します。

  • Artifact Registry
    ビルドしたコンテナイメージを保存するためのレジストリです。今回は CI でビルドしたコンテナイメージを保存するために使用します。

  • Cloud Deploy
    異なる実行環境に対して一連のデリバリーパイプラインを自動化するサービスです。今回は CD の構築に使用します。コンポーネントが多いため、詳細は以下からご確認ください。

Cloud Deploy の詳細

コンポーネントの説明は以下の通りです。

  • デリバリーパイプライン:デプロイの流れ(順番)を定義したものを指す
  • リリース:ビルドしたイメージ(成果物)を一緒にデプロイしたい単位をリリースとしてまとめる
  • ターゲット:デプロイする環境(開発環境、本番環境など)
  • プロモーション:リリースを次のターゲットに展開

詳しくはこちらを参照してください。

以上が主に使用するサービスの概要です。

構成図について

開発者とリリースの管理者の役割を分けて、CI/CD パイプラインを構築します。
全体的な構成は以下のようになります。
0

今回は CI と CD の分離をして構築します。
理由としては、効率的なデプロイを行うためです。
CI は開発者が行い、CD はリリースの管理者が行うことで、開発者はコードの変更に集中できるようになり、リリースの管理者はデプロイの履歴だけを見るなどデプロイを効率的に行うことができます。
また開発者に CI に必要な権限、リリースの管理者に CD に必要な権限を付与することで、アクセス制御が正確に行われるためセキュリティ面でも安心です。

Platform Engineering の実践

それでは、実際に CI/CD パイプラインを構築していきます。
手順は以下の通りです。

    1. 事前準備
    1. Cloud Build による CI
    1. Cloud Deploy によるサンプルアプリの CD

1. 事前準備

事前準備としてチュートリアルの開始、環境変数設定、API 有効化、リソース作成などを行います。

  • チュートリアルの開始
    Google Cloud のコンソール画面でチュートリアルを開くコマンドを実行します。
// ご自身の作業用ディレクトリに移動して、ハンズオン用のリポジトリをクローン
git clone https://github.com/GoogleCloudPlatform/gcp-getting-started-lab-jp.git

// ディレクトリ移動
cd gcp-getting-started-lab-jp/pfe-cicd/

// チュートリアルの起動
teachme tutorial.md
  • 環境変数設定
    環境変数を設定します。
    PROJECT_ID はプロジェクト ID に置き換えてください。
// プロジェクトIDの設定
export PROJECT_ID=[PROJECT_ID]

// gcloud のデフォルト設定
gcloud config set project \
    ${PROJECT_ID} && gcloud config \
    set compute/region \
    asia-northeast1 && gcloud \
    config set compute/zone \
    asia-northeast1-c
  • API 有効化
    今回使用する API を有効化します。
gcloud services enable \
    cloudbuild.googleapis.com \
    container.googleapis.com \
    artifactregistry.googleapis.com \
    clouddeploy.googleapis.com

  • リソース作成
    メインとなる Cloud Build、Artifact Registry、Cloud Deploy 以外のリソースを事前に作成します。
    具体的には、VPC ネットワーク、サブネット、Cloud Router、Cloud NAT、GKE クラスタを作成します。
// VPCネットワークの作成
gcloud compute networks create \
    ws-network --subnet-mode custom

// サブネットの作成
gcloud compute networks subnets \
    create ws-subnet --network \
    ws-network --region \
    asia-northeast1 --range \
    "192.168.1.0/24"

// Cloud Routerの作成
gcloud compute routers create \
    ws-router --network ws-network \
    --region asia-northeast1

// Cloud NATの作成
gcloud compute routers nats create \
    ws-nat --router ws-router \
    --auto-allocate-nat-external-ips \
    --nat-all-subnet-ip-ranges \
    --region asia-northeast1

// GKEクラスタの作成(AutoPilot-Dev)
gcloud container --project \
    "$PROJECT_ID" clusters \
    create-auto "dev-cluster" \
    --region "asia-northeast1" \
    --release-channel "regular" \
    --network "ws-network" \
    --subnetwork "ws-subnet" \
    --enable-private-nodes \
    --no-enable-master-authorized-networks \
    --async

// GKEクラスタの作成(AutoPilot-Prod)
gcloud container --project \
    "$PROJECT_ID" clusters \
    create-auto "prod-cluster" \
    --region "asia-northeast1" \
    --release-channel "regular" \
    --network "ws-network" \
    --subnetwork "ws-subnet" \
    --enable-private-nodes \
    --no-enable-master-authorized-networks \
    --async

2. Cloud Build による CI

次に CI の構築を行います。
具体的には、以下の 4 点を実施します。

  • Artifact Registry にリポジトリを作成
  • Cloud Build と GitHub の連携とトリガーの設定
  • ファイルの追加と GitHub への Push
  • コンテナイメージの動作確認

構成図では以下のようになります。
1
開発者は、GitHub に Push するだけで CI が実行され、コンテナイメージが Artifact Registry に保存されるようになります。
では、構築を行っていきます。

Artifact Registry にリポジトリを作成

Artifact Registry にリポジトリを作成します。
CI でビルドしたコンテナイメージを保存するためのリポジトリです。

gcloud artifacts repositories \
    create app-repo \
    --repository-format docker \
    --location asia-northeast1 \
    --description="Docker \
    repository for python app"

Cloud Build と GitHub の連携とトリガーの作成

Cloud Build と GitHub を連携します。
こちらの手順は、以下の記事を参考にしてください。

トリガーの作成は以下のような設定値で行います。

項目
名前 任意
リージョン asia-northeast1 (Tokyo)
イベント ブランチに push する
ソース 第 2 世代
リポジトリ リポジトリを選択
ブランチ ^develop$
Cloud Build configuration file location cloudbuild/cloudbuild.yaml
  • イベントとは
    Cloud Build では、特定のイベントに基づいてビルドをトリガーできます。
    イベントの選択肢として、あるブランチに push、新しいタグを push、pull Request を作成の 3 つがあります。
    今回は develop ブランチに push したときにビルドが実行されるように設定しています。

  • cloudbuild.yaml について
    Cloud Build でビルドする手順を記述するファイルです。
    これを Cloud Build configuration file location に指定することで、cloudbuild.yaml に記述されたビルド手順が実行されます。

ファイルの追加と GitHub への Push

次にアプリケーション、ビルドに必要なファイルを追加し、GitHub に Push します。
まず GitHub のリポジトリをローカルにクローンし、トリガーで指定した develop ブランチを作成します。

// リポジトリのクローン(URLはご自身のリポジトリURLに置き換えてください)
git clone URL

// ディレクトリ移動
cd クローンしたリポジトリ名

// ブランチの作成
git checkout -b develop

続いてアプリケーション作成のために以下のファイルを用意します。

  • app.py
    • サンプルアプリケーションのソースコード
    • ランダムに犬の品種を返す API です
  • test_app.py
    • テストコード
    • ユニットテストを行う
  • requirements.txt
    • パッケージのインストール
  • Dockerfile
    • コンテナイメージのビルド
  • cloudbuild/cloudbuild.yaml
    • Cloud Build の設定ファイル
app.py
from flask import Flask, jsonify
import random

app = Flask(__name__)

breeds = [
    "Labrador Retriever",
    "German Shepherd",
    "Golden Retriever",
    "French Bulldog",
    "Bulldog",
    "Poodle",
    "Beagle",
    "Rottweiler",
    "German Shorthaired Pointer",
    "Yorkshire Terrier"
]


@app.route('/random-pets', methods=['GET'])
def get_random_dog():
    random_breed = random.choice(breeds)
    return jsonify({'breed': random_breed})


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

test_app.py
import unittest
from app import app


class FlaskTestCase(unittest.TestCase):

    def setUp(self):
        self.app = app.test_client()
        self.app.testing = True

    def test_random_dog(self):
        response = self.app.get('/random-pets')
        self.assertEqual(response.status_code, 200)
        self.assertIn('breed', response.json)


if __name__ == '__main__':
    unittest.main()
requirements.txt
Flask
flake8

Dockerfile
FROM python:3.12-alpine

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt

COPY app.py app.py

EXPOSE 5000
CMD ["python", "app.py"]

cloudbuild/cloudbuild.yaml

steps:
- name: 'python:3.12-alpine'
  entrypoint: 'sh'
  args:
    - '-c'
    - |
      pip install -r requirements.txt
      pip install flake8
      flake8 .

- name: 'python:3.12-alpine'
  entrypoint: 'sh'
  args:
    - '-c'
    - |
      pip install -r requirements.txt
      python -m unittest discover

- name: 'gcr.io/cloud-builders/docker'
  args: ['build', '-t', 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/app-repo/pets:v1', '.']

- name: 'gcr.io/cloud-builders/docker'
  args: ['push', 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/app-repo/pets:v1']

images:
- 'asia-northeast1-docker.pkg.dev/$PROJECT_ID/app-repo/pets:v1'


以上でサンプルアプリケーションが動くファイルが揃いました。
では、トリガーをセットしたリモートブランチに Push して CI を実行します。

// ファイルの追加
git add .

// コミット
git commit -m "add files"

// プッシュ
git push origin develop

コンテナイメージの動作確認

最後に、コンテナイメージが正常に動作するか確認します。
Cloud Shell で以下のコマンドを実行してください。

// コンテナイメージの取得
docker pull \
    asia-northeast1-docker.pkg.dev/$PROJECT_ID/app-repo/pets:v1

// コンテナの起動
docker run -d -p 5000:5000 \
    asia-northeast1-docker.pkg.dev/$PROJECT_ID/app-repo/pets:v1

// APIの確認
curl http://localhost:5000/random-pets

// 出力例
{"breed":"German Shepherd"}


以上で CI の構築が完了しました。
開発者は、GitHub に Push するだけで CI が実行され、コンテナイメージが Artifact Registry に保存されるようになりました。
またソースコードは GitHub に保存されているため、変更履歴の管理もしやすくなりました。
この仕組みを構築することで、開発者はコードの変更に集中できるようになります。
では、次にリリースの管理者側が行う CD 構築を行っていきます。

3. Cloud Deploy によるサンプルアプリの CD

Cloud Deploy を使用して、先ほど作成した GKE クラスタにサンプルアプリをデプロイしていきます。
dev-cluster に対しては、これから作成するトリガーと共にデプロイがされます。
一方、prod-cluster に対しては、Web UI 上でプロモートという操作をするまではデプロイが行われないようにします。

構成図では以下のようになります。
[ dev-cluster デプロイの場合]
2

[ prod-cluster デプロイの場合]
3

デリバリーパイプラインの設定

まずは、デリバリーパイプラインの設定を行います。
ローカルにて以下のファイルを作成します。

  • clouddeploy.yaml
    • デリバリーパイプラインの設定が記述されています。(コードの記述方法はこちら)
    • デリバリーパイプラインとは、デプロイ進行中に各ターゲットへアプリケーションを配信する流れを示します。
  • skaffold.yaml
    • デプロイに利用するマニフェスト(kubernetes-manifests)、およびデプロイに対応する成果物が定義されています。(詳細はこちら)
  • kubernetes-manifests/deployment.yaml
    • Kubernetes クラスタ上にデプロイされるアプリケーションの設定を定義するためのマニフェストファイルです。
  • kubernetes-manifests/service.yaml
    • Kubernetes Service を定義しており、pets アプリケーションへのネットワークアクセスを提供する設定です。
  • clouddeploy/cloudbuild.yaml
    • Cloud Deploy にデリバリーパイプラインを登録し、新しいリリースを作成して GKE にデプロイするための構成ファイルです。

全てのファイルを作成したら、以下のディレクトリ構成になっているかと思います。

ディレクトリ構成
.
├── cloudbuild
│   └── cloudbuild.yaml
├── clouddeploy
│   └── cloudbuild.yaml
├── kubernetes-manifests
│   ├── deployment.yaml
│   └── service.yaml
├── app.py
├── clouddeploy.yaml
├── Dockerfile
├── requirements.txt
├── test_app.py
└── skaffold.yaml
clouddeploy/cloudbuild.yaml
steps:
  - name: "gcr.io/google.com/cloudsdktool/cloud-sdk:slim"
    entrypoint: "sh"
    args:
      - "-c"
      - |
        gcloud deploy apply --file=clouddeploy.yaml --region=asia-northeast1 --project=$PROJECT_ID

  - name: "gcr.io/google.com/cloudsdktool/cloud-sdk:slim"
    entrypoint: "sh"
    args:
      - "-c"
      - |
        gcloud deploy releases create release-$(date +%Y%m%d%H%M%S) \
        --delivery-pipeline=pfe-cicd \
        --region=asia-northeast1 \
        --project=$PROJECT_ID \
        --images=pets=asia-northeast1-docker.pkg.dev/$PROJECT_ID/app-repo/pets:v1
skaffold.yaml
apiVersion: skaffold/v2beta28
kind: Config
metadata:
  name: pets-app
deploy:
  kubectl:
    manifests: ["kubernetes-manifests/*.yaml"]
kubernetes-manifests/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pets-deployment
  labels:
    app: pets
spec:
  replicas: 1
  selector:
    matchLabels:
      app: pets
  template:
    metadata:
      labels:
        app: pets
    spec:
      containers:
        - name: pets
          image: pets
          ports:
            - containerPort: 5000
          env:
            - name: FLASK_ENV
              value: production
kubernetes-manifests/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: pets-service
spec:
  selector:
    app: pets
  ports:
    - protocol: TCP
      port: 80
      targetPort: 5000
  type: LoadBalancer
clouddeploy.yaml
apiVersion: deploy.cloud.google.com/v1beta1
kind: DeliveryPipeline
metadata:
  name: pfe-cicd
description: pfe-cicd
serialPipeline:
  stages:
    - targetId: dev
    - targetId: prod
---
apiVersion: deploy.cloud.google.com/v1beta1
kind: Target
metadata:
  name: dev
description: Dev Environment
gke:
  cluster: projects/$PROJECT_ID/locations/asia-northeast1/clusters/dev-cluster
---
apiVersion: deploy.cloud.google.com/v1beta1
kind: Target
metadata:
  name: prod
description: Production Environment
gke:
  cluster: projects/$PROJECT_ID/locations/asia-northeast1/clusters/prod-cluster

次に Clouddeploy/cloudbuild.yaml を検知する Cloud Build トリガーを作成します。

トリガーの作成

このトリガーは、main ブランチに Push されたときおよび、main ブランチに develop ブランチがマージされたときにトリガーが発火します。

実際の運用では、開発者が develop ブランチにコードを Push し、PR を作成しコードレビューをレビュワーに依頼します。
その後、レビュワーが main ブランチにマージを行います。

実行されるトリガーの内容に関しては、上記で作成した cloudbuild.yaml に記述されています。
step1 では、デリバリーパイプラインとターゲットを Cloud Deploy に登録を行います。
step2 では、新しいリリースを作成し、GKE にデプロイを行います。

全体的な流れ
  1. develop ブランチにコードを Push
  2. Cloud Build トリガーが develop ブランチに push されたことを検知
  3. Cloud Build が cloudbuild/cloudbuild.yaml に従ってビルド&Artifact Registry に保存
  4. リリースの管理者がコードレビューを行い、main ブランチにマージ
  5. Cloud Build トリガーが main ブランチにマージされたことを検知
  6. Cloud Build が clouddeploy/cloudbuild.yaml に従って GKE にデプロイ

トリガーの作成は以下のような設定値で行います。

項目
名前 任意
リージョン asia-northeast1 (Tokyo)
イベント ブランチに push する
ソース 第 2 世代
リポジトリ リポジトリを選択
ブランチ main
Cloud Build configuration file location clouddeploy/cloudbuild.yaml

以上で設定が完了しました。

デプロイの実行

上記で作成したファイルを GitHub の develop ブランチに Push しテストを行います。
次に、main ブランチにマージし、デプロイを行います。

// ファイルの追加
git add .

// コミット
git commit -m "add files"

// プッシュ
git push origin develop

ここでは以下3点を確認します。

  • Cloud Build のコンソール画面でビルドのログ
  • ビルドが成功しているか
  • Artifact Registry にコンテナイメージが保存されているか

次に Cloud Build から Cloud Deploy の実行を行います。
PR が作成してあると仮定し、develop ブランチを main ブランチにマージします。

デプロイの確認

最後に、デプロイが正常に行われているか確認します。
Cloud Deploy のコンソール画面で、デプロイの状況を確認できます。
デプロイされたアプリケーションは以下のような画面が表示されます。
4

次に実際のアプリケーションの動作確認を行います。
GKE のコンソール画面で、デプロイされたアプリケーションのエンドポイントを確認しブラウザでアクセスします。
具体的な手順は以下の通りです。
GKE コンソール >> 「Gateway、Service、Ingress」>> サービスタブ >> pets-service

5

IP アドレスの後に「/random-pets」を追加しアプリケーションが正常に動作しているか確認します。
すると、以下のような画面が表示されるかと思います。
6

これで開発環境で動作確認ができました。
最後に開発環境から本番環境にデプロイするために、Cloud Deploy の UI 上でプロモートを行います。
デリバリーパイプラインの一覧から、pfe-cicd をクリックします。
以下の画面の赤枠の部分をクリックし、プロモートを行います。

7

これで、本番環境にデプロイが行われます。
8

そして、本番環境の外部 IP アドレス(エンドポイント)にアクセスします。
9

アプリケーションが正常に動作しているか確認します。
先ほど同様、IP アドレスの後に「/random-pets」を追加し、ページをリロードしてください。
10

以上で、CI/CD パイプラインの構築が完了しました。

まとめ

今回は、CI/CD パイプラインを構築しました。
開発者、リリースの管理者で役割を分け、CI/CD を分離することで、効率よくアプリケーションをデプロイできる環境を構築しました。
より皆が喜ぶシステムを提供するために、Platform Engineering の実践が重要であることが分かりました。
今後も、Platform Engineering の実践を通じて、より良いプラットフォームを提供できるように努めていきたいと思います。
最後までご覧いただきありがとうございました。

Discussion