📝

Cloud Pub/Sub、Cloud Functions エミュレーターを Docker Compose で立ち上げる

2023/03/01に公開

概要

Cloud Functions を PubSub 経由で起動するものとして作るときに、ローカルで気軽に試せると開発がスムーズに進むと思います。
今回はその解決法の1つとして Cloud Pub/Sub、Cloud Functions の各エミュレーターを Docker Compose で立ち上げて動かしてみようと思います。
Cloud Pub/Sub のサブスクリプションに関しては push 型を想定しています。
Cloud Functions に関しては今回は Python で書いていますが、他の言語でも問題ないと思います。

検証環境

macOS Ventura 13.2.1
❯ docker --version
Docker version 20.10.11, build dea9396

サンプルソース

Github にサンプルソースを上げたので、試したい方はお使いください。
https://github.com/es-nd/pubsub_and_functions_emulator

ディレクトリ構成

❯ tree
.
├── LICENSE
├── Makefile
├── README.md
├── docker
│   ├── function
│   │   └── Dockerfile
│   └── pubsub
│       └── Dockerfile
├── docker-compose.yaml
├── function
│   └── main.py
├── pubsub
│   ├── my_publisher.py
│   └── test.json
└── scripts
    └── entrypoint.sh

6 directories, 10 files

/function 以下には関数の内容を格納しています。
/pubsub 以下にはメッセージをパブリッシュするためのロジックとメッセージ内容がそれぞれファイルでおいてあります。
/scripts/entrypoints.sh は pubsub コンテナ立ち上げ時に実行するスクリプトです。コンテナ内で pubsub エミュレーターを立ち上げ、トピックと push 型のサブスクリプションを作成しています。

Docker Compose

docker-compose.yaml
version: '3.8'
services:
  function:
    build:
      context: .
      dockerfile: docker/function/Dockerfile
    volumes:
      - ./function:/workspace
    environment:
      - PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
    ports:
      - 8080:8080
    tty: true
    command: functions-framework --target=main --signature-type=event --debug
  pubsub:
    build:
      context: .
      dockerfile: docker/pubsub/Dockerfile
    volumes:
      - ./scripts:/python-pubsub/samples/snippets/scripts
      - ./pubsub:/python-pubsub/samples/snippets/pubsub
    ports:
      - 8085:8085
    env_file: .env
    tty: true
    command: ./scripts/entrypoint.sh

function に関しては、Dockerfile でエミュレーターとして動かすためのライブラリである functions-framework を使用してサーバーを立ち上げています。
https://cloud.google.com/functions/docs/functions-framework?hl=ja
pubsub に関してはマウントしたスクリプトファイルを実行し、そのなかでエミュレーターを動かしています。

Cloud Functions

docker/function/Dockerfile
FROM python:3.7
WORKDIR /workspace

RUN pip3 install functions-framework

Dockerfile 内では functions-framework をインストールしています。

function/main.py
import base64


def main(event, _):
    pubsub_message = base64.b64decode(event['data']).decode('utf-8')
    print(f'pubsub_message: {pubsub_message}')

    return "OK"

関数内では検証のためにイベントからメッセージを抽出して標準出力に流すことのみ行います。

Cloud Pub/Sub

docker/pubsub/Dockerfile
FROM gcr.io/google.com/cloudsdktool/cloud-sdk:367.0.0-emulators

RUN apt-get update && \
  apt-get install -y git python3-pip netcat && \
  git clone https://github.com/googleapis/python-pubsub.git

WORKDIR /python-pubsub/samples/snippets
RUN pip3 install -r requirements.txt

Google が提供しているエミュレーターのイメージを使います。
トピックとサブスクリプションを作成するコマンドを使うためのソースをクローンします。

scripts/entrypoint.sh
#!/bin/bash
# pubsub コンテナ立ち上げ時に使用
set -em

gcloud beta emulators pubsub start --project=$PUBSUB_PROJECT_ID --host-port=$PUBSUB_EMULATOR_HOST --quiet &

while ! nc -z localhost 8085; do
  sleep 0.1
done

python3 publisher.py $PUBSUB_PROJECT_ID create $TOPIC_ID
python3 subscriber.py $PUBSUB_PROJECT_ID create-push $TOPIC_ID $SUBSCRIPTION_ID $PUSH_ENDPOINT

fg %1

コンテナ立ち上げ時に実行するスクリプトです。
エミュレーターをバックグラウンドで立ち上げ、トピックとサブスクリプションを作成し、最後に立ち上げたジョブをフォアグラウンドに戻しています。

pubsub/my_publisher.py
# local 開発で使用。json データを publish するために必要。
import sys
from google.cloud import pubsub_v1


def publish_message(project_id: str, topic_id: str, file_path: str):
    # パブリッシャークライアントを作成
    publisher = pubsub_v1.PublisherClient()
    topic_path = publisher.topic_path(project_id, topic_id)

    # JSON ファイルを読み込み
    with open(file_path) as f:
        data_str = f.read()

    # メッセージをパブリッシュする
    data = data_str.encode("utf-8")
    future = publisher.publish(topic_path, data)
    future.result()

    return 0


if __name__ == "__main__":
    args = sys.argv
    project_id = args[1]
    topic_id = args[2]
    file_path = args[3]

    publish_message(project_id, topic_id, file_path)
pubsub/sample.json
{
  "text": "sample"
}

publish_message 関数を実行することで sample.json の内容をパブリッシュできます。

Makefile

Makefile
# .env を読み込む
include .env

.PHONY:build
build:
  docker-compose build

.PHONY:up
up:
  docker-compose up

.PHONY:down
down:
  docker-compose down

.PHONY:reup
reup:
  make down up

.PHONY:publish
publish:
  docker-compose exec pubsub python3 pubsub/my_publisher.py $(PUBSUB_PROJECT_ID) $(TOPIC_ID) ./pubsub/sample.json

make publish でパブリッシュできます。pubsub コンテナ内で pubsub/my_publisher.py を実行しています。

実行

zsh0
❯ make up
function_1  |  * Running on all addresses (0.0.0.0)
function_1  |  * Running on http://127.0.0.1:8080
function_1  |  * Running on http://172.22.0.2:8080
pubsub_1    | Endpoint for subscription is: http://function:8080
zsh1
❯ make publish
zsh0
pubsub_1    | [pubsub] Feb 28, 2023 6:09:46 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
pubsub_1    | [pubsub] INFO: Detected HTTP/2 connection.
function_1  | pubsub_message: {
function_1  |   "text": "sample"
function_1  | }
function_1  | 172.22.0.3 - - [28/Feb/2023 06:09:46] "POST / HTTP/1.1" 200 -

function がメッセージのログを出力しました。

雑感

pubsub の Dockerfile でクローンしたソースのなかにもパブリッシュを実行できる関数があるのですが、メッセージ内容が変えられなかったため(もし変えられた方いらっしゃればコメント頂ければ嬉しいです)、今回自前のパブリッシュ関数を用意しました。
用意するのは多少手間がかかりますが、一度作ってしまえば手軽にテストできるのでやはり開発しやすいです。
pubsub のベースイメージがでかいので、そこが今回における一番の課題かなと思います。

Discussion