Ruby + Sinatra + Google App Engine スタンダード環境で Hello World

## 概要

- Google App Engine スタンダード環境で Ruby + Sinatra によるシンプルな Web アプリケーションを動かす

## 構成

- Google App Engine スタンダード環境
- Ruby 2.5
- Web アプリケーションフレームワーク: Sinatra 2.0.8
- テンプレートエンジン: Erb
- Web アプリケーションサーバ: Puma 4.3

## Google App Engine スタンダード環境の Ruby はベータ版

現時点(2020年2月9日現在)ではまだベータ版の状態。
ベータ版になったのは2019年8月。

[Ruby support comes to App Engine standard environment \| Google Cloud Blog](https://cloud.google.com/blog/products/application-development/ruby-support-comes-to-app-engine-standard-environment)

> Ruby is now Beta on App Engine standard environment, in addition to being available on the App Engine flexible environment.

[Google App Engine Ruby 2\.5 Standard Environment documentation](https://cloud.google.com/appengine/docs/standard/ruby?hl=ja)

> ベータ版
> この機能はプレリリース版の状態で、変更やサポート制限が行われる可能性があります。

## ソースコード

### ファイル一覧

```
├── Gemfile
├── Gemfile.lock
├── app.rb
├── app.yaml
├── config.ru
├── public
│   └── images
│       └── icon.png
└── views
    ├── error.erb
    └── hello.erb
```

### Gemfile

sinatra-contrib は Sinatra Reloader などを使うために必要。

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

gem 'puma'
gem 'sinatra'
gem 'sinatra-contrib'
```

### Gemfile.lock

bundle install で生成された Gemfile.lock ファイル。

```
GEM
  remote: https://rubygems.org/
  specs:
    backports (3.16.0)
    multi_json (1.14.1)
    mustermann (1.1.1)
      ruby2_keywords (~> 0.0.1)
    nio4r (2.5.2)
    puma (4.3.1)
      nio4r (~> 2.0)
    rack (2.1.2)
    rack-protection (2.0.8.1)
      rack
    ruby2_keywords (0.0.2)
    sinatra (2.0.8.1)
      mustermann (~> 1.0)
      rack (~> 2.0)
      rack-protection (= 2.0.8.1)
      tilt (~> 2.0)
    sinatra-contrib (2.0.8.1)
      backports (>= 2.8.2)
      multi_json
      mustermann (~> 1.0)
      rack-protection (= 2.0.8.1)
      sinatra (= 2.0.8.1)
      tilt (~> 2.0)
    tilt (2.0.10)

PLATFORMS
  ruby

DEPENDENCIES
  puma
  sinatra
  sinatra-contrib

BUNDLED WITH
   2.1.4
```

### app.rb

メイン処理をするファイル。

```app.rb
require 'sinatra'
require 'sinatra/reloader' if development?

helpers do
  # HTML エスケープ用にエイリアスを作成
  include Rack::Utils
  alias_method :h, :escape_html
end

# 404 Not Found 時
not_found do
  @error_message = 'Page Not Found'
  erb :error
end

# エラー発生時
error do
  @error_message = h env['sinatra.error'].message
  erb :error
end

# Hello メッセージ表示ページ
get '/hello/:message?' do

  # q パラメータがあるときはエラーを発生させる
  raise params[:q] if params[:q]

  # Ruby のバージョンをセット
  @ruby_desc = RUBY_DESCRIPTION

  # メッセージをセット
  if params['message'] != nil
    @message = params['message']
  else
    @message = 'Hello, world!'
  end

  # 環境情報を取得
  env = {
    :dev  => settings.development?,
    :prod => settings.production?,
    :test => settings.test?
  }

  # HTML を表示
  erb :hello, :locals => env
end
```

### app.yaml

Google App Engine 用の設定ファイル。
Google App Engine スタンダード環境でサポートされている Ruby のバージョンは 2.5。

```app.yaml 
runtime: ruby25

# アプリケーション起動エントリポイント
entrypoint: bundle exec rackup -s Puma -p $PORT

# 第2世代ランタイム, インスタンスクラス F1, メモリ制限 256MB, CPU制限 600MHz
instance_class: F1

# インスタンス数のオートスケール設定
automatic_scaling:
  min_idle_instances: automatic
  max_idle_instances: 1
  min_pending_latency: 3s
  max_pending_latency: 5s
  max_concurrent_requests: 5 # 許容する同時リクエスト数(指定できる最大値は80)
  target_cpu_utilization: 0.95 # インスタンス立ち上げトリガーなCPU負荷率(0.5から0.95の間で指定)
  max_instances: 1 # 最大インスタンス数

# 環境変数
env_variables:
  RACK_ENV: production

# 静的ファイル配信用設定
handlers:
- url: /images
  static_dir: public/images
  secure: always
```

### config.ru

rackup コマンド用に config.ru を用意する。

```config.ru
require './app'
run Sinatra::Application
```

### views/error.erb

エラーページ用 HTML テンプレート。

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Error page</title>
</head>
<body>
<p><%= @error_message %></p>
</body>
</html>
```

### views/hello.erb

ページ用 HTML テンプレート。

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello, World!</title>
</head>
<body>
<p><%= h @message %></p>
<p>Ruby: <%= h @ruby_desc %></p>
<p>development?: <%= dev  %></p>
<p>production?:  <%= prod %></p>
<p>test?:        <%= test %></p>
<p><img src="/images/icon.png" alt="icon" title="icon"></p>
</body>
</html>
```

## ローカルでサーバを起動

### bundle install で環境を構築

bundle install コマンドで Gemfile.lock または Gemfile に記載してあるパッケージがインストールされる。

### development 環境

開発時に使う起動方法。
RACK_ENV=development を指定する。
エラーメッセージなどがわかりやすく Web ページ上に表示される。

```
$ RACK_ENV=development bundle exec rackup -s Puma
Puma starting in single mode...
* Version 4.3.1 (ruby 2.5.7-p206), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://127.0.0.1:9292
* Listening on tcp://[::1]:9292
Use Ctrl-C to stop
```

### production 環境

動作確認時に使う起動方法。
RACK_ENV=production を指定する。

```
$ RACK_ENV=production bundle exec rackup -s Puma
Puma starting in single mode...
* Version 4.3.1 (ruby 2.5.7-p206), codename: Mysterious Traveller
* Min threads: 0, max threads: 16
* Environment: production
* Listening on tcp://0.0.0.0:9292
Use Ctrl-C to stop
```

## Google App Engine にデプロイ

事前にデプロイ先のプロジェクトの作成や gcloud コマンドでアカウント認証などをしておく(課金設定等のためCUIコマンドだけでは完結しないと思われる)。

参考: [Quickstart for Ruby in the App Engine Standard Environment](https://cloud.google.com/appengine/docs/standard/ruby/quickstart?hl=ja)

```
$ gcloud auth login
$ gcloud projects create [YOUR_PROJECT_ID] --set-as-default
$ gcloud app create --project=[YOUR_PROJECT_ID]
$ gcloud config set project [YOUR_PROJECT_ID]
$ gcloud config list
```

gcloud app deploy コマンドで Google App Engine にデプロイする。

```
$ gcloud app deploy
```

## Google App Engine のサーバにアクセス

### 200 OK

```
$ curl -i "https://simple-sample.appspot.com/hello/こんにちは世界<>\"'&"
HTTP/2 200 
content-type: text/html;charset=utf-8
vary: Accept-Encoding
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-cloud-trace-context: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;o=1
date: Sun, 09 Feb 2020 04:20:35 GMT
server: Google Frontend
content-length: 379
alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello, World!</title>
</head>
<body>
<p>こんにちは世界&lt;&gt;&quot;&#x27;&amp;</p>
<p>Ruby: ruby 2.5.7p206 (2019-10-01 revision 67816) [x86_64-linux]</p>
<p>development?: false</p>
<p>production?:  true</p>
<p>test?:        false</p>
<p><img src="/images/icon.png" alt="icon" title="icon"></p>
</body>
</html>
```

### 404 Not Found

```
$ curl -i https://simple-sample.appspot.com/
HTTP/2 404 
content-type: text/html;charset=utf-8
vary: Accept-Encoding
x-cascade: pass
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-cloud-trace-context: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
date: Sun, 09 Feb 2020 04:14:11 GMT
server: Google Frontend
content-length: 133
alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Error page</title>
</head>
<body>
<p>Page Not Found</p>
</body>
</html>
```

### 500 Internal Server Error

```
$ curl -i https://simple-sample.appspot.com/hello/?q=テスト用エラー
HTTP/2 500 
content-type: text/html;charset=utf-8
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-cloud-trace-context: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX;o=1
date: Sun, 09 Feb 2020 04:14:37 GMT
server: Google Frontend
content-length: 140
alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Error page</title>
</head>
<body>
<p>テスト用エラー</p>
</body>
</html>
```

### 画像へのアクセス

app.yaml の handlers で静的配信指定しているのでレスポンスヘッダに Cache-Control や Expires が設定される。

```
$ curl -i https://simple-sample.appspot.com/images/icon.png
HTTP/2 200 
date: Sun, 09 Feb 2020 04:14:50 GMT
expires: Sun, 09 Feb 2020 04:24:50 GMT
etag: "XXXXXX"
x-cloud-trace-context: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
content-type: image/png
server: Google Frontend
cache-control: public, max-age=600
content-length: 18136
age: 0
alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q050=":443"; ma=2592000,h3-Q049=":443"; ma=2592000,h3-Q048=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000
```

## 参考資料

- [App Engine スタンダード環境  \|  App Engine ドキュメント  \|  Google Cloud](https://cloud.google.com/appengine/docs/standard/)
    - [Google App Engine Ruby 2\.5 Standard Environment documentation](https://cloud.google.com/appengine/docs/standard/ruby?hl=ja)
        - [Quickstart for Ruby in the App Engine Standard Environment](https://cloud.google.com/appengine/docs/standard/ruby/quickstart?hl=ja)
        - [app\.yaml Configuration File](https://cloud.google.com/appengine/docs/standard/ruby/config/appref)
        - [Ruby Runtime Environment](https://cloud.google.com/appengine/docs/standard/ruby/runtime)
- [Sinatra: README \(Japanese\)](http://sinatrarb.com/intro-ja.html)
- [ruby\-docs\-samples/appengine/standard\-hello\_world at master · GoogleCloudPlatform/ruby\-docs\-samples · GitHub](https://github.com/GoogleCloudPlatform/ruby-docs-samples/tree/master/appengine/standard-hello_world)