[GCP] Cloud Deployment ManagerでCloud Functionsをデプロイしてみた

2020.05.08

Google Cloud Deployment ManagerでCloud Functionsのデプロイをやってみました。Cloud Functionsにコードをデプロイするため、Pythonテンプレートを利用してCloud BuildでCloud StorageへのZIPファイルのアップロードを実装しています。

事前準備

Cloud Deployment Managerでリソースをデプロイする際にgcloudコマンドを使用します。gcloud CLIはCloud SDKをインストールすることで利用できるようになります。Cloud SDKの導入方法は次のブログが参考になりました。

環境

項目 内容
OS macOS Catalina 10.15.4(19E287)
Python 3.7.0
Cloud SDK 280.0.0

ディレクトリ構成

ディレクトリ構成は以下の通りです。Cloud Functionsのソースコードと構成ファイルやPythonテンプレートを分けています。

・
├─ src
│   ├─ hello-world
│   │   └─ index.js
│   └─ welcome
│       └─ index.js
└─ templates
    ├─ cloud-functions.py
    └─ cloud-functions.yaml

Cloud Functionsのソースコード

シンプルなメッセージを返す2つの関数を実装します。

src/hello-world/index.js

exports.handler = (req, res) => {
  res.status(200).send({
    message: "Hello World!",
    time: new Date(),
  });
};

src/welcome/index.js

exports.handler = (req, res) => {
  res.status(200).send({
    message: "Welcome!",
    time: new Date(),
  });
};

Pythonテンプレート

Pythonテンプレートは各リソース定義の雛形です。Cloud FunctionsのデプロイではソースコードをZIPファイルにまとめて、Cloud Storageのバケットにアップロードする必要があります。これをPythonテンプレートで実装します。

templates/cloud-functions.py

import zipfile
import io
import base64
import hashlib


def GenerateConfig(context):
    in_memory_output_file = io.BytesIO()

    zip_file = zipfile.ZipFile(
        in_memory_output_file, mode='w', compression=zipfile.ZIP_DEFLATED)

    for imp in context.imports:
        if imp.startswith(context.properties['codeLocation']):
            zip_file.writestr(
                imp[len(context.properties['codeLocation']):], context.imports[imp])

    zip_file.close()

    content = base64.b64encode(in_memory_output_file.getvalue())

    m = hashlib.md5()
    m.update(content)

    source_archive_url = 'gs://%s/%s' % (
        context.properties['codeBucket'], m.hexdigest() + '.zip')

    cmd = "echo '%s' | base64 -d > /function/function.zip;" % (
        content.decode('ascii'))

    volumes = [{'name': 'function-code', 'path': '/function'}]

    upload_function_code = {
        'action': 'gcp-types/cloudbuild-v1:cloudbuild.projects.builds.create',
        'name': context.properties['function'] + '-upload-function-code',
        'properties': {
            'steps': [{
                'name': 'ubuntu',
                'args': ['bash', '-c', cmd],
                'volumes': volumes,
            }, {
                'name': 'gcr.io/cloud-builders/gsutil',
                'args': ['cp', '/function/function.zip', source_archive_url],
                'volumes': volumes
            }],
            'timeout': '120s'
        },
        'metadata': {
            'runtimePolicy': ['UPDATE_ON_CHANGE']
        }
    }

    cloud_function = {
        'type': 'gcp-types/cloudfunctions-v1:projects.locations.functions',
        'name': context.properties['function'] + '-cloud-function',
        'properties': {
            'parent': f"projects/{context.env['project']}/locations/{context.properties['location']}",
            'function': context.properties['function'],
            'sourceArchiveUrl': source_archive_url,
            'entryPoint': context.properties['entryPoint'],
            'httpsTrigger': {},
            'timeout': context.properties['timeout'],
            'availableMemoryMb': context.properties['memory'],
            'runtime': context.properties['runtime']
        },
        'metadata': {
            'dependsOn': [context.properties['function'] + '-upload-function-code']
        }
    }

    return {
        'resources': [upload_function_code, cloud_function]
    }

Pythonテンプレートでは、GenerateConfigまたはgenerate_configという名前のメソッドを定義します。contextオブジェクトには、環境に関するメタデータや構成ファイルのプロパティが渡されます。

再利用可能なテンプレートについて | Google Cloud

構成ファイル

構成ファイルではPythonテンプレートを読み込んでリソースを定義します。resourcestypeにPythonテンプレートを指定できます。なお、使用するファイルは全てimportsに書く必要があります。

templates/cloud-functions.yaml

imports:
  - path: ../src/hello-world/index.js
  - path: ../src/welcome/index.js
  - path: cloud-functions.py

resources:
  - type: cloud-functions.py
    name: hello-world
    properties:
      function: hello-world
      codeLocation: ../src/hello-world/
      codeBucket: [ソースコードをアップロードするバケットの名前]
      location: asia-northeast1
      timeout: 60s
      runtime: nodejs8
      memory: 256
      entryPoint: handler
  - type: cloud-functions.py
    name: welcome
    properties:
      function: welcome
      codeLocation: ../src/welcome/
      codeBucket: [ソースコードをアップロードするバケットの名前]
      location: asia-northeast1
      timeout: 60s
      runtime: nodejs8
      memory: 256
      entryPoint: handler

propertiesに定義した値はPythonテンプレートに渡されます。例えばcodeLocationはPythonテンプレートでcontext.properties['codeLocation']のように取得できます。

codeBucketは後でソースコードのアップロード先のバケットを作成した際にバケットの名前に変更します。

デプロイ

Cloud Buildを使いますので有効になっていない場合は有効化します。

GCP Cloud Build 有効化

Cloud StorageでCloud Functionsのソースコードをアップロードするバケットを作成します。

GCP Cloud Storage

デプロイ先のプロジェクトを gcloud コマンドで指定します。

gcloud config set project [PROJECT_ID]

作成した構成ファイルを使用して gcloud コマンドでデプロイします。

gcloud deployment-manager deployments create cloud-functions --config templates/cloud-functions.yaml

Cloud Deployment Managerのコンソールでデプロイの詳細を確認できます。

GCP Cloud Deployment Manager

デプロイが完了するとCloud Functionsのコンソールに表示されます。

GCP Cloud Functions

Cloud Functionsのテストを実行して「Hello World!」が出力されることを確認します。

GCP Cloud Functions テスト

後片付け

作成したリソースに対しては課金が発生するため、忘れずに削除します。

gcloud deployment-manager deployments delete cloud-functions

Cloud StorageのバケットはGCPのコンソールから手動で削除します。

まとめ

Cloud Deployment ManagerではPythonテンプレートを利用することで、構成ファイルでリソースをシンプルに定義できることがわかりました。次はCloud APIなど他のリソースとCloud Functionsを組み合わせた使い方について学習したいと考えています。

構成ファイルやPythonテンプレートはGitHubでもソースコードを公開しています。

参考資料