Cloud FunctionsのRubyがベータ公開されたので使ってみた

## これはなに？
先日、[Google Cloud FunctionsにRubyのサポート](https://cloud.google.com/blog/ja/products/application-development/ruby-comes-to-cloud-functions)が入ったので開発者体験を見ながら検証してみました。なお、私は普段AWS Lambdaを使っておりCloud Functions自体に慣れていないので何かお気づきがあればお知らせください。

### Functionの実装
[Function Framework](https://github.com/GoogleCloudPlatform/functions-framework-ruby)というgemを使って実装をします。HTTPのハンドラーの他に各種イベントハンドラーやテストを便利にできるモジュールが揃っています。AWSでもこういうツールがほしかったですね。SAMとかServerless Frameworkもありますが、Rubyから利用できてデプロイツールから独立しているところが好印象です。AWSさん、お願いできませんかね〜
さっそくgemも入れて最小限の実装をしてみましょう。
`app.rb`と`Gemfile`が[必要だそう](https://googlecloudplatform.github.io/functions-framework-ruby/v0.7.1/file.writing-functions.html#structuring-a-project)です。あと、Lambdaで必要な`vendor/bundle`は入れてもいいですが、デプロイ時にインストールされるので今回はなしにします。

```app.rb
# frozen_string_literal: true

require 'functions_framework'

# 一度初期化して何回も呼び出しをしたくないものはon_startupのブロック内に記述する
# https://googlecloudplatform.github.io/functions-framework-ruby/v0.7.1/file.writing-functions.html#startup-tasks
FunctionsFramework.on_startup do
  require 'json'
  require 'rack'
end

# ここのhandlerはCloud Functionのentrypoint名になる
FunctionsFramework.http 'handler' do |request|
  # requestはRack::Requestのインスタンス
  body_hash = JSON.parse(request.body.read, symbolize_names: true)
  raise StandardError, 'hoge not supported' if body_hash.key? :hoge

  { body: body_hash }
# 何もしなくても500エラーが返るが、bodyをカスタマイズする場合はRack::Responseのインスタンスを返す
rescue StandardError => e
  Rack::Response.new(
    JSON.generate({ error: e }), 500, { 'Content-Type' => 'application/json' }
  )
end
```

```Gemfile
source "https://rubygems.org"

gem "functions_framework", "~> 0.7"
gem "rack"

# 現仕様ではdevelopment, testグループのgemはインストールされないそう
# https://sue445.hatenablog.com/entry/2021/01/24/000623
group :development, :test do
  gem "rspec"
end
```

## テスト
テスト、[書きますよね？](https://raw.githubusercontent.com/akameco/twada-stand/master/twada.png)
今回はRSpecで書きましたがminitestも対応されています。Function Frameworkさまざまですね。Rubyコードではなく実際のリクエストとレスポンスが使えるのも嬉しいところです。
RSpecのボイラープレートは長いので割愛します。

```spec/app_spec.rb
require 'rspec'
require 'functions_framework/testing'

describe 'CloudFunction handler' do
  include FunctionsFramework::Testing

  subject(:response) do
    load_temporary 'app.rb' do # => spec/../app.rbというパスに解釈される
      request = make_post_request('https://example.com/foo', request_body, ['Content-Type: application/json'])
      call_http('handler', request)
    end
  end

  context '正常系' do
    let(:request_body) { { piyo: true }.to_json }
    let(:expected_response_body) { { 'body' => { 'piyo' => true } } }
    let(:response_body) { JSON.parse(response.body[0]) }
    let(:response_status) { response.status }

    it '成功する' do
      expect(response_body).to eq expected_response_body
      expect(response_status).to eq 200
    end
  end

  context '異常系' do
    let(:request_body) { { hoge: true }.to_json }
    let(:expected_response_body) { { 'error' => 'hoge not supported' } }
    let(:response_body) { JSON.parse(response.body[0]) }
    let(:response_status) { response.status }

    it '失敗する' do
      expect(response_body).to eq expected_response_body
      expect(response_status).to eq 500
    end
  end
end

```

## ローカルで実行する
テストが通ったところで必要ないかもしれませんが、SAMたちと違ってDockerでなくてもできるということで試してみましょう。

```bash
$ bundle exec functions-framework-ruby --target=handler --port 8888
I, [2021-01-29T00:40:57.630286 #41870]  INFO -- : FunctionsFramework v0.7.0
I, [2021-01-29T00:40:57.630348 #41870]  INFO -- : FunctionsFramework: Loading functions from "foo.rb"...
I, [2021-01-29T00:40:57.631502 #41870]  INFO -- : FunctionsFramework: Looking for function name "handler"...
I, [2021-01-29T00:40:57.631530 #41870]  INFO -- : FunctionsFramework: Starting server...
I, [2021-01-29T00:40:57.763187 #41870]  INFO -- : FunctionsFramework: Serving function "handler" on port 8888...

# 別ターミナルにて
$ curl localhost:8888 -d '{ "foo": "bar" }'
{"body":{"foo":"bar"}}%
I, [2021-01-29T00:41:19.113264 #41870]  INFO -- : FunctionsFramework: Handling HTTP POST
I, [2021-01-29T00:41:32.663198 #41870]  INFO -- : FunctionsFramework: Caught SIGINT; shutting down server...
```

## デプロイする
一旦[コンソール](https://console.cloud.google.com/functions)からやります。ちゃんとやるときはGCSにzipを上げる運用でもよさそうです。
アップロード手順は割愛しますが、注意するところは以下のとおりです。

- entrypointが入っているファイルの名前が`app.rb`であること
- entrypoint (本記事では`handler`)を指定すること
- private gemをインストールする場合は、当然ながら`BUNDLE_GITHUB__COM`を環境変数に設定するなど認証を通すこと。[ドキュメント](https://cloud.google.com/functions/docs/writing/specifying-dependencies-ruby)ではローカルに配置するやりかたも提示されています

Lambdaと同様にzipに入れてアップロードしましょう。デプロイが終わるまで2分くらいかかって体感的に遅いです。今のところ、デプロイのスピードに関してはLambdaが完全に上ですね。

```bash
zip -x '*.git*' -r function.zip app.rb Gemfile Gemfile.lock
```

## デプロイされたFunctionを試す
アップロードしたらさっそくリクエストを投げてみましょう。[Triggerタブ](https://console.cloud.google.com/functions/details/asia-northeast1/your_function?tab=trigger)に呼び出し用リンクが記載されています。Lambdaと同様に、コンソールからテスト実行がしたり、cliから直接実行したりすることもできます。標準的ですね。

## さいごに
これでひと通り試すことができました。Function Framework、褒めても褒めたりないですね！
さっそく本番運用を始めたいところですが、まだBetaでした。
Ruby使いとしてプラットフォームが1つ増えて嬉しい限りです。
みなさまも快適なRubyライフを！