kawabatas技術ブログ

試してみたことを書いていきます

外部キー制約の親子テーブルにおいて共有ロックからのdeadlock

deadlock のログを発見し、調べました。

結論はタイトルの通りです。

とりあえず、サンプルのSQLで試します。

CREATE TABLE players (
  id int primary key,
  name varchar(20)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE player_items (
  id int primary key,
  player_id int,
  item_id int,
  CONSTRAINT fk_player_id FOREIGN KEY (player_id) REFERENCES players (id)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO players (id, name) VALUES (1, "player_1");

f:id:kawabatas:20200621155640p:plain

ターミナル1

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO player_items (id, player_id, item_id) VALUES (1, 1, 1);
Query OK, 1 row affected (0.00 sec)

mysql> UPDATE players set name = "player_one" where id = 1;
Query OK, 0 rows affected (6.29 sec)
Rows matched: 1  Changed: 0  Warnings: 0

ターミナル2

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO player_items (id, player_id, item_id) VALUES (10, 1, 2);
Query OK, 1 row affected (0.00 sec)

mysql> UPDATE players set name = "player_ichi" where id = 1;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
mysql>

deadlock の発生していたコードをみたとき、UPDATE 文でロック待ちをしても、deadlock が起こるとは思いませんでした。

SHOW ENGINE INNODB STATUS;を確認すると、

共有ロックかかっているレコードをUPDATEしようとしてデッドロックになっているようでしたが、コード上は明示的に共有ロックかけているようなところは見当たらず、なぜ共有ロックがかかっているのか不明でした。。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 14.6.6 InnoDB と FOREIGN KEY 制約

外部キーチェックの実行時に、InnoDB は、調査対象の子または親のレコード上に共有の行レベルロックを設定します。

どうやら MySQL の外部キー制約の仕様で、外部キー制約のテーブルへの INSERT で共有ロックがかかっているようです。

今回は一時的に SET FOREIGN_KEY_CHECKS=0 で外部キー制約を一時的に外すことで対応しました。

Github Actions の deployment イベントでデプロイ

Github Deployments について

Deployments are requests to deploy a specific ref (branch, SHA, tag). GitHub dispatches a deployment event that external services can listen for and act on when new deployments are created.

ブランチやタグ、ハッシュを指定して、外部連携のデプロイを発火するもの(あくまでトリガー)。

f:id:kawabatas:20200421085338p:plain

CI で develop/master ブランチの push や tag でデプロイされるということをよくしていたが、

これは push / tag 本来の機能にデプロイのトリガー機能を追加しているわけで、デプロイのトリガー専用の機能を提供しているのが deployment っぽい。

GitHub-flow (参考)では、

PR作成

修正

デプロイ

フィードバック

マージ

らしい。

このフローだったら push / tag にデプロイのトリガーを追加するのはよくなさそうに思える。

こうして deployment イベントが生まれたのか、と納得した。

一度 deployment を作成すると environment タブが現れる

f:id:kawabatas:20200421085954p:plain

environment タブの中はこんな感じ (PR や issue のように deployment の新規作成ボタンはなかった。あったらボタンぽちぽちデプロイもできると思ったが、、)

f:id:kawabatas:20200421090017p:plain

deployment のステータスを緑色の active とするには Create a deployment status API を叩かねばならない。こちらは public beta らしい。

deployment をトリガーにしたミニマムの Actions はこんな感じ

name: DEPLOYMENT
on: deployment
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: |
          echo "Deploy"
      - name: Update deployment status
        uses: actions/github-script@0.9.0
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const jobStatus = '${{ job.status }}'
            const deployStateSuccess = 'success'
            const deployStateFailure = 'failure'
            const deployStatePending = 'pending'

            let deployState = deployStatePending 
            if (jobStatus == 'Success') {
              deployState = deployStateSuccess
            } else if  (jobStatus == 'Failure') {
              deployState = deployStateFailure
            }

            github.repos.createDeploymentStatus({
              deployment_id: context.payload.deployment.id,
              owner: context.repo.owner,
              repo: context.repo.repo,
              state: deployState
            })

トークンを準備すれば CLI ですぐに実行できる

curl -X POST \
  -H "Authorization: token トークン" \
  -d '{"ref": "master"}' \
  https://api.github.com/repos/kawabatas/deployment-test/deployments

Slack からも実行できる。 Slack ヘルプ - GitHub と Slack を連携させる

しかし、Slack からは Create a deployment API のパラメータで指定できるものは限られているようだ。

https://github.com/integrations/slack/issues/984 で auto_merge、required_contexts の機能追加の PR も発見。

(AutoMergeとは default branch が対象のブランチよりも進んでいる場合、自動で Merge される機能。デフォルトで true)

/github のコマンド一覧は多分こちら

おわり

GitHub-flow(マージ前にデプロイ)の開発なら deployment をトリガーにデプロイするのが良さそうだが、

そうでないなら、push / tag をトリガーにして良さそうかなと感じた。

現状は development の優位性は ChatOps で使いやすそうくらい?

Ruby で Google Cloud Vision API を使って OCR を試してみる

概要

先日、次のバスの出発時刻を聞ける Alexa スキルを作りました。

その際、バスの時刻表を csv にして、プログラムから使ったのですが、

今だったら Cloud Vision API でバス時刻表の画像から時刻を読み取れるのでは、と思ったので試してみました。(Alexa スキルではバス会社の HP をスクレイピングして csv を作りました)

手順

cloud.google.com

ドキュメント通りに進めます。

  1. Google Cloud Platform プロジェクトを作成

  2. 課金を有効に

  3. Cloud Vision API を有効に

  4. サービス アカウントキーを作成(役割は「編集者(roles/editor)」にしました)

ruby スクリプト

Ruby Cloud Vision API クライアントライブラリは v0.32.0 以降と、それ以前のバージョンと大きな違いがあるようで注意が必要です。クイックスタートは古いバージョン用でした。

新しい v0.32.0 以降のバージョンのドキュメントもありました。

cloud.google.com

今回使ったコードはこちら

require "bundler/setup"
require "google/cloud/vision"

image_annotator = Google::Cloud::Vision::ImageAnnotator.new(version: :v1p3beta1)

file_name = "./images/bus-stop.png"

response = image_annotator.document_text_detection image: file_name
response.responses.each do |res|
  res.text_annotations.each do |text|
    puts "------------------------"
    puts text.description
  end
end

github.com

OCR 試す

こちらの png で試します。

f:id:kawabatas:20190916224037p:plain

結果

$ bundle exec ruby main.rb
------------------------
渋谷駅
のりば:5
渋12
渋12
渋12
二子玉川駅ゆき
高津営業所ゆき
<三軒茶屋・駒沢経由>二子玉川駅・高津営業所ゆき
土曜
平日
休日
|06 51
|07| 112946
los|03203754
|09|11305485
|1008528Ⓡ485
|11082848
|12082848
13082848
[1408"28"48"
|15082848
|16082848
|17|0828485
|18|08" 28" 49
|19| 113355
|20|2045
|21| 10" 35
2205" 35
51
112947-
0626" 46" |
06"26" 46
062646
0626461
065 2646
| 06"26" 46
062646
06 2748
082848
093050 |
10"31"52"
13355751
2045
10" 35"
05" 35"
51
1740
002040-
0020"40"
00~20540
| 002040-
| 002041
0223" 43
| 03"2343
032343
| 032343
032445
| 0729"51"
133557 |
2045
| 10" 355
05"35"
-
2017年7月1日 改定|
無印・・・二子玉川駅行き
高・二子玉川経由高津営業所行
スロープ板付バスで運行致します。尚整備等により一般車両で運行する場合がありますのでご了承ください。
道路混雑等のため予定時刻どおりに運行できないことがありますので、ご了承願います
お問合せ先:東急バス 高津営業所 044-833-3241
------------------------
渋谷駅
------------------------
のりば
------------------------
(以下略)

表の枠を「1」と認識してしまったり、「112946」のように時刻のスペースがなかったり、、

今の状態では、プログラムで加工して csv にするのは厳しそうです。。

ML 勉強して、独自の学習モデルを作れるようになりたいな。

赤ちゃんのミルク等の時間を Google Spreadseet に記録する Alexa カスタムスキルを作る

概要

タイトルの通り、赤ちゃんのうんち、おしっこ、ミルクの時間を Google Spreadseet に記録する Alexa カスタムスキルを作ったという話です。

ほぼほぼこちらをマネさせていただきました。mm

miyataro.hatenablog.com

こちらも参考にさせていただきました。

qiita.com

作ったものはこちらになります。

github.com

背景

先日、息子が生まれました。

で、先天性の病気が見つかり、手術し、今入院中で、僕が日勤、妻が夜勤と交替しながら、一日中、病院で付き添い育児をしております。(育休を取ってる)

病院では、ミルク、うんち、おしっこの時間や量を測定していて、

時間は、泣き出した理由の見当に役立つので、家で育児するようになってからも記録したいな、と思いました。

そして、育児では両手がふさがることが多いので、家にある Amazon Echo Dot で音声で記録できればな、と思いました。

手順

1. 記録用の Spreadseet を用意

2. Spreadseet へ書き込む Apps Script を作成

ツール > スクリプト エディタ から作成できる。

records.gs。onFormSubmit はグーグルフォームからの記録用、registerXXX はアレクサからの記録用。

function onFormSubmit(e) {
  var startTime = Date.now();

  var keys = Object.keys(e.namedValues);
  var dateKey;
  var eventKey;
  var memoKey;

  keys.forEach(function(key) {
    if (key.indexOf('日時') > -1) {
      dateKey = key;
    } else if (key.indexOf('イベント') > -1) {
      eventKey = key;
    } else if (key.indexOf('メモ') > -1) {
      memoKey = key;
    }
  });

  var date = e.namedValues[dateKey][0] ? new Date(e.namedValues[dateKey][0]) : new Date();
  var events = e.namedValues[eventKey][0].split(/,\s*/);
  var memo = e.namedValues[memoKey][0];

  if (events.indexOf(TYPE_NAME.poo) > -1) {
    records.appendJournalRecord(date, TYPE.POO, memo);
  }
  if (events.indexOf(TYPE_NAME.pee) > -1) {
    records.appendJournalRecord(date, TYPE.PEE, memo);
  }
  if (events.indexOf(TYPE_NAME.milk) > -1) {
    records.appendJournalRecord(date, TYPE.MILK, memo);
  }
  if (events.indexOf(TYPE_NAME.health) > -1) {
    records.appendJournalRecord(date, TYPE.HEALTH, memo);
  }

  var executionTime = Date.now() - startTime;
  Logger.log('onFormSubmit took ' + executionTime + ' ms');
}

function registerPoo() {
  var startTime = Date.now();

  records.appendJournalRecord(new Date(), TYPE.POO);

  var executionTime = Date.now() - startTime;
  Logger.log('registerPoo took ' + executionTime + ' ms');
}

function registerPee() {
  var startTime = Date.now();

  records.appendJournalRecord(new Date(), TYPE.PEE);

  var executionTime = Date.now() - startTime;
  Logger.log('registerPee took ' + executionTime + ' ms');
}

function registerPooAndPee() {
  var startTime = Date.now();

  records.appendJournalRecord(new Date(), TYPE.POO);
  records.appendJournalRecord(new Date(), TYPE.PEE);

  var executionTime = Date.now() - startTime;
  Logger.log('registerPooAndPee took ' + executionTime + ' ms');
}

function registerMilk() {
  var startTime = Date.now();

  records.appendJournalRecord(new Date(), TYPE.MILK);

  var executionTime = Date.now() - startTime;
  Logger.log('registerMilk took ' + executionTime + ' ms');
}

var records = {};

records.getSheet = function () {
  if (!records.sheet) {
    records.sheet = SpreadsheetApp.getActive().getSheetByName('records');
  }
  return records.sheet;
}

records.appendJournalRecord = function (date, type, memo) {
  var startTime = Date.now();
  Logger.log('appendJournalRecord started');

  records.getSheet().appendRow(records.createJournalRecordRowContent(date, type, memo));

  var executionTime = Date.now() - startTime;
  Logger.log('appendJournalRecord took ' + executionTime + ' ms');
};

records.createJournalRecordRowContent = function (date, type, memo) {
  var row = [];
  row.push("'" + date.toLocaleDateString());
  row.push("'" + date.toLocaleTimeString().replace(/[^:0-9]/g, ''));
  row.push(TYPE_NAME[type]);
  if (memo) {
    row.push(memo);
  }
  return row;
}

const.gs

var COLUMN = {
  DATE: 'date',
  TIME: 'time',
  EVENT: 'event',
  MEMO: 'memo'
}

var TYPE = {
  POO: 'poo',
  PEE: 'pee',
  MILK: 'milk',
  HEALTH: 'health'
};

var TYPE_NAME = {
  'poo': 'うんち',
  'pee': 'おしっこ',
  'milk': 'ミルク',
  'health': '体調'
};

var TYPE_BY_NAME = {};
Object.keys(TYPE_NAME).forEach(function(key) {
  TYPE_BY_NAME[TYPE_NAME[key]] = key;
});

3. Apps Script をAPIとして公開

f:id:kawabatas:20190906155139p:plain

ここが一番てこづった。

Google Cloud Platform プロジェクトを作成し、Apps Script を関連づける。

f:id:kawabatas:20190906155653p:plain

そして、「GCP プロジェクトの Apps Script API を有効化」「OAuth 2.0 クライアントIDとシークレットの取得」「OAuth 2.0 Playground でアクセストークンとリフレッシュトークンの取得」

参考はこちら。

qiita.com

4. Alexaスキルを開発

Alexaスキルの開発は初めてだったが、公式が動画教材を作っていて、とてもわかりやすかった。

developer.amazon.com

Alexa-hostedスキルで開発すれば alexa developer console 上で開発が完結する。すごい。

developer.amazon.com

f:id:kawabatas:20190906161000p:plain

対話モデルの設定

alexa developer console メニュー上のビルド > JSONエディター から json をアップロード。

そして、モデルを保存し、モデルをビルド。

{
    "interactionModel": {
        "languageModel": {
            "invocationName": "リクログ",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": [
                        "使い方",
                        "ヘルプ"
                    ]
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": [
                        "ありがとう",
                        "終了",
                        "終わり",
                        "ストップ"
                    ]
                },
                {
                    "name": "RegisterPooIntent",
                    "slots": [],
                    "samples": [
                        "うんち"
                    ]
                },
                {
                    "name": "RegisterPeeIntent",
                    "slots": [],
                    "samples": [
                        "おしっこ"
                    ]
                },
                {
                    "name": "RegisterPooAndPeeIntent",
                    "slots": [],
                    "samples": [
                        "うんちとおしっこ",
                        "おしっことうんち"
                    ]
                },
                {
                    "name": "RegisterMilkIntent",
                    "slots": [],
                    "samples": [
                        "ミルク"
                    ]
                },
                {
                    "name": "AMAZON.NavigateHomeIntent",
                    "samples": []
                }
            ],
            "types": []
        }
    }
}

Lambda 関数の作成

alexa developer console メニュー上のコードエディタ。

package.json に googleapis を追加。

"dependencies": {
    "ask-sdk-core": "^2.6.0",
    "ask-sdk-model": "^1.18.0",
    "aws-sdk": "^2.326.0",
    "googleapis": "^25.0.0"
  }

gas-accessor.js を作成。

const google = require('googleapis');
const OAuth2 = google.auth.OAuth2;

const CLIENT_ID = process.env['CLIENT_ID'];
const CLIENT_SECRET = process.env['CLIENT_SECRET'];
const ACCESS_TOKEN = process.env['ACCESS_TOKEN'];
const REFRESH_TOKEN = process.env['REFRESH_TOKEN'];
const SCRIPT_ID = process.env['SCRIPT_ID'];
const DEV_MODE = process.env['DEV_MODE'] ? /^true$/i.test(process.env['DEV_MODE']) : false;

const gasAccessor = {};

gasAccessor.executeFunction = function (functionName, callback, opt_parameter) {
    var startTime = Date.now();

    console.log('executeFunction started [functionName=' + functionName + ', parameter=' + opt_parameter);
    const auth = new OAuth2(CLIENT_ID, CLIENT_SECRET);
    auth.setCredentials({
        access_token: ACCESS_TOKEN,
        refresh_token: REFRESH_TOKEN
    });
    const script = google.script('v1');
    script.scripts.run({
        auth: auth,
        scriptId: SCRIPT_ID,
        resource: {
            function: functionName,
            parameters: [opt_parameter],
            devMode: DEV_MODE
        }
    }, (err, result) => {
        var turnAroundTime = Date.now() - startTime;
        console.log(functionName + '  API execution took ' + turnAroundTime + ' ms');
        if (err || result.data.error) {
            console.error(JSON.stringify(err));
            console.error(JSON.stringify(result.data.error));
            throw 'API Execution Failure';
        } else {
            console.log(JSON.stringify(result.data.response));
            callback(result.data.response.result);
            var callbackExecutionTime = Date.now() - startTime - turnAroundTime;
            console.log('callback execution took ' + callbackExecutionTime + ' ms');
        }
    });
};

module.exports = gasAccessor;

index.js を編集。

// This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
// Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
// session persistence, api calls, and more.
const Alexa = require('ask-sdk-core');
const gasAccessor = require('./gas-accessor');

const LaunchRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
    },
    handle(handlerInput) {
        const speakOutput = 'こんにちは。リクログでは、うんち、おしっこ、ミルクが記録できます。何をしますか?';
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
const RegisterPooIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'RegisterPooIntent';
    },
    handle(handlerInput) {
        gasAccessor.executeFunction('registerPoo', function (result) {
            console.log(`IntentHandler executed gasAccessor.executeFunction registerPoo.`);
        }.bind(this));
        const speakOutput = 'うんちを記録しました。';
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};
const RegisterPeeIntentHandler = {
  canHandle(handlerInput) {
      return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
          && Alexa.getIntentName(handlerInput.requestEnvelope) === 'RegisterPeeIntent';
  },
  handle(handlerInput) {
      gasAccessor.executeFunction('registerPee', function (result) {
          console.log(`IntentHandler executed gasAccessor.executeFunction registerPee.`);
      }.bind(this));
      const speakOutput = 'おしっこを記録しました。';
      return handlerInput.responseBuilder
          .speak(speakOutput)
          .getResponse();
  }
};
const RegisterPooAndPeeIntentHandler = {
  canHandle(handlerInput) {
      return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
          && Alexa.getIntentName(handlerInput.requestEnvelope) === 'RegisterPooAndPeeIntent';
  },
  handle(handlerInput) {
      gasAccessor.executeFunction('registerPooAndPee', function (result) {
          console.log(`IntentHandler executed gasAccessor.executeFunction registerPooAndPee.`);
      }.bind(this));
      const speakOutput = 'うんちとおしっこを記録しました。';
      return handlerInput.responseBuilder
          .speak(speakOutput)
          .getResponse();
  }
};
const RegisterMilkIntentHandler = {
  canHandle(handlerInput) {
      return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
          && Alexa.getIntentName(handlerInput.requestEnvelope) === 'RegisterMilkIntent';
  },
  handle(handlerInput) {
      gasAccessor.executeFunction('registerMilk', function (result) {
          console.log(`IntentHandler executed gasAccessor.executeFunction registerMilk.`);
      }.bind(this));
      const speakOutput = 'ミルクを記録しました。';
      return handlerInput.responseBuilder
          .speak(speakOutput)
          .getResponse();
  }
};
const HelpIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
    },
    handle(handlerInput) {
        const speakOutput = 'うんち、おしっこ、ミルクが記録できます。何をしますか?';

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
const CancelAndStopIntentHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
            && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
                || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
    },
    handle(handlerInput) {
        const speakOutput = '終了します。バイバイ、またね。';
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};
const SessionEndedRequestHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
    },
    handle(handlerInput) {
        // Any cleanup logic goes here.
        return handlerInput.responseBuilder.getResponse();
    }
};

// The intent reflector is used for interaction model testing and debugging.
// It will simply repeat the intent the user said. You can create custom handlers
// for your intents by defining them above, then also adding them to the request
// handler chain below.
const IntentReflectorHandler = {
    canHandle(handlerInput) {
        return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest';
    },
    handle(handlerInput) {
        const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
        const speakOutput = `You just triggered ${intentName}`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            //.reprompt('add a reprompt if you want to keep the session open for the user to respond')
            .getResponse();
    }
};

// Generic error handling to capture any syntax or routing errors. If you receive an error
// stating the request handler chain is not found, you have not implemented a handler for
// the intent being invoked or included it in the skill builder below.
const ErrorHandler = {
    canHandle() {
        return true;
    },
    handle(handlerInput, error) {
        console.log(`~~~~ Error handled: ${error.stack}`);
        const speakOutput = `エラーが発生しました。もう一度、お願いします。`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};

// The SkillBuilder acts as the entry point for your skill, routing all request and response
// payloads to the handlers above. Make sure any new handlers or interceptors you've
// defined are included below. The order matters - they're processed top to bottom.
exports.handler = Alexa.SkillBuilders.custom()
    .addRequestHandlers(
        LaunchRequestHandler,
        RegisterPooIntentHandler,
        RegisterPeeIntentHandler,
        RegisterPooAndPeeIntentHandler,
        RegisterMilkIntentHandler,
        HelpIntentHandler,
        CancelAndStopIntentHandler,
        SessionEndedRequestHandler,
        IntentReflectorHandler, // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
        )
    .addErrorHandlers(
        ErrorHandler,
        )
    .lambda();

5. 開発したAlexaスキルをテスト

alexa developer console メニュー上のテストより試す。

「リクログでうんち」

そして、amazon echo dot 実機で試す。

「アレクサ、リクログでうんち」

以上。

実際に使ってみて変更を加えるかも。

【GCP認定資格】Professional Cloud Architectの勉強

概要

近々、Professional Cloud Architectの試験を受ける。

試験前に勉強したことについて要点をまとめておく。

落ちた場合も、受かった場合も(有効期限あるので)役に立つはず。

※2019/08時点のこと。内容が変わっている可能性は大いにあり。

やったこと(時系列)

Professional Cloud Architect とは、Google Cloud の技術を組織が活用するために必要なクラウド アーキテクチャGoogle Cloud Platform に関する専門的な知識を有することを認められ、ビジネス目標を実現するために、スケーラブルで高可用性を備え、堅牢かつ安全な動的ソリューションを設計、開発、管理できる者をいいます。

まずは Professional Cloud Architect を取得された方のブログ等を調べた。

試験の内容等は公開してはならないよう。無料でなんども受けれる模擬試験があること、ケーススタディを読んでおくことを知った。

試験ガイドとケーススタディ

模擬試験(何度も受けれるが、同じ問題だった...)

自分が使ったことのない Cloud Bigtable、Dataflow、Dataproc など、公式ドキュメントを読み、理解しなければならないと気づく。

Coursera の Preparing for the Google Cloud Professional Cloud Architect Exam の動画をみた。 f:id:kawabatas:20190818113907p:plain Coursera のことはよくわかっていないが、とりあえず登録し「監査のみ」にチェックをして進めると、動画は無料で見ることができた。Coursera の修了証は不要なので、十分かと。

要点まとめ

Architect 選定について

f:id:kawabatas:20190818120505p:plain GCPのサービスを理解して、ビジネス要件を理解して、各レイヤーごとに最適なアーキテクチャを考えていきましょうとのこと。

ネットワークについて

f:id:kawabatas:20190818120718p:plain subnet はリージョンのリソース。同じリージョンであれば異なるゾーンを跨ぐことができる。

データストアの使い分け

f:id:kawabatas:20190818120854p:plainf:id:kawabatas:20190818120900p:plainf:id:kawabatas:20190818120904p:plain

Cloud Storage の使い分け

f:id:kawabatas:20190818121029p:plain

Compute の使い分け

f:id:kawabatas:20190818121119p:plainf:id:kawabatas:20190818121122p:plain

データ処理

f:id:kawabatas:20190818121909p:plain

f:id:kawabatas:20190818121943p:plainf:id:kawabatas:20190818121947p:plain

IoT

f:id:kawabatas:20190818121308p:plain

Cloud IoT Core → Cloud Pub/Sub → BigQuery といった流れ

Security

f:id:kawabatas:20190818121419p:plain

最小限の役割を付与する。

例えば、A さんに DeptY に viewer 権限を与えて、static-assets プロジェクトでは editor 権限を与えるなど

f:id:kawabatas:20190818121620p:plain

f:id:kawabatas:20190818121649p:plain

ファイアウォールルールは階層化可能。

SRE

f:id:kawabatas:20190818121726p:plain

まずはモニタリング。

試験の受け方

f:id:kawabatas:20190818121812p:plainf:id:kawabatas:20190818121815p:plainf:id:kawabatas:20190818121819p:plainf:id:kawabatas:20190818121822p:plainf:id:kawabatas:20190818121827p:plain

ケーススタディのサンプル回答

Mountkirk Games

サンプルの回答

f:id:kawabatas:20190818183341p:plain f:id:kawabatas:20190818183337p:plain

ポケモンGOはCloud Datastore使ってるらしい

Dress4Win

サンプルの回答

f:id:kawabatas:20190818183407p:plainf:id:kawabatas:20190818183411p:plainf:id:kawabatas:20190818183418p:plainf:id:kawabatas:20190818183421p:plainf:id:kawabatas:20190818183427p:plainf:id:kawabatas:20190818183432p:plain

TerramEarth

サンプルの回答

f:id:kawabatas:20190818183452p:plainf:id:kawabatas:20190818183455p:plainf:id:kawabatas:20190818183459p:plain

終わりに

試験に合格できるだろうか...

後日、ブログをかく。

(追記)試験結果

結果は、、、不合格。残念。。

試験内容については口外してはいけないので、触れない。

終了後、合否のみ表示されていた。点数や正答等の詳細はわからない。

自分的には6割くらいはできてた感。

あとの4割は(自信はなかったが)とりあえず回答した感じだった。

試験を受ける前までは、落ちたら再挑戦する気満々だったが、

当分いいかな、という気持ちになっている。

理由は二つ。

一つ目は、合格はできなかったが、GCPの知識は深められたこと(自分的には合格できるだろうと思えるくらいに)。

たぶん一番大事なことはこれで、業務にも活きてくると思う。

二つ目は、設問文や回答文において、わかりにくい表現が。

僕の知識が足りないのか、日本語訳がおかしいのか、そもそも微妙な問題なのかはわからない。

とにかく、この資格を取ることがそんなに重要なのか、と自分の中に疑問を抱いたので、再挑戦するかどうかは保留にする。

落ちたのは残念。

でも、実力がアップしたのは事実。

終わったことだから、結果ではなく、プロセスを褒めてあげたい。

Google Cloud Next '19 in Tokyo 参加レポート

概要

昨年に続き、今年も参加できたので、レポートです。

Google Cloud Nextとは、

Next は Google Cloud に関する技術や事例などを共有するためのイベントです。

kawabatas.hatenablog.com

受講セッション

セッション動画は youtube に徐々に上げられています。

基調講演1日目

  • Cloud Run が東京リージョンへ
  • Anthos。オンプレや他クラウドVM のアプリを GKE のコンテナにワークロードに変換し、モダナイズできるらしい。今回の Next の目玉だった感。
  • DeNA が オンプレから GCP を選択した一番の理由は、費用ではなく、人材がより創造性にフォーカスできると確信したかららしい。

D1-1-S07 アプリ開発のスピードをあげる Google App EngineGCP のご紹介

AppEngine 新機能

  • 第二世代ランタイム
    • アーミーナイフ問題(AppEngine に memcache や DB があったり)を解決していってる
    • gVisor で動く
    • しれっと、メモリが倍になってるらしい
  • VPC 接続
    • VPC 作成、VPC コネクタ生成、app.yaml の設定追加 → Internal IP で GCP リソースを使える(Cloud Memorystoreも使える?)
  • Cloud Run 互換性

また AppEngine に Deplicate の予定はなく、これからも長期で開発を進めていく予定だとのこと。

D1-2-S11 ユーザ目線で作る AI アプリ開発の極意 ~ AI 開発を発注するその前に

機械学習を利用して確率を表示するとき、確率だけを表示するのは良くない。

例えば、

×遅延確率 83 %

○現在XX線は通常より1.5倍混み合っているので、10分の遅延が見込まれます

D1-3-S02 Cloud Run ~ Knative を使った新しいサーバーレス

Knative:コンテナをサーバレス like に使える

今までのサーバレスの課題(制約)

  • 言語、ライブラリに制約
  • 特定のベンダーロックイン
  • GPU/TPU 特定のハードウェアへアクセスできない
    • → Cloud Run on GKE なら GPU など使える

IAM Role で認証つきリクエストもできる

一般ユーザ向けのサービスを Cloud Run で提供するなら、Concurrency は検証して、適切な設定をしたほうが良さそう

D1-4-S02 メルペイのマイクロサービスを支える GKE と Cloud Spanner

マイクロサービスの話しはこちらと同じだった感。

Cloud Spanner は SQL like だが、MySQL とは全然違う(Primary Keyダメ)ので、公式ドキュメントをしっかり読み込みましょう、とのこと。

現状、エミュレータがないので、共有インスタンスをローカル用、CI用に立てて利用しているとのこと。辛い。。

D1-5-S01 Google Kubernetes Engine によるコンテナセキュリティの道

CI/CD、コンテナビルドのレイヤー

  • GCR のイメージの脆弱性スキャン

Kubernetes へのデプロイのレイヤー

  • イメージのデプロイ可能なリポジトリをポリシーに設定できる

アプリケーション運用時のレイヤー

  • gVisor
  • Istio のサービスメッシュ
  • NEW) Event Thread Detection
    • stackdriver のログ出力から脅威を検出するらしい

その他

  • NEW)Cloud Security Command Center
    • セキュリティの問題を一箇所で確認できるダッシュボード

D1-6-S10 GCPにおけるリソースとコスト管理のベストプラクティス

ドメイン、組織のベストプラクティスは、今まで考えたことなかったので、なるほどと思いました。

  • 重要な役割を複数の人に割り当てる
  • 特権管理者用のメールアドレスを作成

D1-7-S03 GCP で稼働する GO アプリケーションのパフォーマンスチューニング

go tool xxx でトレースや CPU プロファイリングなどができ、パフォーマンスチューニングをできることがわかった。

正直、今の僕では、全然使いこなせないと感じた。

これが使いこなせれば、Stackdriver Profiler も活躍しそう。

Tips f:id:kawabatas:20190811081236j:plain f:id:kawabatas:20190811081218j:plain

基調講演2日目

ファミマの社長のプレゼンが魅力的だったw

Gmail にチャット機能がくる。

データ収集〜分析〜可視化〜機械学習 までのツールが増えた感。

D2-1-S08 Googleスプレッドシートインサイトを発見し共有するための30の方法

Connected sheetsで BigQuery の何十億のデータをスプレッドシートで分析できるようになる。

スプレッドシートで分析レポートを作成できる。

データポータルが不要になる感。。とりあえずデータは全てBigQueryにつっこみ、軽く分析するならスプレッドシートで、

データアナリストが詳細を分析するなら Loocker 等、BIツールを使う感じになるのかなと。

D2-2-S01 少人数で実現する GKE と Firebase を使ったモバイルアプリ開発手法

ブロックチェーンの理解が少しだけ深まった

D2-3-S11 Google Cloud で読む、書く、翻訳する:テキスト処理を容易に実現する機械学習

AutoML Natural Language では専門用語を学習させることもでき、

僕らも機械学習を使うことができると思った。

Tips

  • 12時のセッションを受講すると、お弁当出ました。18〜19時台のセッションでもお弁当出ました。
  • 充電スペースはありますが、混雑しているのでモバイルバッテリーを持っていった方が良さそうです。
  • Expo 会場では各社がノベルティを配布しているのですが、レベルが上がっていました。。帰りにバックが一杯になったので、PCも持って行かず、荷物少なめの方が良さそうです。

終わりに

多くのインプットがありました。

来年も参加できるといいな。

【無料】Ruby で Cloud Run を使って API を作ってみた

概要

コンテナをサーバレス like に使える Cloud Run。

今までのサーバレスの課題(制約)

  • 言語、ライブラリに制約
  • 特定のベンダーロックイン
  • GPU/TPU 特定のハードウェアへアクセスできない → Cloud Run on GKE なら GPU など使える

を解決できる。

ポケモンGOで、特定の位置に★5レイドが出現しているか調べる API を作ってみた。 (※外部のAPIを使っているので、いつの間にか使えなくなってるかも...)

github.com

言語は Ruby

料金

今回使った GCP リソースは

  • Cloud Run
  • Container Registry
  • Cloud Storage
  • Cloud Build

で全部のはず。

Container Registry の料金  |  Container Registry  |  Google Cloud

Container Registry は Docker イメージに使用する Cloud Storage の費用のみ。

GCP Free Tier  |  Google Cloud Platform Free Tier  |  Google Cloud

Always Free(無料枠)で Cloud Storage、Cloud Build は事足りそう。

Pricing  |  Cloud Run Documentation  |  Google Cloud

Cloud Run も無料枠があり、自分1人で使うものなので、事足りそう。

ということで、おそらく無料で済むかと。(もしお金がかかっていたら追記します。)

手順

ドキュメントはこのあたり

Cloud Build、Cloud Run の API が有効になっていなければ、有効にする。

Container Registry に Docker image をあげる

gcloud builds submit --tag gcr.io/$GCP_PROJECT_ID/pokemon-go-raid-appearance --project $GCP_PROJECT_ID

Cloud Run にデプロイ

gcloud beta run deploy pokemon-go-raid-appearance --image gcr.io/$GCP_PROJECT_ID/pokemon-go-raid-appearance --project $GCP_PROJECT_ID --set-env-vars THIRD_PARTY_ENDPOINT="http://127.0.0.1",BASE_LAT="35.681236",BASE_LNG="139.767125"

API エンドポイントへアクセス。https://XXXXXXXXX.run.app

終わり。

非常に簡単に API を公開できた。

ハマったこと

とはいえ、いくつかハマった。

その1

f:id:kawabatas:20190809231000p:plain

ERROR: (gcloud.beta.run.deploy) Cloud Run error: Container failed to start. Failed to start and then listen on the port defined by the PORT environment variable. Logs for this revision might contain more information.

その時のコードはこちら

Webサーバーを立ち上げる必要があるらしい。

sinatra を使うことに。

リクエストをトリガーにスクリプトを一回実行できる、今までのサーバレスの課題を解決したものがあれば、もっと便利そうだなー。

その2

f:id:kawabatas:20190809232145p:plain

CORS。

無事にアクセスできた。

f:id:kawabatas:20190810070215p:plain

所感

Cloud Run は仕事でもプライベートでももっともっと活用していこうと思った。

今回の「ポケモンGOのレイド出現アプリケーション」は後ほどアクセス制限して、セキュリティ高めよう。

また View も google map に表示する等にしようと思う。