astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

ターミナルなんて怖くない

ご挨拶

どうもお久しぶりです、元バンドマンの新米エンジニアgucciです。
未経験からエンジニアに転職して早1年が経ちました。
「まだ1年目なもんで、てへ」というのが通用しないと思うと、気を引き締めていかないとなと思う今日この頃です。
あっという間の1年であり、忘れられない1年となりました。
2年目はエンジニアとしても人間としても、もっともっと成長していきたく思います。

ターゲット

さて、綺麗事はさておき今回のブログのターゲットは、
まだプログラミングを初めて日が浅い新米エンジニアの方
や、
これからエンジニアを目指そうという未来のエンジニアの方
でございます。
もう熟練のエンジニアの皆さんからすると物足りなかったり、もっといいのがあるよ!といった内容があるかもしれませんがご容赦くださいませ。

今回のブログの内容はタイトルにもありますように「ターミナルなんて怖くない」です。
なぜこのような内容にしたのかというのも、
私がプログラミングスクールでWebアプリケーション開発の基礎を学び、未経験エンジニアとしてこの業界に入って一番感じたことは、 ターミナルめっちゃ触る。ということです。 (windowsでいうコマンドプロンプト)

f:id:astamuse:20190213143117j:plain アプリケーションを開発するのに、Java、Scala、Ruby、PHP、Goなどなど…数々のプログラミング言語がありますが、共通して言えることはコンピュータを操作するのにターミナルでのコマンド操作が必要不可欠だということです。(もちろん、働いている環境により様々なやり方があると思いますので、あまり使わない方もいるかと思います)

「ターミナルで何ができるの?」というと、

  • ファイル・ディレクトリ(いわゆるフォルダ)を作る
  • ファイルの中を編集する
  • コピー・削除する
  • ログファイルから欲しい言葉を検索する

などなど、パソコンに対する命令はほとんどターミナルでできるのです。

「ターミナルなんか使わなくてもFinder(エクスプローラ)でいいじゃない」という意見もわかります。
わかりますが、

  • ターミナルでしか行えない操作がある
  • ターミナルの方が圧倒的に速い(場合がある)
  • FinderやエクスプローラといったGUIがない環境がある

などなど、ターミナルとは必ず向き合わないと行けない日がきます。
その反面、まだプログラミングの経験が浅い段階だとターミナルの使い方がわからなかったり、うまく使えないでイライラしてしまい「ターミナルなんて嫌い!」と感じる方もいるかもしれません。
私も最初の頃はターミナル操作が全然わからず苦手でしたが、今ではドヤ顔でコマンドを叩いております。(大したことはしていませんが)
なのでまずは少しずつコマンドを叩いてみて、コンピュータに命令を出してみましょう。
ターミナルなんて怖くないんだ、ということをまずは知っていきましょう。

今回は、
「知っていて当たり前!これだけは抑えておこうターミナルコマンド!」をご紹介します。
コマンドと合わせて、そのコマンドがどういった意味を持つのか補足をします。
英語の意味と合わせて覚えるだけで、訳の分からないコマンドがグッと身近になります。
さらに、知っておくと便利なオプションや使い方もプラスαでご紹介します。

それでは参りましょう。

よく使うコマンドたち編

cd

cd 移動先のパス

ディレクトリ間を移動する。「Change Directory」の略。

cd ..
 一つ上のディレクトリに移動。
cd -
 一つ前にいたディレクトリに移動。

ls

ls

ファイルやディレクトリを表示する。「LiSt」の略。

ls -a
 隠しファイルも含めすべて表示。「all」の略。
ls -l
 ファイルの詳細も表示。「long」の略。
ls -1
 縦一列に表示。「1列」の1が覚えやすい。

pwd

pwd

現在の作業ディレクトリのパスを表示。「Print Working Directory」の略。

cp / mv / rm / mkdir

cp コピーする元 コピーする先

ファイルやディレクトリをコピーする。「CoPy」の略。

cp -r コピー元ディレクトリ コピー先ディレクトリ
 ディレクトリごとコピーする。「recursive(再帰的)」の略。←他のコマンドでもよく使うオプション。

mv 移動する元(移動するファイル名) 移動する先
mv 変更前の名前 変更後の名前

ファイルやディレクトリを移動する。また、名前を変更するのにも使う。 「MoVe」の略。

mv -v 移動する元(移動するファイル名) 移動する先
 移動の詳細を表示。「verbose」の略。←他のコマンドでもよく使うオプション。

rm ファイル名

ファイルやディレクトリを削除する。「ReMove」の略。

rm *
 全てのファイルを削除する。*はワイルドカードで、色々な場面で使う。
rm -f
 警告メッセージを表示せずに削除する。「force(強制する)」の略。フォースの力。

mkdir

ディレクトリを作成する。「MaKe DIRectory」の略。
rmdirというディレクトリを削除するコマンドもある。

less/cat

less ファイル名

ファイルの内容を一画面ずつ表示する。moreの対義語。(moreというコマンドが存在するがあまり使わない)
lessでファイルの内容を表示後
 「/」キーワード前方検索。
 「?」キーワード後方検索。
 「q」を入力して終了。

cat ファイル名

ファイルの内容を最後まで続けて表示。「conCATenate(連結する)」 の略。

cat -n
 行番号をつけて表示。「number」の略。

tail ファイル名

最終行から数行を表示する。標準では10行。そのまま「尾」という意味。

tail -f ファイル名
 ファイルの追記を監視し、追記分を表示する。「follow」の略。
 ログの監視をしている際などによく使用する。

diff

diff ファイルA ファイルB

ファイル同士の差分を表示する。「difference」の略。

diff -y ファイルA ファイルB
 比較した結果を横並びで表示。「side-by-side形式」の略。
 「横(yoko)」の「y」が覚えやすい。

grep

grep 検索正規表現 ファイル名

ファイル中の文字列に対して正規表現を使って検索して表示する。「Global Regular Expression Print」の略。

grep -i 検索正規表現 ファイル名
 大文字と小文字を区別せず検索する。「ignore-case」の略。

コマンドの標準出力 | grep 検索正規表現
 このように「|(パイプ)」でつないで、コマンドの実行結果に対して検索をかけて抽出する使い方がとても便利。

scp

scp コピーするファイル名 コピー先のリモートのホスト:パス名
scp コピーするリモートのホスト:パス名/ファイル名 コピー先のローカルパス名

ローカルとリモートサーバ間でファイルの転送を行う。「Secure CoPy」の略。

scp -r コピー元 コピー先
 ディレクトリごとコピーする。「recursive(再帰的)」の略。(再登場)

サーバでよく使う編

du / df

du

ファイルのディスク使用量を推定する。「Disk Usage」の略。

df

ファイルシステムのディスク容量の使用状況を表示する。「Disk Free」の略。

-hオプション
 単位を見やすい形にして表示してくれる。「human-readable」の略。

ps

ps

現在動作しているプロセスを表示する。「Process Status」の略。

ps aux
 よく使うauxオプションは、aとuとxの合わせ技。
 a 端末を持つ全てのプロセスを表示
 u ユーザー名を表示
 x 端末を持たない全てのプロセスを表示

free

free

macにはないLinuxコマンド。現在のメモリ使用量を確認。Swapの使用状況などを確認することができる。

free -m
 メガバイト単位で表示。

top

top

現在のシステム全体の負荷情報を表示する。
LinuxだとCPU使用率順で出るが、macだと表示の仕方が異なる。

vmstat

vmstat

macにはないLinuxコマンド。メモリやCPUの情報だけでなく、スワップやディスクI/Oの情報を表示する。 「Virtual Memory STATistics」の略。

vmstat 1
 1秒ごとに更新して表示。

ちょっとしたお助けコマンド編

man

man コマンド名

マニュアルを表示する。「MANual」の略。
ここまで色々と書いてきましたが、manコマンドで叩けば全て乗っている。 コマンドの使い方やオプションについて調べたい時に便利。

which

which コマンド名

コマンドを探し出し、フルパスで表示する。
使いたいコマンドがマシンに入っているか調べたい時に便利。

ちょい足しプラスα編

タブ補完

とにかく便利なタブ補完。
ファイル名やディレクトリ名を途中まで打って、キーボードの「Tab」キーを押すと、該当する名前を自動で入力補完してくれる。超絶便利。 同じようなファイル名のものが複数あった場合は、名称が同じ部分まで補完。

ログアウト

ctrl + d

サーバに入っている時などにlogoutexitと叩くとサーバから出ることができるが、このショートカットキーを叩くと同様の操作が行える。

強制終了

ctrl + c

なにかプログラムやコマンドを実行している時に、強制的に終了してくれるのがこのショートカットキー。

クリア

ctrl + l

clearコマンドという画面をクリアしてくれるコマンドがあるが、このショートカットキーでもクリアしてくれる。何かと画面に色々出力していて、見にくくなってしまっても一発で綺麗に。

過去検索

ctrl + r

過去に叩いたコマンドを探したくなることがよくある。
historyというコマンドで履歴を表示することもできるが、もっとお便利なのがこのコマンド。
ctrl + rを叩くと表示が「(reverse-i-search)...」と変わる。
ここで文字を入力していくと、入力された文字を含む過去に叩いたコマンドが右側に出てくる。
表示されるものが欲しいものと違ったら、再びctrl + rを叩けばどんどん遡ってくれる。

最後に

以上で今回のご紹介はおしまいです。
コマンドにはたくさんのオプションがあって、色々な組み合わせ方ができるとても便利なものです。
少しずつ慣れていくことで、少しずつターミナルと仲良くなっていきましょう。
他にもたくさんのまとめブログや記事があると思いますので、色々と見てみることをおすすめします。
このブログを読んでいただいて、
「このコマンドはこんな意味だったのか〜」「こんなお便利なものがあったのね」
となり、ターミナルと仲良くなる入り口になれたなら幸いです。

アスタミューゼでは、エンジニア・デザイナーを募集中です。
ご興味のある方は下記バナーからチェックして頂き、ぜひぜひご応募ください!お待ちしています。
ではまた。

Python が Cloud Functionsで使えるようになったので試してみました

f:id:astamuse:20181030211402j:plain

こんにちは、アスタミューゼでデザイナーをしている@YojiShirakiです。最近、細かい自動化したい処理が増えてきて困っておりまして。そんな折に Cloud Functions で Python が使えるようになったと聞いたので早速触ってみようかと。

そこそこちゃんと書いたので、読む時間が無い方はブックマークすることをおススメします。


目次

Cloud Functions とは

昨今のサーバーレスアーキテクチャ思想に呼応する関数をクラウド上で実行できるサービスです。有名どころでは AWS の Lambda、 Microsoft の Azure Functions があります。GCP にも Cloud Functions というサービスがありこれまでは JavaScript のみに対応していました。最近、これの Python 対応がベータリリースされたので練習がてら一つ作ってみます。

今回の練習アプリ

今回は Search Console のデータを Cloud Functions 上で取得して BigQuery に入れる処理を作ります。Search Console は非常に便利ですが、データ保持期間が16カ月であるため年間比較する際にやや不便です。そこで今回、Search Console のデータを引っこ抜いて保存してしまいます。

構成は下図。途中、JSON 鍵の管理などで Cloud KMS、Cron 処理のために GAE + Cloud Pub/Sub が登場するてんこ盛りの内容ですがどうぞお付き合いください。

f:id:astamuse:20181030184822j:plain

前提

今回のコードについては GCP プロジェクトの選択・作成やAPIの有効化は事前に終わっているものとします。また、今回の機能はすべてベータ版なので仕様変更により動作しなくなることもあります。悪しからずご了承ください。また Cloud のサービスの性質上、様々なタスクを走らせた結果、一定の料金が発生します。今回の構成は無茶なことをしなければ大きな金額は発生しませんが、プログラムの実行は自己責任でお願いします。

JSON キー の発行と管理

まずは今回のプロジェクト用に API キー(JSON) を発行します。Cloud Console のAPIとサービスから認証情報を生成しましょう。鍵のタイプはサービスアカウントキーにします。役割は後で編集しますが、一度「BigQueryのデータ編集者」で良いです。取得した JSON は一度ローカルに保存しておいてください。

f:id:astamuse:20181030184851j:plain サービスアカウントキーを選択。

f:id:astamuse:20181030184901j:plain 権限は BigQuery のデータ編集者で(たぶんジョブユーザーでも良い)

権限の追加

鍵を発行したら権限の追加設定を行います。IAMから、今発行した鍵ユーザーの編集画面を開き、役割に「Pub/Subパブリッシャー」を追加してください。これで Pub/Sub に対してメッセージ投げられます。

f:id:astamuse:20181030184940j:plain

Search Console へのアカウント追加

ついでに、データを取得したい Search Console に今回の JSON キー のアカウントを追加しておきます。これによってOAuth 認証を経ずとも Search Console のデータを取得できます。取得した JSON キー の client_mail の値(メールアドレス形式)を Search Console に追加しましょう。権限はフルで良いかと思います。

f:id:astamuse:20181030185154j:plain JSON キー の client_mail の値を確認します。

f:id:astamuse:20181030185223j:plain 先ほどのアドレスを Search Console のユーザーに追加します。これによって OAuth 認証を回避できます。

なお、Search Console の権限ドキュメントはこちらです。

Search Console の権限資料

KMS を利用して JSON 鍵 を暗号化する

さて次は、取得した JSON 鍵 を暗号化します。JSON 鍵 をそのまま git 管理するのは憚られますし、況やコードに埋め込むのも問題です。そこで今回は Cloud KMS を使って暗号化された状態で保持し、必要に応じて復号する流れにします。

KMSの暗号化・復号化については公式の説明がわかりやすいので、一度ご覧ください。

暗号化されたリソースの使用

JSON 鍵 を暗号化するための暗号鍵を作成

まず暗号化の前に、暗号化のための暗号鍵を準備します。この概念についても公式(オブジェクト階層 )に詳しい説明があるのでそちらをご参照ください。

鍵の作成は以下のコマンドでできます。まずkeyring(鍵束)を作成し、その後、key(鍵)を作成します。

# KEY RING の作成(KEYRING-NAME は任意に決めてください)
gcloud kms keyrings create [KEYRING-NAME] --location=global

# 暗号鍵の作成(KEY-NAME は任意に決めてください)
gcloud kms keys create [KEY-NAME] \
  --location=global \
  --keyring=[KEYRING-NAME] \
  --purpose=encryption

これで暗号化に必要な鍵ができました。鍵は KMS 内にあり、私たちは直接触ることはありません。

暗号鍵を用いて JSON 鍵 を暗号化

先ほど作成した暗号鍵(@Cloud KMS)を用いて JSON 鍵 を暗号化します。

gcloud kms encrypt \
  --plaintext-file=secrets.json \
  --ciphertext-file=secrets.json.enc \
  --location=global \
  --keyring=[KEYRING-NAME] \
  --key=[KEY-NAME]

secret.jsonというのが先ほど取得した JSON 鍵 だとお考え下さい。これを KMS の暗号鍵を使って暗号化するという寸法です。そして、このコマンドで作成されたsecret.json.encというバイナリファイルが暗号化された JSON 鍵です。基本はこれをgit管理し必要に応じて復号して利用します。

バイナリファイルを文字に変換する

さて、出来上がったsecret.json.encについてはこのまま利用してもよいのですが、Cloud Function の性質上バイナリでやり取りするのが面倒なので文字列変換しておきます。ターミナルから Python でデコードします。

$ python
> import io
> import base64
> with io.open('secret.json.enc', 'rb') as f:
>     base64.b64encode(f.read()).decode('ascii')

結果は

CiQA6mnLKL032sEfR3rnCEocT8dbZxHAS03RsiAN6uxVHSA......3a5ckdgNQ==

といったようなよく見る文字列が返ってきます。これを今後使っていきます。

Cloud Functions のコード

鍵周りの下準備が整ったので次に Cloud Functions に移ります。

構成

Cloud Functions 側を作るためにまずは3つのファイルを用意します。

project_dir(任意にお作りください)
├ main.py
├ requirements.txt
└ env.yaml

main.py は実際にコードを書くファイルです。requirements.txt にはインストール必要なモジュールを記載します。env.yaml には実行時に与えたい環境変数を定義します。後者2ファイルは以下のようなものです。

google-api-python-client
google-cloud-bigquery

requirements.txt

CIPHERTEXT : CiQA6mnLKL032sEfR3rnCEocT8dbZxHAS03RsiAN6uxVHSA......3a5ckdgNQ==
PROJECT_ID : [YOUR_PROJECT_ID]
LOCATION : [KMS_LOCATION]
KEY_RING : [KMS_KEYRING_NAME}
KEY_NAME : [KMS_KEY_NAME]
DATASET : [BIGQUERY_DATASET_NAME]
TABLE : [BIGQUERY_TABLE_NAME]
SITE_URL : [YOUR_SITE_URL] # 取得したサイトのURL。プロトコルから記載する(例:https://astamuse.co.jp/

env.yaml

CIPHERTEXTには、先ほど KMS で 暗号済の JSON 鍵 を入れます。 Cloud Functions は外部ファイルを持てないのでこういった形にせざるを得ません。

その他PROJECT_IDなどはご自身のプロジェクトのIDなど適宜記入してください。

main.py

main.py はやや長いですが、以下のようなコードです。

def import_gsc(event, context):
    import os
    import json
    import datetime
    import base64
    import logging
    import tempfile

    from google.oauth2 import service_account
    from googleapiclient.discovery import build
    from google.cloud import bigquery

    ## 固定変数
    PROJECT_ID = os.environ['PROJECT_ID']
    LOCATION   = os.environ['LOCATION']
    KEY_RING   = os.environ['KEY_RING']
    KEY_NAME   = os.environ['KEY_NAME']
    DATASET    = os.environ['DATASET']
    TABLE      = os.environ['TABLE']
    SITE_URL   = os.environ['SITE_URL']

    date_format = "%Y-%m-%d"

    ## 日付のジェネレータ(後で使います)
    def date_range(start_date, end_date, delta=datetime.timedelta(days=1)):
        current_date = start_date
        while current_date <= end_date:
            yield current_date
            current_date += delta

    ## 引数取得(実際は引数チェックなど入れます)
    messages   = json.loads(base64.b64decode(event['data']).decode('ascii'))
    start_date = datetime.datetime.strptime(messages['start_date'], date_format)
    end_date   = datetime.datetime.strptime(messages['end_date'], date_format)


    ## KMSからJSONキーの復元
    logging.info("Fetch encrypted key form kms and decrypt it.")
    kms_client   = build('cloudkms', 'v1')
    key_path     = 'projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}'.format(
                        PROJECT_ID, LOCATION, KEY_RING, KEY_NAME)
    crypto_keys  = kms_client.projects().locations().keyRings().cryptoKeys()
    kms_request  = crypto_keys.decrypt(
                          name = key_path
                        , body = {'ciphertext': os.environ['CIPHERTEXT']})
    kms_response = kms_request.execute()
    json_key     = json.loads(base64.b64decode(kms_response['plaintext'].encode('ascii')))


    ## GSCからデータ取得, GBQへインサート
    logging.info("Fetch data from gsc.")
    scope        = ['https://www.googleapis.com/auth/webmasters.readonly']
    credentials  = service_account.Credentials.from_service_account_info(json_key, scopes=scope)
    gsc_client   = build('webmasters', 'v3', credentials=credentials)

    for target_date in date_range(start_date, end_date): # 先ほどのジェネレーターを利用して日付ごとにデータ取得
        target_date_str = target_date.strftime("%Y-%m-%d")
        gsc_request = {
                      'startDate'  : target_date_str
                    , 'endDate'    : target_date_str
                    , 'dimensions' : ["page","query","device"]
                    , 'rowLimit'   : 1000
                }

        gsc_response = gsc_client.searchanalytics().query(
                                siteUrl   = SITE_URL
                                , body    = gsc_request).execute()
        print("Fetched data of {}".format(target_date_str))

        ## GBQへインサート
        output_rows = []

        if 'rows' in gsc_response:
            for row in gsc_response['rows']:
                output_rows.append([
                    row['keys'][0]
                    , row['keys'][1]
                    , row['keys'][2]
                    , row['clicks']
                    , row['impressions']
                    , row['ctr']
                    , row['position']
                    , target_date_str
                    , SITE_URL
                ])

            if len(output_rows) > 0:
                # JSON KEY を一時的にファイルで利用したいのでtempfileで書き出します
                with tempfile.NamedTemporaryFile(mode='w+t', encoding='utf-8') as tf:
                    json.dump(json_key, tf)
                    tf.seek(0)

                    bq_client   = bigquery.Client.from_service_account_json(tf.name, project=PROJECT_ID)
                    dataset_ref = bq_client.dataset(DATASET)
                    table_ref   = dataset_ref.table(TABLE)
                    table_obj   = bq_client.get_table(table_ref)
                    error       = bq_client.insert_rows(table_obj, output_rows) 

                    # TODO : error 内容に応じた処理

この関数は、想定として Pub/Sub から送られてくるメッセージの中にstart_dateend_date`%Y-%m-%dのフォーマットで渡ってくる仕様にしています。その日付に基づいてrange_dateで日付をジェネレートして一日ごとに Search Console からデータ取得し BigQuery にインサートするという流れです。

関数の引数となっているeventcontextは Pub/Sub から送られてくるデータを受ける引数です。この辺りは公式仕様を確認しておくとよいです。

引数チェックや、同一レコードチェック、例外処理などは割愛しているので適宜追加してください。

BigQuery のテーブルスキーマ

BigQuery のテーブルスキーマーも掲載しておきます。先ほどの関数をデプロイする前にテーブルは作っておきましょう。

[
      {
        "name": "page",
        "type": "STRING"
      },
      {
        "name": "words",
        "type": "STRING"
      },
      {
        "name" : "device",
        "type" : "STRING"
      },
      {
        "name": "clicks",
        "type": "FLOAT"
      },
      {
        "name": "impressions",
        "type": "FLOAT"
      },
      {
        "name": "ctr",
        "type": "FLOAT"
      },
      {
        "name": "position",
        "type": "FLOAT"
      },
      {
        "name": "data_date",
        "type": "DATE"
      },
      {
        "name": "site_url",
        "type": "STRING"
      }
]

このスキーマ―については、Search Console から取得するデータのディメンジョンによってはカラムを追加する必要があるかもしれません(countryとか?・・使わないと思うけど)。

function のデプロイ

さて一通り準備できたので関数をデプロイします。デプロイはコマンドから実行します。今回は Pub/Sub からトリガーを引くことができるように下記のコマンドでデプロイします。

gcloud beta functions deploy [YOUR_FUNCTION_NAME] --trigger-resource [YOUR_TOPIC_NAME] --trigger-event google.pubsub.topic.publish --runtime=python37 --env-vars-file env.yaml 

上記コマンドを実行すると、自動的に、実行ディレクトリのmain.py内にある対象関数をデプロイするようになっています。ですので YOUR_FUNCTION_NAMEmain.pyに記述しているデプロイしたい関数名を入れてください(今回の場合はimport_gsc)。YOUR_TOPIC_NAMEは今回利用する Pub/Sub 内のトピック名を指定します。おそらく既存のトピックを使うことにはならないと思うので、このタイミングで任意に決めて問題ありません(例:import_gsc_topic など)。デプロイすると自動で Cloud Pub/Sub にトピックが作成されています(下記のリンク先で確認できます)。

Cloud Pub/Sub トピック一覧

デプロイした関数の実行

先ほどデプロイした関数を実行します。gcloudコマンドには Pub/Sub 経由でトリガーを引くコマンドがあるのでデバッグとしてこちらを利用します。

gcloud beta pubsub topics publish [YOUR_TOPIC_NAME] --message '{"start_date" : "2018-10-01", "end_date" : "2018-10-02"}'

実行ログの確認

実行ログについては Stackdriver から確認できます。利用している module の関係で WARNING が出ていますが気にしないでください。

GAE と Cloud Pub/Sub でスケジューリングする

Cloud Functions で Search Console のデータを BigQuery に格納できるようになったので、これを定期実行します。が、Cloud Function には定期実行する仕組みありません。そこで Google App Engine の Cron 機能を利用します。Pub/Sub にメッセージを送るだけなのでそこまで複雑ではありませんが、面倒だと思う場合は Cloud Function を HTTP トリガーできるようにデプロイして、その HTTPエンドポイントを 外部サービスから叩くという手もありだと思います(誰でも叩けてしまいますが)。

構成がやや複雑に見えますが、Google 中の人も同じ方法を謳ってるので仕方ありません。

Cloud Functions for Firebase でジョブをスケジューリング(cron)する

構成

GAE 側は4つのファイルを用意します。

project_dir(任意)
├ app.yaml
├ cron.yaml
├ main.py
└ requirements.txt

app.yaml

これは GAE に乗せるアプリケーションの設定と環境変数を記載しておきます。KMS の鍵情報や Pub/Sub のトピック情報。また JSON 鍵 の暗号化文字列を持たせています。

runtime: python37
env_variables:
  PROJECT_ID: [YOUR_PROJECT_ID]
  LOCATION: [KMS_LOCATION]
  KEY_RING: [KMS_KEY_RING]
  KEY_NAME: [KMS_KEY_NAME]
  CIPHERTEXT: XXXXXXXXXXXX(JSON 鍵の暗号化済文字列)XXXXXXXXXXXXXXXXXXX...
  PUBSUB_TOPIC: [YOUR_TOPIC_NAME]

cron.yaml

次に cron の設定を記載します。ここで定期的に GAE 上のURLを叩く仕組みにします。

cron:
- description: "daily summary job"
  url: /import_gsc
  schedule: every monday 09:00
  timezone: Asia/Tokyo

timezone 設定は忘れずにね。

requirements.txt

requirement.txt は Cloud Function と同様に必要な外部モジュールを記載します。今回はアプリケーションプレームワークに Bottle を利用しました。Flask でも問題ありません。お好みでどうぞ。

bottle
google-cloud-pubsub
google-api-python-client

main.py

main.py では実行するトリガーのためのURLの処理を記述します。

from google.oauth2 import service_account
from googleapiclient.discovery import build
from google.cloud import pubsub
from bottle import Bottle, debug, route, run, default_app, request
from pprint import pprint
import datetime
import os
import json
import base64
import logging

logging.getLogger().setLevel( logging.DEBUG )

@route('/')
def index():
    return "Hi, Georgie! Aren't you gonna say hello?"


@route('/import_gsc')
def import_gsc():

    ## アクセス制御 
    if request.headers.get('X-Appengine-Cron') != 'true':
        logging.error('Invalid access.')
        return 'Error'
    
    ## KMSからキーの復元
    logging.info("Fetch encrypted key form kms. And decrypt.")
    kms_client   = build('cloudkms', 'v1')
    key_path     = 'projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}'.format(
                          os.environ['PROJECT_ID']
                        , os.environ['LOCATION']
                        , os.environ['KEY_RING']
                        , os.environ['KEY_NAME']
                    )
    crypto_keys  = kms_client.projects().locations().keyRings().cryptoKeys()
    kms_request  = crypto_keys.decrypt(
                        name = key_path,
                        body = {'ciphertext': os.environ['CIPHERTEXT']})
    response     = kms_request.execute()
    json_key     = json.loads(base64.b64decode(response['plaintext'].encode('ascii')))

    ## PUB/SUBにメッセージを投げる
    logging.info("Send message to pub/sub.")
    
    credentials = service_account.Credentials.from_service_account_info(json_key)
    pub_client  = pubsub.PublisherClient(credentials=credentials)
    topic_path  = 'projects/{}/topics/{}'.format(os.environ['PROJECT_ID'], os.environ['PUBSUB_TOPIC'])

    target_date = datetime.datetime.today() - datetime.timedelta(2)
    target_date = target_date.strftime("%Y-%m-%d")

    logging.info("Target date is {}".format(target_date))
    message     = {"start_date" : target_date, "end_date" : target_date }
    message     = json.dumps(message)
    
    response    = pub_client.publish(topic_path, message.encode('utf-8'))
    pprint(response)

    return 'Success'

app = default_app()

流れとしては、まずアクセス制御。ヘッダーにX-Appengine-Cron:Trueが無いものは弾きます。このヘッダーは GAE の Cron アクセスに付与されるものです。外部からこのヘッダーを持ったアクセスが来た場合が Google 側がそのヘッダーを削除する仕組みになっています(参考:cron.yaml リファレンス )。また Google App Engine の Cron は IP アドレス 0.1.0.1 からのアクセスするらしいのでそれで制御しても良さそうです(未検証)。

アクセス制御が終わったら KMS でJSON 鍵を復号します。その鍵を利用して Pub/Sub にメッセージを送り処理は終了します。データの取得対象日は2日前を指定します。前日だとAPI経由でデータが取得できないことがあるためです。

GAE にデプロイ

コードができたら GAE にデプロイします。cron.yamlもデプロイします。

gcloud app deploy app.yaml cron.yaml

cron を強制実行してテスト

デプロイが完了したら cron を強制実行してテストします。先ほどのデプロイで GAE の Cron Job にジョブが登録されているので、そこから強制実行します。

Google App Engin Cron Job

実行した際のログについては Stackdriver から確認できます。利用している module の関係で WARNING が出ていますが気にしないでください(二回目)。

BigQuery のデータを確認

特にエラーがなければ BigQuery にデータがインサートされていると思います。確認してみましょう。

Big Query (コマンドで確認してもいいです)

特に問題なければ、Pub/Sub 経由で過去分のデータを取得するのもいいかもしれません。先に載せたコマンドを再掲しますので適宜ご利用ください。

gcloud beta pubsub topics publish [YOUR_TOPIC_NAME] --message '{"start_date" : "2018-10-01", "end_date" : "2018-10-02"}'

Data Studio で表示してみる

さて最後に余興として BigQuery のデータを Data Studio で取得してダッシュボードにしてみます。ダッシュボードはいろいろ載せすぎると却ってよく分からなくなるので2,3グラフを載せるのに留めます。 下記のリンクから Data Studio を開いてください。

Google Data Studio

開いたら新しいレポートの開始をします。開いたら右側のデータソースの追加のところから、一番下にある「新しいデータソースを追加」をクリックします。遷移先で BigQuery を選択しウィザードを進んでください。途中、BigQuery のデータセットを選ぶ場面があると思うので、そこで今回作成したデータセットを選択すれば後は自由にデータを抽出できます。

私は自分のダッシュボードとして以下のようなものを作りました。

f:id:astamuse:20181030210745j:plain

全体の検索データを俯瞰しつつ、特に注力したいキーワードを含む検索クエリの数値をグラフ化してます。もう少し本気で作りこめばまぁまぁ使えそうです。 なお、当然ながら BigQuery のデータの取得には料金が発生しますので、誤ってクラウド破産などしないように十分にお気を付けください(自己責任ですお願いします)。

BigQueryで150万円溶かした人の顔

まとめ

ちょっと興味がのってしまいややゴツイ構成になってしまいましたが、サーバーレスアーキテクチャというのは往々にしてこうなるものなのかと思います。ネット上でそこまで情報がなく、手探りだったこともあり土日を潰しましたが非常にいい勉強になりました。もし本稿について何かご質問があれば@YojiShirakiにDMお願いします。

では、本日も最後までお読みいただきありがとうございました。

………あ、そうだ。

例によって当社では一緒にサービス開発してくれるエンジニア・デザイナー・ディレクターを超絶賛募集しております。

カジュアル面談も随時行っておりますので、「ちょっと話聞きたい」という方は、このブログのサイドバー下にあるアドレスか@YojiShirakiにDMいただければと思います。いつでもウェルカムです。採用サイトもありますので下の水色のバナーから是非どうぞ!ではまた:- )

@YojiShirakiの過去記事)

参考

google cloud functions

Cloud KMS

Cloud Pub/Sub

GAE Cron Job

Data Studio

技術的負債と戦う指標の取り方と改善の仕方、そして諦め方

どうも、えいやです。

今回もお鉢が、というかぬいぐるみが回ってきたので、ブログを書きます。

今回は、技術的負債の計測と返済についてです。こんな方向で適当にやってるというだけの話とポエムなので、真面目に話を聞きたい人は、品質管理の専門家に相談してください。

なお、今回もJavaで開発を行っている想定での話です。あと、最後はやっぱり暗い気持ちで締めることになるので、誰か手伝いに来てください。

SonarQubeの紹介

僕がメインでメンテナンスしているサービスでは、CIに組み込んだSonarQubeを用いて静的解析ツールや動的テスト結果の解析ツールを実行し、CodeSmellなどのメトリクスを集計、可視化しています。

技術的負債とは、それらのメトリクスの個々の理由うちコード修正が必要なものについて、修正に要する日数を算定し、コストとして積み上げたものです。

www.sonarqube.org

SonarQubeについての詳しい内容は、公式のサイトを見てください。多様なプログラミング言語について指標を出せますが、解析ツールが洗練されているJavaについての指標がもっともきちんと提示できるようです。

Astamuse.comの場合

弊社の主要なサービスでは、Astamuse.com、AstaID.comについて取得しています。また、それらを構成するライブラリについても、自製のものについては取得しています。

例えば、コードベースが大きいAstamuse.comでは、Java言語で実装されている部分について、現在のメトリクスは以下の数値となっています。

         Lines of Code   Bugs    Vulnerabilities Code Smells Coverage    Duplications
main-java   42k             49      20              1.4k        30.4%       4.9%

これらの指標に基いて算出された技術的負債は62日となっています。これが多いのか少ないのかは、プロジェクトによって異なると思いますので言及しません。

ここ2年の技術的負債の推移を見てみると以下のようになります。

f:id:astamuse:20171023180246p:plain
技術的負債推移グラフ

新機能の追加で負債が増加することもあり、二年前の時点の167日を完済とは行きませんが、メインコードについては負債は減少傾向にあります。

借金で例えると「新規の借り入れ」はありつつも「元本」を返せている感じがしますね。まぁ頑張っている方だと思います。

数値の改善に取り組む

さて、この返済をどう行ってきたのかについてです。

もちろん、基本的には、個別の指摘事項を潰していくことで返済していますが、指摘されたことを機械的にやっていたのでは埒があきません。

以下では一気に改善してみせる方法を取り上げます。といっても、どれも当たり前な改善方法ですが。

抽象設計を行う

まず、負債が大きいコードでは設計がなっていない事が多いです。

コードをみて、オブジェクト指向の基礎にしたがった設計をしっかりと行います。

また、既存のコードが増える度に、抽象化して共通化すべき概念は増えていくはずです。エリック・エヴァンスが好きならそれらの一部はドメインと言い換えても良いでしょう。

手続きとしての共通化ではなく、抽象度の高い設計を行い、コードを共通化しましょう。

抽象設計が正しく行えると、コードの重複が少なくなりますので、わりと劇的に指標が改善します。

とくに、Java8以降では、ラムダ式やdefaultメソッドが使えるので、Java7以前から存在しているコードについては見直せば大きな効果が期待できるでしょう。

テストで使用されているMockの見直し

単体テストが設計時のままの場合、テストの対象となっているクラスと連携する他のクラスでモックを使用しているケースが多いかもしれません。

それらがインスタンス化が出来るなら、インスタンス化してしまいましょう。無理ならスパイを使っても良いでしょう。

単純にカバレッジが改善することもあれば、実際にはどうやったって通ることのない不要なコードが見つかって、それを削除できたりします。

例えば、Nullになり得ないシーンでのNullチェックや、@NonNullアノテーションをつけられるシンボルの発見などです。

ライブラリへの切り出しでよく見せる

指標を改善するだけなら最もよく見せる方法がこれです。

じつは、グラフ中に現れる大きな減少は、メインから別のライブラリへコードを切り出した結果としてメインコードから負債が消えたという部分があります。

むろん、その場合では、ライブラリの負債も合計すれば負債総額は変わらないです。いわば(メインからは)視えない化ですね。

それでも、メインコードの負債が減るのは短期的には良い傾向だと捉えられます。

なぜなら、一般にライブラリへのコードの追加・更新はメインコードへの追加更新よりも頻度が低いからです。このことをまた借金で例えるなら、返済期限が長く、新規の借り入れをあまり必要としない返済方法への乗り換えと捉える事ができます。

また、ライブラリ化によってコードの独立性が高まることで、そのライブラリに関する業務を分離することが出来ます。メンバーが増えたときにその業務を担当させやすくなるでしょう。借金の肩代わりをさせるようなものといえます。

なお、ライブラリ化できる構造に変更すること自体が、設計を正しくし、指標の改善となる傾向があります。

もちろん、機能をライブラリとして切り出すべきかどうか、きちんと考えた上で行う必要がありますので、なんでも分割すればいいというわけではないことには注意しましょう。メイン部分と独立している、改修の頻度が高くない機能が切り出す対象として向いています。

そして、切り出した後のライブラリを知らんぷりしていると、多重債務者状態になって立ち行かなくなることも心に留めておきましょう。

視えない指標にも対抗する。。。けど心構えは必要

上記までは、基本的に視えている技術的負債に対抗する方法です。

技術的に負債には、見えない部分があって、それらについても対抗しておかないと困ったことなります。

陳腐化に対抗する。。。のにも限界はある

さて、視えない部分で大きな部分を占めるのが陳腐化による内在的な負債です。

陳腐化とは、時間経過により使用する基盤技術やライブラリが古くなってしまい、刷新を行わなければならなくなることを指します。もちろんコストがかかります。

いつライブラリや環境が新バージョンをリリースするか、旧バージョンのメンテナンス期限が切れるかなどは、その全てを自動で把握することが難しいため、SonarQubeでも指標として取りにくいケースが多いです。

セキュリティ上の問題が発覚したりなどは、ある日突然に起こる場合もあります。

使用している技術が突如として使用不能になったり、採用した技術の新しいバージョンへの移行が極端に難しいことが判明してしまう様子は、借金に例えるなら、貸し剥がしや貸し渋りといったところでしょうか。

可能な限り最新のライブラリ環境に寄せ続けることでリスクを減らすことは出来ますが、なかなかに難しいことが多いでしょう。

対抗策として考えられるのは、設計段階で腐敗防止層を設けたり、ファサードのような実装が可換なパターンを用いていたりなどですが、それらはライブラリの変更には耐えられても、言語のアップグレードなど基盤技術の変更には耐えられません。

どんなシステムでも、陳腐化の原因については対策は後手に回る以外にないため、「陳腐化速度>メンテナンス速度」が宿命づけられており、いずれ陳腐化による借金が返済不能になると思っています。

何処かでしっかりと諦めて、システムの一からの作り直しをするか、もう止めてしまう覚悟が必要だと思います。

ちなみに、今のAstamuse.comはJava製ですが、かつて黎明期にあったScalaを用いて、同じく初期頃のバージョンのLiftで組まれていた時期があります。乗り換えの理由は、総合的な理由であり、陳腐化のみではなかったかと思います。昔のScalaは、今よりも問題が多かったことは確かです。

前提条件の変更もある。。。流れに乗らざるを得ない

陳腐化と似ているのですが、時代の変化により前提の条件が変更されるということもあります。時間というより、もっと大局的な時代でおきる、いわゆる時代の要請ですね。

条件の変更の例を言えば、CPUの進化や、メモリ容量の向上、クラウドの普及、スマートフォンユーザの増加、HTTPSデフォルトの一般化、などです。

それらを理由に、システムの根本的な作りや、データの取り扱い方、必須とされる機能の要件など、様々なところで変更が強いられます。

つまり、かつての時代の常識が、今の時代の非常識になったため、対応を取らなければならないことですが、Webサービスで言えば、FlashからHTML5への刷新などですね。

こうした内容は、陳腐化と同様にSonarQubeでは指標化出来ないこともありますので、他の方法で気にかけられるようにしておくべきでしょう。たとえばモバイルフレンドリー調査ツールやアクセス解析など、要件ごとの解析ツールなどです。

Astamuse.comでも、こうしたツールを用いて、細かな修正で対応可能な範囲においては、時代の要請にあった改修を続けています。

もちろん、小さな変更では済まないことも多くあります。例として出したFlashの廃止も、手法によってはそのうちの一つとなるでしょう。

Astamuse.comにおいてそのような部分の一つに、コンポーネント化への対応というのがあります。

Astamuse.comの基本的機能は、かつてGoogleの検索ボットが動的ページのインデックスをうまく行えなかった時代に作られています。それゆえ、Astamuse.comは、データを元に静的なHTMLを生成できるCMSのような実装が得意なFWを用いて、サーバサイドでHTMLをレンダリングするシステムとして組まれています。

ですが、ご存知の通り、今ではGoogleのBotは賢く、フロントエンドで描画されるインタラクティブなページも難なく解析し、検索インデックスに加えることが出来ます。

一般に、サーバサイドレンダリングは、サーバサイドへの負荷が高く、クライアントでも重複するHTMLデータを何度もダウンロードをしなければならないため、非効率とされています。

なので、いつとは言えませんがAstamuse.comもそのようなサーバサイドレンダリングからフロントエンドレンダリングに実装を切り替えていくことになるでしょう。

こうした改修は、コストが大きいので、既存のシステムを「諦める」可能性についても考える必要があります。

今のAstamuse.comのコードベース全てを諦めるということはないと思いますが、ミドルウェアやフレームワークの選択を含め今後考えていくことになるでしょう。割りと好き勝手に。

まとめ

  • 指標を見えるようにしよう。
  • 指標を改善する設計を行おう。
  • 一度書いたテストも放っておかずにメンテしよう。
  • メインコードから切り出すべき機能を探してライブラリにしておこう。
  • 見えない部分にも気を配ろう。
  • 諦めるときは諦めよう。
  • 諦めたら割りと好き勝手出来ると思う。

P.S で、Java9もう出てるよね

先日、待望のJava9が発表され、モジュール化機能の検証をせざるを得ないなぁと思っているところにその後のJavaのリリース計画も半年ごとに云々とか、、、

新しいことが出来るようになっていくのはとてもいいことなんだけど、抱え込んでるものの大きさにもよるよなぁ。

全くもって、人手が足りません。

Copyright © astamuse company, ltd. all rights reserved.