PHP
GAE
GoogleAppEngine
GoogleCloudStorage
GoogleCloudPlatform

Google App Engineで普通のWebサイトを格安にホスティングする

はじめに

経緯

静的コンテンツを中心に、PHPによるお問い合わせフォームが付いているような、シンプルかつ実用的なWebサイトを、格安、かつ拡張性・信頼性のある形で提供出来ないかと思い、Google App Engineでのホスティングを試してみました。

筆者の環境

  • お名前.com
  • Windows 10 Pro 64ビット
  • Chrome

技術選定

Standard Environment / Flexible Environment

App Engineには、1台あたりのCPUやメモリの構成に既定のものを用いるStandard Environmentと、より細かく設定できるFlexible Environmentがあります。Standard Environmentで対応しているPHPのバージョンは5.5のみですが、Flexible EnvironmentではPHPのより新しいバージョンにも対応しているようです。ただし、Flexible Environmentには無料利用枠が無いので、今回はStandard Environmentを使用することにします。

データの保存先

App Engineは分散環境なので、PHPスクリプトからローカルファイルへのアクセスはできません。データの保存先としては、以下のような候補があります。

  • Cloud Storage
    • いわゆるファイル単位でデータを保存するためのストレージです。Amazon S3と同等のサービスです。PHPからは、専用のAPIの他、file_put_contents()やその他一部のPHP関数でアクセスできます(App Engineドキュメント参照)。
  • Cloud Datastore
    • Key-Value型のNoSQLデータベースです。
  • Cloud SQL
    • MySQL/PostgreSQL互換のRDBMSです。

Cloud SQLには無料利用枠が無く、またインスタンスを起動しておくだけで最小でも\$0.0150/時(=\$10.8/月)かかってしまうので、Cloud DatastoreおよびCloud Storageを使うようにしました。

アプリケーションフレームワーク

以下の理由から、Laravel等のアプリケーションフレームワークは利用しません。

  • PHPのバージョンが5.5である点、ローカルファイルへのアクセスができない点から、既存のフレームワークを利用するには一工夫が必要。
  • フレームワーク上のいわゆるルーティング等でPHPスクリプトが動くと、App EngineインスタンスのCPU時間が消費されてしまい、コスト増につながる。
  • 今回はコストを抑えるため、Google Cloud Platform上のRDBMSは一切利用できない。

通知先

Webサイト上でいわゆる投稿があった際の通知先として、まず思いつくのがメールです。メールはPHPの標準のmail()関数や、専用のAPIで送れます。ただ、App Engineの提供するメールサーバは、課金アカウントであっても100通/日までしか送れず、割り当てを増やすこともできないので、実用的ではありません。また、専用APIで送れる管理者向けメールの制限は5000通/日となっていますが、1メールのサイズが16KBまでに制限されています。(App Engineドキュメント参照

本格的にメールを送る場合は、App Engineと提携しているSendGridなどを用いる方が良いでしょう。

なお今回は、メール以外の方法で通知を行うことにしました。(本題から逸れるので、実装は省略します。)

ネームサーバ

無料利用枠はありませんが、Google Cloud PlatformでCloud DNSが提供されています。ドメインレジストラなどが提供する無料のネームサーバを用いても良いのですが、料金がわずかなのと、パフォーマンスや、将来的にAPIでDNSを設定できるメリットから、Cloud DNSを利用することにしました。

価格

Google Cloud Platformの無料利用枠、および料金は以下のとおりです。アクセス数の少ないWebサイトでしたら、Cloud DNSの料金が毎月100円以下かかるのみになると思われます。

  • App Engine
    • デフォルトで使用されるF1インスタンスを基準として、28インスタンス時間まで無料です。つまり、通常1インスタンスで動作しており、2インスタンスにスケールアウトされる時間が4時間以内なら無料です。
    • App Engineのデフォルトバケットとして利用されるCloud Storageに対して、容量5GBまで無料です。
    • 送信トラフィック1GB/日まで無料です。
  • Cloud Storage
    • 容量5GBまで無料です。
    • 書き込み5000回/日、読み取り50000回/日まで無料です。
  • Cloud Datastore
    • 容量1GBまで無料です。
    • 書き込み20000回/日、読み取り50000回/日まで無料です。
  • Cloud DNS
    • ゾーン毎に\$0.20/月
    • クエリ100万件ごとに$0.40/月

You might want to use the default bucket which provides the first 5GB of storage for free.
- https://cloud.google.com/appengine/docs/standard/python/googlecloudstorageclient/setting-up-cloud-storage

インフラ設定

Google Cloud Platformへの登録

Google Cloud Platformを開きます。必要に応じて無料トライアルを開始してください。なお、プロジェクトのオーナーや課金管理者は後から変更出来ますので、これから作成する成果物が、ここで使用するGoogleアカウントに永続的に関連づけられるわけではありません。

デフォルトのプロジェクトの削除

無料トライアルを開始した場合は、デフォルトでプロジェクトが作成されているはずですが、必要なプロジェクトは所望のIDで作成するので、[IAMと管理]-[設定]-[削除]から既存のプロジェクトを削除します。

プロジェクトの作成

プロジェクトを作成します。プロジェクトには、Google Cloud Platform全体を通じて一意なIDを付与する必要があります。プロジェクトに対して1つ以上のドメイン名が対応づけられることになります。

プロジェクトを作成すると、ダッシュボードが以下のようになります。

App Engineアプリの作成

[App Engine]からアプリを作成します。

アプリを動かすリージョンを選択します。

なお、リージョンにまたがってアプリを配置することはできません。マルチリージョンでのコンテンツ配信などが必要な場合、Cloud Storageなどを併用することになります。

App Engine is regional, which means the infrastructure that runs your apps is located in a specific region and is managed by Google to be redundantly available across all the zones within that region.
- https://cloud.google.com/appengine/docs/locations

Cloud SDKのインストール

Cloud SDKをインストールします。gcloudコマンドを実行する際には、管理者として実行する必要がある場合があります。

コンテンツの作成

手元の環境に作業ディレクトリを用意し、以下のファイルを作成します。

app.yaml
runtime: php55
api_version: 1
handlers:
- url: /
  static_files: index.html
  upload: index.html
index.html
<html>
<body>
Hello Google App Engine.
<a href="contact.html">Contact</a>
</body>
</html>

gcloudコマンドの設定

現在のgcloudコマンドの設定を確認します。

>gcloud config list
[core]
account = {{your account name}}
disable_usage_reporting = True
project = data9824-static-web-example

Your active configuration is: [default]

必要に応じて、accountやprojectを設定します。

>gcloud config set project data9824-static-web-example
Updated property [core/project].

デプロイ

作業ディレクトリから、アプリをデプロイします。

>gcloud app deploy

App Engineのダッシュボードに以下のように表示され、デプロイされたのが確認できます。

デプロイされたアプリは、 https://{プロジェクト名}.appspot.com/ からブラウズできます。

Cloud DNSの設定

[ネットワーキング]-[Cloud DNS]から、これから設定するカスタムドメインに対応するDNSゾーンを作成します。

NSレコードに設定されているネームサーバを、ドメインレジストラ側でネームサーバとして設定します。

カスタムドメインの追加

[App Engine]-[設定]から、カスタムドメインを追加して、所望のドメインでアクセス出来るようにします。

ドメインの所有権の確認が必要になるので、Googleの指示に従い、[ネットワーキング]-[Cloud DNS]から先ほど追加したゾーンに、TXTレコード、もしくはCNAMEレコードを設定します。このレコードは、今後も定期的にドメインの所有権の確認に用いられますので、削除しないようにしてください。

ドメインの所有権が確認出来たら、カスタムドメインを登録します。

そして、[App Engine]-[設定]-[カスタムドメイン]で指定されるAレコード、AAAAレコードを、[ネットワーキング]-[Cloud DNS]から、ゾーンに設定します。

なお、私の場合はネームサーバにCloud DNSを用いることにしましたが、レジストラが提供するネームサーバ等を利用する場合は、レジストラ側で各レコードを設定してください。

SSLサーバ証明書の設定

Let's Encryptの証明書を利用する場合

[App Engine]-[設定]-[カスタムドメイン]で、[マネージドセキュリティを有効にする]とすることで、Let's Encryptの証明書が設定され、自動更新されます。

これにより、所望のドメインでSSLによるアクセスができるようになります。

その他のSSL証明書を利用する場合

[App Engine]-[設定]から、SSLサーバ証明書、および秘密鍵を登録します。

認証局によっては、認証用にファイルをデプロイする必要があることがあります。以下に例を示します。

以下のようにapp.yamlを設定の上、well-known/acme-challenge/{your file}に必要なファイルを配置しています。ドットで始まる.well-knownはデプロイ対象からデフォルトで除外されてしまいますので、ドットの付かないwell-knownをデプロイしています。ご注意ください。

app.yaml
runtime: php55
api_version: 1
handlers:
- url: /
  static_files: index.html
  upload: index.html
- url: /.well-known/(.+)
  static_files: well-known/\1
  upload: well-known/(.+)

コンテンツの作成

PHPパッケージの取得

Google Cloud Client Library for PHPのパッケージを取得します。なお、本記事の例ではGoogle APIs Client Library for PHPは用いていません。

>composer require google/cloud google/cloud-datastore google/cloud-storage

API呼び出しのための認証情報の作成

[API Manager]-[認証情報]-[認証情報の作成]-[サービスアカウントキー]から、App Engineにアクセスするためのキーを作成し、JSON形式で取得し、作業ディレクトリにcredentials.jsonとして保存します。

コンテンツの作成

以下のとおりコンテンツを作成します。

app.yaml
runtime: php55
api_version: 1
instance_class: F1
automatic_scaling:
  min_idle_instances: automatic
  max_idle_instances: 1
  min_pending_latency: 3000ms
  max_pending_latency: automatic
handlers:
- url: /.well-known/(.+)
  static_files: well-known/\1
  upload: well-known/(.+)
- url: /vendor/*
  script: 404.php
  secure: always
- url: /
  static_files: index.html
  upload: index.html
  secure: always
- url: /(.+\.php)
  script: \1
  secure: always
- url: /((index|contact)\.html)
  static_files: \1
  upload: (.+\.html)
  secure: always
- url: /(.+\.(css|js|jpg|png))
  static_files: \1
  upload: (.+\.(css|js|jpg|png))
  secure: always
- url: /.*
  script: 404.php
  secure: always
contact.html
<html>
<body>
<form action="contact.php" method="post" enctype="multipart/form-data">
    <textarea name="content"></textarea>
    <input name="attached" type="file">
    <input type="submit">
</form>
</body>
</html>
contact.php
<?php

require 'vendor/autoload.php';

use Google\Cloud\Datastore\DatastoreClient;
use Google\Cloud\Storage\StorageClient;

putenv('GOOGLE_APPLICATION_CREDENTIALS=credentials.json');

// Store data
$projectId = /* 作成したプロジェクトのID */;
$datastore = new DatastoreClient(['projectId' => $projectId]);
$key = $datastore->key('ContactMessage');
$key = $datastore->allocateId($key);
$storage = new StorageClient(['projectId' => $projectId]);
$bucket = $storage->bucket($projectId . '.appspot.com');
$object = NULL;
$now = new DateTime();
if (isset($_FILES['attached'])) {
    if ($_FILES['attached']['size'] > 10 * 1024 * 1024) {
        http_response_code(400);
        exit;
    }
    $object = $bucket->object('attached/' . $now->format('YmdHis') . '-' . $key->path()[0]['id']);
    if (!move_uploaded_file($_FILES['attached']['tmp_name'], $object->gcsUri())) {
        $object->delete();
        $object = NULL;
        http_response_code(400);
        exit;
    }
    $object->update([
        'contentDisposition' => 'attachment; filename=' . $_FILES['attached']['name']
    ]);
}
$entity = [
    'created' => $now,
    'content' => $_REQUEST['content']
];
if ($object !== NULL) {
    $entity['attached'] = $object->gcsUri();
    $entity['attachedUrl'] = 'https://storage.cloud.google.com/' . $bucket->name() . '/' . $object->name();
}
$message = $datastore->entity(
    $key,
    $entity,
    [
        'excludeFromIndexes' => [
            'content'
        ]
    ]
);
$datastore->insert($message);
?>
<html>
<body>
Done!
</body>
</html>
404.php
<?php
http_response_code(404);
?>
<html>
<body>
The requested page doesn't exist.
</body>
</html>
php.ini
google_app_engine.enable_functions = "php_sapi_name"
upload_max_filesize = "11M"
post_max_size = "11M"

コンテンツを作成したら、デプロイします。

>gcloud app deploy

ブラウザから作成したWebサイトを確認できます。contact.htmlから投稿してみてください。[データストア]で投稿した内容を確認できます。

[Storage]で投稿したファイルを確認できます。

開発環境

開発用サーバ

ローカル環境にPython 2.7をインストールしておけば、開発用のサーバを起動できます。作業ディレクトリで以下のコマンドを実行してください。

python "C:\Program Files (x86)\Google\Cloud SDK\google-cloud-sdk\bin\dev_appserver.py" .

プライベートGitリポジトリ

[Source Repositories]からプライベートGitリポジトリを作成でき、コンテンツの管理に利用できます。容量1GBまで無料で利用できます。

注意事項

スケーリング

デフォルト設定ではインスタンスが起動されすぎて、料金に影響を与えるので、app.yamlautomatic_scalingに記述したとおり、待機するインスタンスの数は最大でも1つ、リクエストの待機時間が3000msを超えたら新しいインスタンスを起動するようにしています。

認証

Apacheの.htaccessのように、Basic認証を簡単に設定することはできません。どうしても必要な場合はスクリプトで実装することになりますが、CPU時間の無駄な消費や、静的コンテンツの配信パフォーマンス等の観点から、お勧めできません。

開発中のサイトを外部に見せたくないという要件の場合は、app.yamlhandlersの各urllogin: adminを記載することで、プロジェクトの管理者のみを認証することができます。

リダイレクト

同様に、リダイレクトについても設定ファイルで簡単に設定することはできません。必要な場合はスクリプトで実装することになります。

ただし、HTTPからHTTPSへのリダイレクトは、app.yamlsecure: alwaysの記載で行えます。

ステータスコード404

App Engineでは、デフォルトの404応答を直接変更する方法はありません。そこで以下の方法で、存在しないHTMLファイルに対する独自の404応答を生成しています。

  • app.yamlhandlersの末尾に404応答を生成するスクリプトを設定。
  • .htmlで終わるURLについて、明示的にHTMLファイル名をURLのパターンとして記載することで、存在しないHTMLファイルに相当するURLにマッチしないようにする。

php.ini

php.iniのカスタマイズが必要な場合、コンテンツのルートに配置します。

The php.ini file should be placed in the base directory of an application (the same directory as the app.yaml file).
- https://cloud.google.com/appengine/docs/standard/php/config/php_ini

PHPスクリプトでのPOSTされたファイルの受信

32Mバイト未満のファイルを受け取る場合、アップロードされたファイルは、上記PHPスクリプトのように通常通り$_FILESを用いて取り扱えます(記事参照)。それ以上のサイズのファイルを受け取るには、特別な方法をとる必要があります(App Engineのドキュメント参照)。

Cloud StorageのオブジェクトのURL

Cloud Storageのオブジェクトは、デフォルトでは公開設定になっていませんが、当該プロジェクトのアカウントでログインできるならば、以下のURLでアクセスできます(Cloud Storageドキュメント参照)。

https://storage.cloud.google.com/(バケット名)/(オブジェクト名)

Cloud Storageのオブジェクトのレスポンスヘッダ

Cloud Storageのオブジェクトのレスポンスヘッダは、上記PHPスクリプトのようにAPIから設定可能です(Cloud Storageドキュメント参照)。

まとめ

一般的なレンタルサーバで静的コンテンツ中心のWebサイトを作った場合と同等のWebサイトを、App Engine上で構築できました。

格安に抑えるためにはCloud SQLを利用できない、すなわちマネージドなRDBMSを利用できないため、一般的なWebアプリケーションフレームワークの採用は難しそうですが、Key-Value型NoSQLや、オブジェクトストレージは利用できますので、必要に応じて自前で実装できそうです。(Compute Engineの無料インスタンスにRDBMSを構築する方法もありそうですが、マネージドサービスでは無いので、可用性や保守コストの問題があるかと思われます)

今回はアクセス数の少ないサイトを想定しましたが、オートスケールの機能があるため、アクセス数が極端に多くてもダウンしにくいサイトを比較的割安に運用できるのではないかと思います。

技術ドキュメントも充実していますが、やはりソフトウェアエンジニアでないと敷居は高そうですので、デザイナ中心のチームでは導入は難しいかもしれません。