CODING ECHO

インターネット広告屋でエンジニアをやっている人のブログ

Cloud Deployment Managerでバケット作成時のパーミッションエラーはバケット名が原因かもしれない

Cloud Deployment Managerでプロビジョニングするとき、エラー表記で少しハマったところについてメモ。

Cloud Storageをプロビジョニングするとき以下のようなエラーが出ることがあります。

$ gcloud deployment-manager deployments create example --config vm.yaml
The fingerprint of the deployment is YjuHda-HR8_aaaaaaaaaaa==
Waiting for create [operation-1549936918163-581a8cf6e71ac-aaaaaaaa-3f20fe82]...failed.                                                                                      
ERROR: (gcloud.deployment-manager.deployments.create) Error in Operation [operation-1549936918163-581a8cf6e71ac-aaaaaaaa-3f20fe82]: errors:
- code: RESOURCE_ERROR
  location: /deployments/batch/resources/example
  message: '{"ResourceType":"gcp-types/cloudfunctions-v1:projects.locations.functions","ResourceErrorCode":"500","ResourceErrorMessage":"Failed
    to retrieve function source code"}'
- code: RESOURCE_ERROR
  location: /deployments/batch/resources/image
  message: '{"ResourceType":"gcp-types/storage-v1:buckets","ResourceErrorCode":"403","ResourceErrorMessage":{"code":403,"errors":[{"domain":"global","message":"example@cloudservices.gserviceaccount.com
    does not have storage.buckets.get access to image.","reason":"forbidden"}],"message":"example@cloudservices.gserviceaccount.com
    does not have storage.buckets.get access to image.","statusMessage":"Forbidden","requestPath":"https://www.googleapis.com/storage/v1/b/image","httpMethod":"GET","suggestion":"Consider
    granting permissions to example@cloudservices.gserviceaccount.com"}}'

does not have storage.buckets.get access to image.","statusMessage":"Forbidden"Consider granting permissions to example@cloudservices.gserviceaccount.com と言われるので、普通に考えるとパーミッションかなと思いサービスアカウントの権限を確認しますが十分な権限が付与されていました。

結論としては、作成しようとしているバケットの名前が他のユーザーのバケット名と被っているためです。バケットの名前は全てのユーザーでユニークな名前にする必要があります。

当たり前ですが作成しようとしているバケットが自分のものではない場合、そのバケットにアクセスする権限はありません。なので、does not have storage.buckets.get access to image のような表記になっています。

本のカバーはなぜ必要なのか?

カバーといっても書店の店員さんが後からつけてくれるやつではなくて、初めから付いている値段やバーコードが書かれているカバーです。

本を読むときに邪魔になっていたのでなぜ必要なのか以前から気になっていました。

「本を汚れから守る」ようなことを見かけましたが、出典元を探せていませんでした。多分それっぽいとは思っていましたが。

それについて、先日たまたま読んだ「出版」に記載がありました。

p.52

カバーの存在理由。それはひとつに、「取り替えられる」からである。

出版〈2018年度版〉 (産業と会社研究シリーズ)

出版〈2018年度版〉 (産業と会社研究シリーズ)

間違いなさそうです。

取り替えのサイクルとして

  1. 本が手に取られて汚れる
  2. 取次に返品
  3. 取次でカバーを変えて書店に戻る

の流れがあるようです。カバーがあるおかげで、汚れた本を処分することなく新品として書店に戻るわけです。

また、取次に戻らずに出版の営業の人が書店でカバーだけを変えていくパターンもあるようです。

ちなみにカバーも編集の人に依頼された(出版に所属するか外部の)デザイナーさんが考えて作ってくれたものなので、捨てるときはその辺を思い出してみるといいかもしれません!

PythonでgRPCのリクエストをする

PythonでgRPCを扱う方法です。クライアント側だけの紹介です。

Protocol Buffersの定義ファイルを用意します。

crawl.proto

syntax = 'proto3';

package proto;

service Crawler {
    rpc Crawl(CrawlRequest) returns (CrawlResponse);
}

message CrawlRequest {
    string url = 1;
}

message CrawlResponse {
    string content = 2;
}

Protocol Buffersの定義ファイルを、Pythonのソースファイルにコンパイルします。

以下のコマンドを実行します。

$ protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/crawl.proto

$SRC_DIR: protoファイルがあるディレクトリ

$DST_DIR: コンパイルしたソースファイルの保存先

crawl_pb2.pycrawl_pb2_grpc.py のファイルが生成されます。

生成されたPythonのファイルを利用してgRPCのサーバーに対してリクエストします。

import grpc
from crawl_pb2_grpc import CrawlerStub
from import CrawlRequest

def crawl(url):
    channel = grpc.insecure_channel('localhost:50005')
    crawl_request = CrawlRequest(url=url)  # リクエストで送るデータの生成
    stub = CrawlerStub(channel)  
    result = stub.Crawl(crawl_request)  # リクエスト

    print(result.content)  # レスポンスのcontentを取得

参考

Pythonで文字列に絵文字と半角カタカナが含まれているか判定する

ここではPython 3.7を使った方法を紹介します。

絵文字の判定

現時点の最新版であるUnicode® Emoji Charts v11.0では、絵文字のUnicodeは1F600から1F3F4の範囲でコードが振られています。

なので、Unicodeの範囲を指定すればいけるじゃんと思っていたのですが、絵文字についてはUnicodeが連番ではなかったので正規表現で範囲を指定した方法ができませんでした。WikipediaのUnicode表を参考に範囲を指定していますが、絵文字以外も判定されてしまいます。

以下がダメなサンプルコードです。

text = '今日はいい天気'
if re.search('[\U00010000-\U0010ffff]|[\U000000A9-\U00003299]', text):
    print('found it!')
else:
    print('not found')

ちなみに、新しい絵文字を扱うため \U (UTF-32) を使っています。

上記のサンプルコードの範囲の間にはひらがななどの絵文字ではない文字が含まれているのでこのコードでは絵文字を判定できません。

いろいろ調べてみて最終的にemojiというライブラリが良さそうなのでこちらを使用しました。

import emoji

def text_is_emoji(text):
    for c in text:
        if c in emoji.UNICODE_EMOJI:
            return True
    return False

text = '今日はいい天気'

if text_is_emoji(text):
    print('found it!')
else:
    print('not found')

結果

not found
import emoji

def text_is_emoji(text):
    for c in text:
        if c in emoji.UNICODE_EMOJI:
            return True
    return False

text = '今日はいい天気☀️'

if text_is_emoji(text):
    print('found it!')
else:
    print('not found')

結果

found it!

半角カタカナの判定

半角カタカナのUnicodeはFF61からFF9Fまでの範囲が決まっていので簡単です。

以下は半角カタカナを判定するサンプルコードです。UnicodeのエンコーディングにはUTF-8を使います。

全角カタカナ

import re

text = '今日はイイ天気'
if re.search('[\uFF61-\uFF9F]', text):
    print('found it!')
else:
    print('not found')

結果

not found

半角カタカナ

import re

text = '今日はイイ天気'
if re.search('[\uFF61-\uFF9F]', text):
    print('found it!')
else:
    print('not found')

結果

found it!

正規表現で UTF-8 と UTF-32 の扱いかた

Pythonの正規表現でUnicodeを扱うにはコードの前に \u または \U を使用します。

UTF-8の場合は \u、UTF-32の場合は \U を使います。詳しくは公式ドキュメントを参照してください。

おわりに

絵文字に関してはUnicode® Emoji Charts v11.0のようにバージョンが振られていることからもわかるように、絵文字の数が年々増えています。なので、絵文字が正しく判定されているか適宜見直していく必要がありそうです。

参考

RNNと時系列データを使った気温の予測

時系列データをRNNで扱うサンプルです。過去の気温から未来の気温予想を予測します。

以下のサンプルはGoogle Colaboratory上で実行しています。

気象データセットのダウンロードします。

ちなみに !<command> のようにするとnotebook上でコマンドが使えます。

!wget https://s3.amazonaws.com/keras-datasets/jena_climate_2009_2016.csv.zip
!unzip jena_climate_2009_2016.csv.zip

ロードしてヘッダーとデータ数を確認します。

import os

fname = 'jena_climate_2009_2016.csv'

f = open(fname)
data = f.read()
f.close()

lines = data.split('\n')
header = lines[0].split(',')
lines = lines[1:]

print(header)
print(len(lines))
import numpy as np

float_data = np.zeros((len(lines), len(header) - 1))
for i, line in enumerate(lines):
  values = [float(x) for x in line.split(',')[1:]]
  float_data[i, :] = values

とりあえずプロットしてみます。縦軸は摂氏温度、横軸は10分ごとの時系列です。

from matplotlib import pyplot as plt

temp = float_data[:, 1]
plt.plot(range(len(temp)), temp)

f:id:ytanak:20190130075555p:plain
なんとなく周期性があることがわかりますね!

過去の期間(lookback)を見て、先のポイント(delay)を予測します。

def generator(data, lookback, delay, min_index, max_index, shuffle=False, batch_size=128, step=6):
    if max_index is None:
        max_index = len(data) - delay - 1
    i = min_index + lookback
    while 1:
        if shuffle:
            rows = np.random.randint(min_index + lookback, max_index, size=batch_size)
        else:
            if i + batch_size >= max_index:
                i = min_index + lookback
            rows = np.arange(i, min(i + batch_size, max_index))
            i += len(rows)

        samples = np.zeros((len(rows),
                           lookback // step,
                           data.shape[-1]))
        targets = np.zeros((len(rows),))
        for j, row in enumerate(rows):
            indices = range(rows[j] - lookback, rows[j], step)
            samples[j] = data[indices]
            targets[j] = data[rows[j] + delay][1]
        yield samples, targets

元のデータセットから学習、検証、テスト用のデータに分けます。

lookback = 1440
step = 6
delay = 144
batch_size = 128
train_gen = generator(float_data,
                      lookback=lookback,
                      delay=delay,
                      min_index=0,
                      max_index=200000,
                      shuffle=True,
                      step=step,
                      batch_size=batch_size)
val_gen = generator(float_data,
                    lookback=lookback,
                    delay=delay,
                    min_index=200001,
                    max_index=300000,
                    step=step,
                    batch_size=batch_size)
test_gen = generator(float_data,
                     lookback=lookback,
                     delay=delay,
                     min_index=300001,
                     max_index=None,
                     step=step,
                     batch_size=batch_size)

val_steps = (300000 - 200001 - lookback)

test_steps = (len(float_data) - 300001 - lookback)

Sequential を使ってRNNを構築して学習させます。

from tensorflow.keras.models import Sequential
from tensorflow.keras import layers
from tensorflow.keras.optimizers import RMSprop

model = Sequential()
model.add(layers.Flatten(input_shape=(lookback // step, float_data.shape[-1])))
model.add(layers.Dense(32, activation='relu'))
model.add(layers.Dense(1))
model.compile(optimizer=RMSprop(), loss='mae')
history = model.fit_generator(train_gen,
                              steps_per_epoch=500,
                              epochs=20,
                              validation_data=val_gen,
                              validation_steps=batch_size)
Epoch 1/20
500/500 [==============================] - 9s 19ms/step - loss: 1.5980 - val_loss: 0.4938
Epoch 2/20
500/500 [==============================] - 9s 17ms/step - loss: 0.5903 - val_loss: 0.3942

予測するには predict を使います。

d = next(train_gen)
x = d[0]
print(x[0:2].shape)
b = np.array(x[0:2])
model.predict(b)

参考

時系列データの扱い方はKeras作者のCholletさんの本に詳しく紹介されています。

Deep Learning with Python

Deep Learning with Python

日本語

PythonとKerasによるディープラーニング

PythonとKerasによるディープラーニング

Google App Engineを使って運用が辛くならない社内向けWebアプリを作る

Google App Engineが社内のWeb Appやちょっとしたツールを公開したいときにとても良さそうだったので紹介したいと思います。

問題点

社内でちょっとWebアプリを提供する環境が欲しいと言ったことはないでしょうか?

Amazon Web ServiceのEC2やGoogle Cloud PlatformのCompute Engine を使っていたり、オンプレミスの環境を用意したりしていますか?

このようなサービスや物理サーバーを使おうとすると、後々になって管理が大変になるだったことがよくあるかもしれません。特にクラウド上で共用で使用しているプロジェクトで、どのインスタンスが使われているかわからなかったり削除してもいいのかわからなくなるなどサーバー管理者にとってとても手間のかかる作業が増えると思います。また少し使うだけなのに、使っていない間も起動しっぱなしになってしまい課金が発生したりします。

社内に公開して外部には公開したくないので認証機能をいれないことも多いと思います。アカウント認証にActive Directoryを使っていることも多い思います。

App Engineを使うとどうなるの?

  • 予算管理の時間を節約できます。リクエストがない間は課金が発生しません。
  • 認証管理や構築の時間を節約できます。G Suiteの認証にActive Directoryを使用しているならApp Engineでも数ステップの設定でActive Directoryによる認証を使うことができます。
  • インフラ管理の時間を節約できます。自動でスケールしてくれたりログをまとめてくれます。

事前に用意すること

  • G Suiteと独自ドメイン
  • Google Cloud Platformのアカウントと支払の設定

公開準備

公開までは

  1. App Engineを課金可能な状態にしておく
  2. ソースコードを書く
  3. アプリケーションを公開する
  4. App Engineの認証設定で特定のドメインのみ許可するように設定する

こんな感じです。

細かく見ていきましょう。

App Engineを課金可能な状態にしておく

App Engineのコンソールを開いて、以下の図のようになっていれば大丈夫です。

f:id:ytanak:20190127085606p:plain
App Engineのコンソール画面

このような画面になっていなく初期設定が必要であればそれに従ってください。

ソースコードを書く

App Engineの画面右上に左から1番目のCloud Shellアイコンをクリックします。

f:id:ytanak:20190127090027p:plain
左から1番目のCloud Shellをクリックする

ローカルで環境整えることも可能なのですがとりあえず手っ取り早くやるには、この方法が良いと思います。Cloud Shell上でアプリケーション作ってみたいと思います。

次に画面の下にクラウドシェルシェルの画面が現れるので、そこの画面の右上にあるCode Editorをクリックします。

f:id:ytanak:20190127085938p:plain
左から2番目のCode Editorを選択する

Cloud Shellが開くので、ここにコードを書いていきます。ちなみにブラウザはSafariだとうまく動かなかったのでGoogle Chromeを使うのがいいと思います。

基本的にやっている事GitHubにあるGoogle Cloud Platformのサンプルと同じです。

必要なのは main.pyrequirements.txtapp.yaml の3つのファイルです。

main.py

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    """Return a friendly HTTP greeting."""
    return 'Hello World!'


if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8080, debug=True)

requirements.txt

runtime: python37

app.yaml

runtime: python37

全てのファイルをコピペすると以下の図のようになると思います。

f:id:ytanak:20190127091819p:plain
必要なファイルを用意した状態

アプリケーションを公開する

後はクラウドする上でコマンドを実行するだけです。

まず初めにgcloudコマンドを使うために認証してアクセストークンを取得しておきます。

$ gcloud auth application-default login

このコマンドを実行するとURLが表示されてGoogleアカウントにログインするように言われるので、リンクをクリックしてG Suiteのユーザーとしてログインします。その後ブラウザにコードが表示されるのでそれをコンソール上に貼り付けます。これでデプロイするための設定が完了しました。

次に以下のコマンドでアプリケーションの作成します。

$ cd my_app
$ gcloud app create

以下のようなエラーが出た場合は既にアプリケーションを作成されている状態なので、特に何もしなくて問題ありません。

$ gcloud app create
ERROR: (gcloud.app.create) The project [...] already contains an App Engine application in region [us-central].  You can deploy your application using `gcloud app deploy`.

次にアプリケーションをデプロイします。

$ gcloud app deploy

以下のように表示されてこの設定でデプロイしていいか聞かれるので、問題なければ実行します。

Services to deploy:

descriptor:      [/home/ysnr_tanaka/my_app/app.yaml]
source:          [/home/ysnr_tanaka/my_app]
target project:  [my-project]
target service:  [default]
target version:  [20190127t092337]
target url:      [https://my-project.appspot.com]


Do you want to continue (Y/n)?

大丈夫です。実行しましょう!

デプロイには数分から10分程度お時間がかかるので、コーヒーを淹れて待ちましょう☕️。

デプロイが完了したら以下のような表示になります。

File upload done.
Updating service [default]...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://my-project-123.appspot.com]

ブラウザで開いて確認してみます。

gcloud app browse

上記のコマンドを実行すると https://my-project-123.appspot.com のようにデプロイ先のリンクが表示されるのでクリックします。

Hello World! が表示されていれば正しくアプリケーションが動いています!

App Engineの認証設定で特定のドメインのみ許可するように設定する

App Engine -> Settings -> Edit ボタンをクリックします。

以下の図のように Google authentication を Google Apps domain に変更して、下のフォームにG Suiteで使用しているドメインを設定します。Save ボタンで設定を保存します。

f:id:ytanak:20190127094936p:plain
認証設定を Google Apps domain に変更する

ちなみに英語が表示されていますが、日本語設定の場合は日本語が表示されると思います。

次にCloud Shell上でアプリケーションの認証設定を以下のように変更します。

app.yaml

runtime: python37

handlers:
  - url: /.*
    script: auto
    login: required

この設定を追加することですべてのURLパスに対して認証を有効にさせます。

設定を有効にするためにもう一度デプロイします。

$ cd my_app
$ gcloud app deploy

これで認証の設定をしたアプリケーションができました。もう一度ログインしていない状態でアプリケーションをブラウザで開いてみると、ログイン画面が表示されると思います。G Suiteのユーザーとしてログインした状態でアクセスすると前回と同じように Hello World! が表示されます。

おわりに

アプリケーションの作成と公開、認証設定を説明しました。今回はPythonを使ってアプリケーションを作成しましたが、Node.jsやJavaなどのサポートされた言語やフレームワークも自由に使うこともできます。また、Dockerコンテナを動かすこともできるのでライブラリや言語に制限はなかったりします。

サーバーや課金、認証管理に多くの時間を使わなくなるといった点でGoogle App Engineは優れていると思います。

Node.jsのrequestでShift JISやバイナリーデータを正しく取得する方法

バージョン

request: 2.88.0

問題と解決策

requestパッケージを使ってShift JISやEUC、ZIPなどのバイナリデータを正しく取得する方法です。

日本語の文字コードをUNICODEに変換したり、zipを解凍したりするときには注意が必要です。

request({ url:'http://example.com/data.zip' }, (err, httpResponse, body) => {
    // bodyにバイナリーが入っているはず
    do something...
});

のようなやり方では勝手に body がUTF-8に変換されてしまってうまくいかないです。

なのでオプションで encoding: null を指定する必要があります。

request({ url:'http://example.com/data.zip', encoding: null }, (err, httpResponse, body) => {
    // bodyには生のバイナリーが入る
    do something...
});

このようにするとbodyはUTF-8に変換されずに生のバイナリーを受け取ることができます。

少しハマりました。。

参考

stackoverflow.com