Google App EngineのNode.js/Standard環境の仕組みについて調べてみた


2022/02/18追記

3年ほど前に書いたこの記事なのですが、Cloud Functions(こいつも裏ではCode Buildが使われ、Container Registryにイメージが置かれます)の料金確認していたらContainer RegistryはCloud Storageにファイルを置いているということが書いてありました。
https://cloud.google.com/functions/pricing#deployment_costs
https://cloud.google.com/container-registry/pricing

「一つのイメージ」に対してファイルが多すぎだろうという気がしていてダウンロードして何物なのかまで確認はしてないのですが容量食うからと言って消しちゃいけないっぽいです(ライフサイクル設定して1年経ったら消すようにしてたのですが、デプロイしたらどっか別のところにコピーされるので問題ない、ということなのかも未調査)

はじめに

Google App EngineのStandard環境でNode.jsが使えるようになりました(まだベータですけど)
というわけでランタイムをNode.jsにしてアプリをデプロイしていたのですが、ある時ふと気づきました。

asia.artifacts、お前誰だ。

とりあえず中を見てみる

何が入ってるのだろうと中を確認すると、containers/imagesとフォルダ1があって以下のようなファイルが格納されていました。

sha256って、しかも結構でかいファイルもあるし、置いとくだけで課金されてしまうではないか。

というわけでこれらのファイルが何者なのかの調査が今回の記事です。勘のいいひとはcontainersという名前で察しがつくとは思いますが。

新規プロジェクト作成

artifactsなるバケットがいつできたのか調べるためにまっさらな状態のプロジェクトから始めてみます。
ところでgcloudコマンドってなんでコンソールみたいにID適当に付けてくれるって機能がないんですかね。

App Engine有効化

まずApp Engineを有効にします。

gcloud app create --project=プロジェクトのID

この時点で以下の2つのバケットが作成されます。まあこれは以前から知っていることなので特に問題なし。

Node.jsアプリをデプロイ

ではランタイムをNode.jsにしたアプリをデプロイしてみます。内容は何でもいいので以下の超シンプルHello, Worldをデプロイします。一応最低限の説明もしておきます。

app.yaml
runtime: nodejs8

Node.jsアプリをデプロイするにはこの一行だけでOKです。ほかにもいろいろと設定できるようですが今のところ使ったことはありません。

app.js
const http = require('http');

const server = http.createServer((req, res) => {
  res.end('Hello World\n');
});

server.listen(process.env.PORT || 8080, () => {
  console.log('app started');
});

アプリを作る際に注意が必要なのはPORTにlistenすべきポート番号が設定されているという点です。それ以外は普通にNode.jsでWebサーバを作る場合と変わりません。

package.json
{
  "name": "appengine",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node app.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

ここで重要なのはscriptsのstartです。初めどうやって実行するjsファイルを認識してるのか謎だったのですが、startとして指定されているコマンドラインを実行しているだけ(つまり、「npm start」している)というからくりでした。mainで指定しているファイル名と合わせる必要もありません。
今回は何もパッケージを使っていないのでdependenciesはありませんが書いておくとパッケージがインストールされます。詳しくは種明かし編で説明します。

デプロイ実行と変化の確認

それでは改めて、デプロイを実行してみます。

gcloud app deploy --project=プロジェクトのID

デプロイ後、Storageを確認してみると例のartifactsバケットが増えています。

他に変わっているところがないかと見て回ると、しれっとサービスアカウントが増えています。
Container Registry Service Agentと、Cloud Buildの役割を持ったアカウントです。

Container Registry

Container Registryはご想像通り、Dockerのイメージを格納するためのサービスです。OCIイメージもサポートしてるらしいけどそこら辺あまり詳しくない。

というわけでContainer Registryを見てみると確かにイメージが置かれています。何故かキャッシュっぽいものしか置かれてないのですがパッケージ何も入れなかったからなのかな(実際に使ってる方はキャッシュじゃなさそうなのも置かれています)

Cloud Build

Cloud BuildはDockerのイメージを作成するためのサービスのようです。
実際にビルド履歴を見てみると確かにビルドが行われていることがわかります。ここでnpm installも行われ必要なパッケージが全部入ったコンテナイメージが構築されます。

課金を止めてみる

Standard環境なら課金なしで使えるはず、と試しに課金を無効にしてもう一回デプロイしてみます。

D:\work\js\node\appengine>gcloud app deploy --project=idyllic-orbit-221013
Services to deploy:

descriptor:      [D:\work\js\node\appengine\app.yaml]
source:          [D:\work\js\node\appengine]
target project:  [idyllic-orbit-XXXXXX]
target service:  [default]
target version:  [20181103t085812]
target url:      [https://idyllic-orbit-XXXXXX.appspot.com]


Do you want to continue (Y/n)?

Beginning deployment of service [default]...
#============================================================#
#= Uploading 0 files to Google Cloud Storage                =#
#============================================================#
File upload done.
Updating service [default]...failed.
ERROR: (gcloud.app.deploy) Error Response: [9] Cloud build b6c7ba75-47dd-4c03-b9d1-0e57e7ea170c status: FAILURE.
Build error details: Build error details not available..
Check the build log for errors: https://console.cloud.google.com/gcr/builds/b6c7ba75-47dd-4c03-b9d1-0e57e7ea170c?project=XXXXXXXXXXXX

ログを確認してみる。

Step #1 - "builder": containerregistry.client.v2_2.docker_http_.V2DiagnosticException: response: {'status': '403', 'content-length': '297', 'x-xss-protection': '1; mode=block', 'transfer-encoding': 'chunked', 'server': 'Docker Registry', '-content-encoding': 'gzip', 'docker-distribution-api-version': 'registry/2.0', 'cache-control': 'private', 'date': 'Fri, 02 Nov 2018 23:58:23 GMT', 'x-frame-options': 'SAMEORIGIN', 'content-type': 'application/json'}
Step #1 - "builder": Permission denied for "0dc31f7d79eec6d1fcd635818d688d795e58cddf5663e1e3ac6cefb53a390146" from request "/v2/idyllic-orbit-XXXXXX/app-engine-tmp/build-cache/ttl-7d/node-cache/manifests/0dc31f7d79eec6d1fcd635818d688d795e58cddf5663e1e3ac6cefb53a390146". : None

いつから無課金でNode.jsが使えると錯覚していた?
まあ実際にはこのページを信じると、無料枠以上使うことはないので実質無課金のようですが。

Enable billing for your project.

Note: The sample app in this guide does not exceed the free quotas. You will only be charged if your GCP usage exceeds the free quotas.

まとめ

話をまとめます。

  • App EngineのStandard環境ではNode.jsはDockerイメージが構築されてデプロイされる。その際、Storageにバケットが作られる。
  • (おそらく)お金を支払うことはないが、課金は有効にしておかないとデプロイできない。

ちなみにPythonの場合はこの方法はとられておらず、課金設定をしなくても問題なくデプロイして動かすことができます。逆にどうなってるのか気になるところですがまあDocker出てくる前から存在しているので独自の方法でデプロイしているのでしょう。
それ以外の言語については動かしたことがないので知りません。

参考資料

App EngineのStandard環境でNode.jsを動かす方法は以下のページが詳しいです。ちなみに書いてる途中でNode.js v10がLTSになってランタイムとしてnodejs10が使えるようになったみたいですね。
https://cloud.google.com/appengine/docs/standard/nodejs/runtime


  1. AWSもですけど、バケットのフォルダはフォルダとしての実体(権限設定とかが行える対象)があるわけではなく、あくまで複数のオブジェクトを「格納されているように見せる」ものです。