元営業WEBエンジニアのアプリ開発日記

営業出身のWEB系エンジニアが気になったものから作ってはメモを残してくブログ

MacでPython, JupyterLabのローカル環境構築

概要

最近はPython使うことも多くなったきたので
ローカル環境構築したときのメモ残しとこう

手順

Pythonインストールのためのツールpyenvをインストール

pyenv使うと好きなPythonのバージョン指定可能

brew install pyenv

# インストールされていることの確認
pyenv --version

Python自体のインストール

pyenv使ってPythonのインストール

# インストールできる一覧の確認
$ pyenv install --list
Available versions:
  ...
  3.7.4
  ...

# 最新バージョンをインストール
$ pyenv install 3.7.4

# 使用するPythonバージョン指定
$ pyenv global 3.7.3
$ pyenv versions
  system
* 3.7.4 (set by /Users/s.uchiyama/.pyenv/version)

# pyenvを有効にするためのおまじない
$ cat << 'EOS' >> ~/.zshrc
# python
eval "$(pyenv init -)"
EOS

$ source ~/.zshrc

# Pythonバージョン確認
$ python --version
Python 3.7.4

プロジェクト用のPython環境作成

自身のローカルにPython環境はできたので
Python3.3以降標準搭載されているvenvでプロジェクト用のPython環境作成
これで違う人がこのプロジェクト参加した時も同じバージョンで開発できーる

# 環境作成
$ python -m venv venv
# 環境をアクティベイと!
$ source venv/bin/activate
(venv) $ python --version
Python 3.7.4
# 環境をディアクティベイと!
$ deactivate

個別の環境ができているか確認

上記の流れだとプロジェクト個別にPythonバージョン設定されたかわからないので、検証

# Python2系をインストール
$ pyenv install 2.7.16
# バージョンを確認
$ pyenv versions
  system
  2.7.16
* 3.7.4 (set by /Users/s.uchiyama/.pyenv/version)
# 2系を有効にする
$ pyenv global 2.7.16
# バージョンを確認
$ pyenv versions
  system
* 2.7.16 (set by /Users/s.uchiyama/.pyenv/version)
  3.7.4
$ python --version
Python 2.7.16

# 仮想環境でバージョンを確認
$ source venv/bin/activate
(venv) $ python --version
Python 3.7.4
# ちゃんとバージョン3系になってる!!!

ローカルはPython 2.7.16なのにプロジェクトではPython 3.7.4だからちゃんと動いてる!!
ローカルのPythonバージョン3系に戻しとこう

$ deactivate
$ pyenv global 3.7.4
$ python --version
Python 3.7.4

JupyterLabのインストール

# jupyter labのインストール
$ echo 'jupyterlab==1.2.4' > requirements.txt
$ pip install -r requirements.txt
# jupyter lab起動
$ jupyter lab
[I 15:51:31.470 LabApp] JupyterLab extension loaded from /Users/s.uchiyama/src/github.com/shintaro123/gcp-infra/etl/eda/venv/lib/python3.7/site-packages/jupyterlab

デフォルトブラウザが開いてjupyter labが表示された!

まとめ

Dockerでローカル環境作ろうかと思ったけど、まずは伝統的なお作法から入門しようということで、ググって出てきたやつでやってみた。

jupyterもanacondaとか環境構築ツール書いてあったけど
基本的にパッケージのたぐいは最小限だけ入れたい子なのでpipで入れやした。

約1ヶ月でGCP Professional Cloud Architectに合格したお話

概要

2019/12/21に受験して何とか合格したので日記を書いておく
何か審査してるみたいだけど合格とは書いてある!

f:id:shintaro-0112:20191223010047p:plain

勉強法

以下3点に取り組み、最終的には問題系を2往復して頭に叩き込んだ

かかったコスト

23,327円(受験料:22,127円 + Udemy:1,200円 + Coursera:0円)
※110.635円/USD換算
※Courseraは課金が始まる前にやり切るスタイルで頑張った

勉強期間

平日と週末に勉強し1ヶ月。
会社の制度を使って平日2日丸々この試験勉強に使えたのも大きかった。

また、正確にいうと
GCPを触り始めたのが2019年10月頃からなので実務2ヶ月、試験対策1ヶ月で挑んだ感じです

所感

「今回はCourseraとUdemyあと模擬試験だけで合格できるかな」という勝手なテーマを設けて挑みました。
結果的には合格でしたが、正直この3点セットだけでは不安が残るなという感想でした。

もちろん上記3点セットは十分に役立ちましたが
GKE、Cloud CDN、Load Balancer、BigQueryなどに関してはもう一声勉強しておけばよかったなーという所感です。
ドキュメントのコンセプトや「Googleのベストプラクティスでは」みたいな表記が書いてあるとこは重点的に見ておきたかったっす。

何はともあれ受かったので
これから物を作りながらより深く知識を深めて行きたいっす

ほぼ無料でGCPのフォルダリソースにIAMポリシーを紐付ける

概要

AWSやるぞって資格までとったけど、最近は仕事も変わりもっぱらGCPさわってます
GCPの権限(IAM)周りの勉強も踏まえて自分で色々設定してみる! こんな感じの物を作るのだ! f:id:shintaro-0112:20191116153154p:plain

ほぼ無料でと書きましたが独自ドメイン(数円〜数十円)の料金がかかります

Cloud Identityの設定

フォルダ使うためには組織が必要
組織使うためにはGSuiteかCloud Identityが必要

GSuiteは最低でも1ユーザー/月680円するけど Cloud Identityは無料。無料!!こっち使おう

GCPコンソールにて「IAMと管理」->「IDと組織」->「お申し込み」を押下して ぽちぽちと適当にアカウント情報を入れていく f:id:shintaro-0112:20191116153355p:plain

ドメインの所有権の確認

途中で「ドメインの所有権の確認」というステップがある ドメインが自分のものであることを証明する必要がある

独自ドメイン購入

とりあえずお名前.comで適当にドメインを買う
(今回関係ないけどWhois代行設定は初回購入時無料なので設定が吉)

無料サーバーの準備

今回は「ドメインの所有権の確認」の方法としてhtmlのmetaタグを用いた認証を行う。そのために、自身のドメインにhttpでアクセスできる環境が必要になる。

無料で独自ドメイン対応したwebサーバー構築調べたら XFREEなるサーバーが使えそうだったのでこれで行く。無料プランを申し込み。

独自ドメインの向き先を自身のサーバーに向ける

「お名前.com」のネームサーバーでAレコードを設定して 購入したドメインの向き先ipを無料で購入したサーバーに設定する必要がある。

まずはXFREE管理パネルのドメイン設定追加画面に行き
「Aレコード認証」のコンテンツIPをコピーする。 f:id:shintaro-0112:20191116154003p:plain

コピーしたIPアドレスを、お名前.comのDNS設定でAレコードとして設定する。 f:id:shintaro-0112:20191116155715p:plain

以下コマンドのexample.comを自身のドメインに変更して実行
設定したIPアドレスが表示されればOK!

dig a example.com +short

DNS設定が反映されるまでにそこそこ時間がかかるかも)
(お名前.comのNAMEサーバーが0x.dnsv.jpになっていることも要確認。
違うサービスのNAMEサーバー使ってる場合はそちらでAレコード設定しなきゃ)

上記反映された状態でXFREEの管理パネルのドメイン追加画面に行き 「ドメイン設定を追加する(確認)」ボタンを押下すると購入したドメイン追加される。

ドメイン所有権確認

XFREE管理パネルでwebFTPボタンを探してindex.htmlをブラウザで編集。
Cloud Identity設定画面で表示されたmetaタグをコピーして保存

f:id:shintaro-0112:20191116154815p:plain

Cloud Identity設定画面に戻ると認証成功するはず!

GCPフォルダを作成

組織->フォルダ->プロジェクトを作成

# 組織ID確認
gcloud organizations list

# 組織IDを指定してフォルダ作成
gcloud alpha resource-manager folders create --display-name=[folder_name] --organization=[organization_id]

# フォルダID確認
gcloud alpha resource-manager folders list --organization=[organization_id]

# フォルダID指定してプロジェクト作成
gcloud projects create --folder=1048782978666 [project_name]

IAMポリシーをフォルダーに紐付け

個別アカウントではなく、エンジニアとかマーケターとかざっくりした区分けをGoogle Groupで作成して、それに役割を与える。 あとはそれに当てはまる個別アカウントを対象グループに入れるようにしたい。

検証のために、Cloud Identityで適当なアカウント「aaa.bbb@example.com」を作成してGCPコンソールにログインする。 なんの役割も与えてないのでGCEの画面とか行くと権限エラーになる f:id:shintaro-0112:20191116155528p:plain

Google Groupの作成

Google Groupを作成。
作成した適当なアカウントを作成したグループに追加。

Google Groupに対して役割を付与し、フォルダーにIAMポリシーを紐付け

# 作成したGoogle Groupにフォルダーレベルのowner役割を付与
# 例)[google_group_email]:engineer@example.com
gcloud resource-manager folders add-iam-policy-binding [folder_id] --member group:[google_group_email] --role roles/owner

# フォルダリソースに紐づくIAMポリシーの確認
gcloud resource-manager folders get-iam-policy [folder_id]

再度適当なアカウントでGCPコンソールを見るとプロジェクトにアクセスできるようになってる!! f:id:shintaro-0112:20191116155149p:plain

所感

フォルダを使った運用イメージがつかなかったので自分で実際に手を動かしてみた。
フォルダを適切に区切れば、IAMポリシーの設計も素敵にできそうな空気がした

「アジャイルサムライ」読んでみた

概要

仕事でスクラム開発を行うにあたり
アジャイルサムライ を推されたので読んでみる

所感

非常に読みやすかった。週末土日でぱっと読めた。

前の会社でみんなが言っているアジャイルだと
ウォーターフォールではなく「とりあえずコーディングして物作っちゃうこと」程度の浅い話しか聞いたことなかったが、この本を読んでもっと広い範囲での話であることを理解した。

確かに、短い期間(イテレーション)で動くものを作成していくと言うことは合っていたが、その開発に至るまでも重要なのである。
プロジェクトメンバー全員を同じバスに乗せるためのインセプションデッキ作成に始まり、独立性やテスト可能かなど、適切に考えたストーリーボードの作成。
バーンダウンやバーンアップチャートによりストーリーボードの消化具合の監視。 これによりイテレーション間での進み具合(ベロシティー)も把握できる。

その他ユニットテスト、TDD(テスト駆動開発)、CIの重要性など非常にわかりやすく説明してくれている。

何より、心に響いたのは「お客様の期待値をコントロールするために厳しい質問も毅然としていく必要がある」と言うことである。
ウォーターフォールではある程度隠してこちょこちょ進められるが
アジャイルの場合、短い期間で動くものを提供するので嫌でも進捗が見えてくる。

自身がアジャイルで開発していくことを想像した時に、毅然とした態度で勇敢に現状を伝えお客様の期待値をコントロールできるのかと不安になった。
この本はシステム開発手法だけではなく、仕事に取り組む上での心構えを考える勉強にもなった。

GAE(go1.12)でCloud SQLを操作してみるぞ

概要

GAE(go1.12)からCloud SQLを操作するぞ

事前準備

Cloud SQLインスタンスの作成

Cloud SQLの種類一覧表示や

gcloud sql tiers list

一番安そうなやつにトライ

gcloud sql instances create ucwork --tier=db-f1-micro --region=asia-northeast1
Creating Cloud SQL instance...done.
Created [https://www.googleapis.com/sql/v1beta4/projects/ucwork-ai-000002/instances/ucwork].
NAME    DATABASE_VERSION  LOCATION           TIER         PRIMARY_ADDRESS  PRIVATE_ADDRESS  STATUS
ucwork  MYSQL_5_7         asia-northeast1-b  db-f1-micro  35.243.68.17     -                RUNNABLE

SQLインスタンス情報取得

gcloud sql instances describe ucwork
backendType: SECOND_GEN
connectionName: [your connection name]
...

MySQLパスワードを設定しとく

gcloud sql users set-password root --host % --instance ucwork --password [your password]
Updating Cloud SQL user...done.

Cloud SQL Proxy をインストール&実行

ローカルからCloud SQLに接続するためのProxyをインストール

# 取得
curl -o cloud_sql_proxy https://dl.google.com/cloudsql/cloud_sql_proxy.darwin.amd64
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 13.6M  100 13.6M    0     0  6792k      0  0:00:02  0:00:02 --:--:-- 6796k

# 実行権限付与
chmod +x cloud_sql_proxy

# 実行
./cloud_sql_proxy

CloudSQLに接続

MySQLコマンドやDataGrip的なツールで以下情報を打ち込み
Cloud SQLに接続できることを確認

  • ホスト名
  • ポート名
    • 3306
  • ユーザー名
    • root
  • パスワード
    • [your password]

構築

Cloud SQLへの接続

CloudSQL初期化開始

initメソッドの中で初期化処理を開始
事前準備で作成したユーザー名やパスワードなどの情報を設定

internal/config.go

var (
    DBSql repository.OrderDatabase
)

func init(){
    var err error
    // Cloud SQLの初期設定
    DBSql, err = configureCloudSQL(cloudSQLConfig{
        Username: "root",
        Password: "[your password]",
        Instance: "[your connection name]",
    })
    if err != nil {
        log.Fatal(err)
    }
}

type cloudSQLConfig struct {
    Username, Password, Instance string
}

Cloud SQLを操作するためのDao的DB作成

GAE用とローカル環境用で接続先が異なる模様

internal/config.go

func configureCloudSQL(config cloudSQLConfig) (repository.OrderDatabase, error) {
    if os.Getenv("GAE_INSTANCE") != "" {
        // Running in production.
        return db.NewMySQLDB(db.MySQLConfig{
            Username:   config.Username,
            Password:   config.Password,
            UnixSocket: "/cloudsql/" + config.Instance,
        })
    }

    // Running locally.
    return db.NewMySQLDB(db.MySQLConfig{
        Username: config.Username,
        Password: config.Password,
        Host:     "localhost",
        Port:     3306,
    })
}

internal/db/sql.go

type MySQLConfig struct {
    // Optional.
    Username, Password string

    // Host of the MySQL instance.
    //
    // If set, UnixSocket should be unset.
    Host string

    // Port of the MySQL instance.
    //
    // If set, UnixSocket should be unset.
    Port int

    // UnixSocket is the filepath to a unix socket.
    //
    // If set, Host and Port should be unset.
    UnixSocket string
}

Cloud SQLへの接続確認&実際に接続&SQL準備

BookShelfチュートリアルの処理が多いので小分けに理解していく

接続確認

ensureTableExistsメソッドで実際に接続させてpingで接続確認
DBとテーブルが存在するか確認して、存在しない場合は必要なものを作成

internal/db/sql.go

func NewMySQLDB(config MySQLConfig) (repository.OrderDatabase, error) {
    // Check database and table exists. If not, create it.
    if err := config.ensureTableExists(); err != nil {
        return nil, err
    }
  
    // 実際に接続
  
    // SQL準備
    
    return db, nil
}

func (config MySQLConfig) ensureTableExists() error {
    conn, err := sql.Open("mysql", config.dataStoreName(""))
    if err != nil {
        return fmt.Errorf("mysql: could not get a connection: %v", err)
    }
    defer conn.Close()

    // Check the connection.
    if conn.Ping() == driver.ErrBadConn {
        return fmt.Errorf("mysql: could not connect to the database. " +
            "could be bad address, or this address is not whitelisted for access.")
    }

    if _, err := conn.Exec("USE ucwork"); err != nil {
        // MySQL error 1049 is "database does not exist"
        if mErr, ok := err.(*mysql.MySQLError); ok && mErr.Number == 1049 {
            return createTable(conn)
        }
    }

    if _, err := conn.Exec("DESCRIBE orders"); err != nil {
        // MySQL error 1146 is "table does not exist"
        if mErr, ok := err.(*mysql.MySQLError); ok && mErr.Number == 1146 {
            return createTable(conn)
        }
        // Unknown error.
        return fmt.Errorf("mysql: could not connect to the database: %v", err)
    }
    return nil
}

// dataStoreName returns a connection string suitable for sql.Open.
func (c MySQLConfig) dataStoreName(databaseName string) string {
    var cred string
    // [username[:password]@]
    if c.Username != "" {
        cred = c.Username
        if c.Password != "" {
            cred = cred + ":" + c.Password
        }
        cred = cred + "@"
    }

    if c.UnixSocket != "" {
        return fmt.Sprintf("%sunix(%s)/%s", cred, c.UnixSocket, databaseName)
    }
    return fmt.Sprintf("%stcp([%s]:%d)/%s", cred, c.Host, c.Port, databaseName)
}

var createTableStatements = []string{
    `CREATE DATABASE IF NOT EXISTS ucwork DEFAULT CHARACTER SET = 'utf8' DEFAULT COLLATE 'utf8_general_ci';`,
    `USE ucwork;`,
    `CREATE TABLE IF NOT EXISTS orders (
      id INT UNSIGNED NOT NULL AUTO_INCREMENT,
      name VARCHAR(255) NULL,
      PRIMARY KEY (id)
  )`,
}

// createTable creates the table, and if necessary, the database.
func createTable(conn *sql.DB) error {
    for _, stmt := range createTableStatements {
        _, err := conn.Exec(stmt)
        if err != nil {
            return err
        }
    }
    return nil
}

実際に接続&SQL準備

実際に接続してPINGで再度接続確認
conn.PrepareメソッドでDB structureにSQLを設定

type mysqlDB struct {
    conn *sql.DB

    list   *sql.Stmt
    insert *sql.Stmt
}

func NewMySQLDB(config MySQLConfig) (repository.OrderDatabase, error) {
  // 接続確認

  conn, err := sql.Open("mysql", config.dataStoreName("ucwork"))
    if err != nil {
        return nil, fmt.Errorf("mysql: could not get a connection: %v", err)
    }
    if err := conn.Ping(); err != nil {
        conn.Close()
        return nil, fmt.Errorf("mysql: could not establish a good connection: %v", err)
    }

    db := &mysqlDB{
        conn: conn,
    }

    // Prepared statements. The actual SQL queries are in the code near the
    // relevant method (e.g. addOrder).
    if db.list, err = conn.Prepare(listStatement); err != nil {
        return nil, fmt.Errorf("mysql: prepare list: %v", err)
    }
    if db.insert, err = conn.Prepare(insertStatement); err != nil {
        return nil, fmt.Errorf("mysql: prepare insert: %v", err)
    }

    return db, nil
}

const listStatement = `SELECT * FROM orders ORDER BY name`

const insertStatement = `INSERT INTO orders ( name ) VALUES ( ? )`

Cloud SQL操作実装

登録

CloudSQLにレコードを登録し、登録されたIDを返却

internal/db/sql.go

func (db *mysqlDB) AddOrder(b *repository.Order) (id int64, err error) {
    r, err := execAffectingOneRow(db.insert, b.Name)
    if err != nil {
        return 0, err
    }

    lastInsertID, err := r.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf("mysql: could not get last insert ID: %v", err)
    }
    return lastInsertID, nil
}

stmt.Execメソッドでinsert処理を実行して1レコードだけ登録されていることを確認

internal/db/sql.go

func execAffectingOneRow(stmt *sql.Stmt, args ...interface{}) (sql.Result, error) {
    r, err := stmt.Exec(args...)
    if err != nil {
        return r, fmt.Errorf("mysql: could not execute statement: %v", err)
    }
    rowsAffected, err := r.RowsAffected()
    if err != nil {
        return r, fmt.Errorf("mysql: could not get rows affected: %v", err)
    } else if rowsAffected != 1 {
        return r, fmt.Errorf("mysql: expected 1 row affected, got %d", rowsAffected)
    }
    return r, nil
}

一覧表示

db.list.Queryでselect処理を実施
scanOrderメソッドで期待する構造体orderに結果を詰め込んでいく

internal/db/sql.go

func (db *mysqlDB) ListOrders() ([]*repository.Order, error) {
    rows, err := db.list.Query()
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var orders []*repository.Order
    for rows.Next() {
        order, err := scanOrder(rows)
        if err != nil {
            return nil, fmt.Errorf("mysql: could not read row: %v", err)
        }

        orders = append(orders, order)
    }

    return orders, nil
}

Scanメソッドでselect結果から期待するカラムを取得

internal/db/sql.go

// rowScanner is implemented by sql.Row and sql.Rows
type rowScanner interface {
    Scan(dest ...interface{}) error
}

// scanOrder reads a order from a sql.Row or sql.Rows
func scanOrder(s rowScanner) (*repository.Order, error) {
    var (
        id              int64
        name            sql.NullString
    )
    if err := s.Scan(&id, &name); err != nil {
        return nil, err
    }

    order := &repository.Order{
        ID:            id,
        Name:         name.String,
    }
    return order, nil
}

検証

ローカル環境で実行

起動

go run cmd/ucwork/main.go
2019/09/26 10:00:52 Defaulting to port 8080
2019/09/26 10:00:52 Listening on port 8080  

アクセス

登録

curl -v -X POST -H "Content-Type: application/json" -d '{"Name":"ucwork"}' http://localhost:8080/orders
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /orders HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 17
>
* upload completely sent off: 17 out of 17 bytes
< HTTP/1.1 200 OK
< Date: Thu, 26 Sep 2019 01:01:33 GMT
< Content-Length: 24
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
{"ID":0,"Name":"ucwork"}%

一覧確認

curl -v http://localhost:8080/orders
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /orders HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Thu, 26 Sep 2019 01:02:24 GMT
< Content-Length: 51
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
[{"ID":1,"Name":"ucwork"}]%

GAE環境で構築

デプロイ

いつも通りデプロイしたら怒られた

gcloud app deploy deployments/app.yaml
Services to deploy:

descriptor:      [/Users/shintaro.a.uchiyama/project/github.com/shintaro123/ucwork-go/deployments/app.yaml]
source:          [/Users/shintaro.a.uchiyama/project/github.com/shintaro123/ucwork-go/deployments]
target project:  [ucwork-ai-000002]
target service:  [default]
target version:  [20190926t100502]
target url:      [https://ucwork-ai-000002.appspot.com]


Do you want to continue (Y/n)?  y

Beginning deployment of service [default]...
Created .gcloudignore file. See `gcloud topic gcloudignore` for details.
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 16 files to Google Cloud Storage               ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...failed.
ERROR: (gcloud.app.deploy) Error Response: [9] Cloud build 8b178da0-ca9d-4066-a56b-4c6c0c9a3859 status: FAILURE.
Build error details: {"error":{"errorType":"BuildError","canonicalCode":"INVALID_ARGUMENT","errorId":"7873E743","errorMessage":"2019/09/26 01:05:37 Building /tmp/staging/srv, with main package at ./deployments, saving to /tmp/staging/usr/local/bin/start\n2019/09/26 01:05:37 Running \u0026{/usr/local/go/bin/go [go build -o /tmp/staging/usr/local/bin/start ./deployments] [PATH=/go/bin:/usr/local/go/bin:/builder...

app.yamlをdeployment、main.goをcmdディレクトリに移動したからmainがどこにあるのかわかってないっぽい。
app.yamlを以下の通り修正して再度デプロイ

gcloud app deploy deployments/app.yaml
Services to deploy:

descriptor:      [/Users/shintaro.a.uchiyama/project/github.com/shintaro123/ucwork-go/deployments/app.yaml]
source:          [/Users/shintaro.a.uchiyama/project/github.com/shintaro123/ucwork-go/deployments]
target project:  [ucwork-ai-000002]
target service:  [default]
target version:  [20190926t120405]
target url:      [https://ucwork-ai-000002.appspot.com]


Do you want to continue (Y/n)?  y

Beginning deployment of service [default]...
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 2 files to Google Cloud Storage                ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://ucwork-ai-000002.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s default

To view your application in the web browser run:
  $ gcloud app browse

アクセス

登録

curl -kv -X POST -H "Content-Type: application/json" -d '{"Name":"ucwork_gae"}' http://ucwork-ai-000002.appspot.com/orders
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 172.217.161.84...
* TCP_NODELAY set
* Connected to ucwork-ai-000002.appspot.com (172.217.161.84) port 80 (#0)
> POST /orders HTTP/1.1
> Host: ucwork-ai-000002.appspot.com
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 21
>
* upload completely sent off: 21 out of 21 bytes
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=utf-8
< Vary: Accept-Encoding
< X-Cloud-Trace-Context: d90b597e3668e8d9a47fca8d83bfe594;o=1
< Date: Thu, 26 Sep 2019 03:18:23 GMT
< Server: Google Frontend
< Content-Length: 28
<
* Connection #0 to host ucwork-ai-000002.appspot.com left intact
{"ID":0,"Name":"ucwork_gae"}%

一覧確認

curl -kv https://ucwork-ai-000002.appspot.com/orders
*   Trying 172.217.161.84...
* TCP_NODELAY set
* Connected to ucwork-ai-000002.appspot.com (172.217.161.84) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-CHACHA20-POLY1305
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=California; L=Mountain View; O=Google LLC; CN=*.appspot.com
*  start date: Sep  5 20:18:07 2019 GMT
*  expire date: Nov 28 20:18:07 2019 GMT
*  issuer: C=US; O=Google Trust Services; CN=GTS CA 1O1
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fdfc4806c00)
> GET /orders HTTP/2
> Host: ucwork-ai-000002.appspot.com
> User-Agent: curl/7.54.0
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
< HTTP/2 200
< content-type: text/plain; charset=utf-8
< vary: Accept-Encoding
< x-cloud-trace-context: 62b1a9540c1a9e024724646c7b904c95;o=1
< date: Thu, 26 Sep 2019 03:18:54 GMT
< server: Google Frontend
< content-length: 80
< alt-svc: quic=":443"; ma=2592000; v="46,43",h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000
<
* Connection #0 to host ucwork-ai-000002.appspot.com left intact
[{"ID":1,"Name":"ucwork"},{"ID":2,"Name":"ucwork"},{"ID":3,"Name":"ucwork_gae"}]%

まとめ

GAEってえいやでアプリケーションできるけど、ネットワークの制御とかどうなってるのかいまいち理解できてないな・・・
とりあえず一通り触るだけ触ってから気になるとこ調べていこう

GAE(Go1.12)でDatastoreに接続してみるぞ

概要

GAE(Go1.12)でDatastoreに接続してみる

構築

Datastoreへ接続

Datastoreに接続するための初期化処理開始

initメソッドで初期化処理を実施
Datastoreを操作するDao的なDBを作成していく!!

internal/config.go

var (
    DB repository.MemberDatabase
)

func init(){
    var err error
    DB, err = configureDatastore("ucwork-ai-000002")
    if err != nil {
        log.Fatal(err)
    }
}

Datastoreに接続するためのclient作成

projectIDを指定してDatastoreに接続するためのclientを作成

internal/config.go

func configureDatastore(projectID string) (repository.MemberDatabase, error){
    ctx := context.Background()
    client, err := datastore.NewClient(ctx, projectID)
    if err != nil {
        return nil, err
    }
    return db.NewDatastoreDB(client)
}

Datastoreへの接続確認と構造体の完成

NoSQLってトランザクションないんちゃったっけ。
トランザクション使ってDatastoreへ接続できるか確認

接続できることを確認したら対象のclientをdatastoreDBへ設定し返却
戻り値の型がrepository.MemberDatabaseなのでこの構造体はDB操作系のメソッドを持つのである。

internal/db/datastore.go

type datastoreDB struct {
    client *datastore.Client
}

var _ repository.MemberDatabase = &datastoreDB{}

func NewDatastoreDB(client *datastore.Client) (repository.MemberDatabase, error) {
    ctx := context.Background()
    // Verify that we can communicate and authenticate with the datastore service.
    t, err := client.NewTransaction(ctx)
    if err != nil {
        return nil, fmt.Errorf("datastoredb: could not connect: %v", err)
    }
    if err := t.Rollback(); err != nil {
        return nil, fmt.Errorf("datastoredb: could not connect: %v", err)
    }
    return &datastoreDB{
        client: client,
    }, nil
}

Datastoreでの操作作成

インターフェース定義

とりあえず登録と一覧表示のメソッドを宣言
プロパティはなんでもいいからIDとNameを定義

internal/repository/member.go

// Member hold metadata about a Membe<ffff>r
type Member struct {
    ID      int64
    Name    string
}

// MemberDatabase provides access to a database od member
type MemberDatabase interface {
    // ListMembers returns member list
    ListMembers() ([]*Member, error)
    // AddMember saves a given member, assigning it a new ID
    AddMember(member *Member) (id int64, err error)
}

実装

client.Putとかclient.GetAllすると登録したり一覧取得できるらしい

internal/db/datastore.go

func (db *datastoreDB) AddMember(member *repository.Member) (id int64, err error){
    ctx := context.Background()
    k := datastore.IncompleteKey("Member", nil)
    k, err = db.client.Put(ctx, k, member)
    if err != nil {
        return 0, fmt.Errorf("datastoredb: could not put Member: %v", err)
    }
    return k.ID, nil
}

func (db *datastoreDB) ListMembers() ([]*repository.Member, error) {
    ctx := context.Background()
    members := make([]*repository.Member, 0)
    q := datastore.NewQuery("Member").
        Order("Name")

    keys, err := db.client.GetAll(ctx, q, &members)

    if err != nil {
        return nil, fmt.Errorf("datastoredb: could not list books: %v", err)
    }

    for i, k := range keys {
        members[i].ID = k.ID
    }

    return members, nil
}

DB操作の呼び出し

Goでルーティング(gorilla/mux) で作成したルーティングに今回の処理を実装してみる。

jsonで受け取ってjsonで返却するAPIのイメージ。とりあえず動けばいいや感。

func createHandler(w http.ResponseWriter, r *http.Request) *appError {
    // json decode
    decoder := json.NewDecoder(r.Body)
    var memberRequest endpoint.MemberRequest
    err := decoder.Decode(&memberRequest)
    if err != nil {
        return appErrorFormat(err, "decode error: %s", err)
    }

    // object convert
    member, err := memberFromJson(&memberRequest)
    if err != nil {
        return appErrorFormat(err, "convert error: %s", err)
    }

    // save member to db
    id, err := internal.DB.AddMember(member)
    if err != nil {
        return appErrorFormat(err, "add db error: %s", err)
    }

    // create response
    response, jsonError := json.Marshal(member)
    if jsonError != nil {
        return appErrorFormat(jsonError, "%s", jsonError)
    }
    _, writeError := w.Write(response)
    if writeError != nil {
        return appErrorFormat(writeError, "%s", writeError)
    }
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Location", "/members/"+string(id))
    w.WriteHeader(201)
    return nil
}

func listHandler(w http.ResponseWriter, r *http.Request) *appError {
    members, err := internal.DB.ListMembers()
    if err != nil {
        return appErrorFormat(err, "%s", err)
    }

    response, jsonError := json.Marshal(members)
    if jsonError != nil {
        return appErrorFormat(jsonError, "%s", jsonError)
    }

    _, writeError := w.Write(response)
    if writeError != nil {
        return appErrorFormat(writeError, "%s", writeError)
    }
    w.Header().Set("Content-Type", "application/json")
    return nil
}

検証

ローカル環境起動

以下コマンド実行

go run cmd/ucwork/main.go

datastoreに登録

POST通信で登録してみると何やら返却された。
きっとうまくいってそう。本当は201だろとかあるけどdatastoreには登録できたっぽい

curl -v -X POST -H "Content-Type: application/json" -d '{"Name":"ucwork"}' http://localhost:8080/members                      +[add_datastore_connection#6]
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /members HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 17
>
* upload completely sent off: 17 out of 17 bytes
< HTTP/1.1 200 OK
< Date: Wed, 25 Sep 2019 08:04:14 GMT
< Content-Length: 24
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
{"ID":0,"Name":"ucwork"}%

datastoreから一覧取得

GETで一覧取得してみるとさっきの取れた!

curl -v http://localhost:8080/members                                                                                         +[add_datastore_connection#6]
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /members HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Wed, 25 Sep 2019 08:05:56 GMT
< Content-Length: 41
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
[{"ID":5631986051842048,"Name":"ucwork"}]%

コンソール上

コンソール上でも登録されてる!!
f:id:shintaro-0112:20190925171502p:plain

というか、ローカルから叩いても本物に登録されるのか...

gcloud beta emulators datastore start使うとローカルで仮想的なdatastoreできるらしい。
とりあえず一通り触ってみるが目的なんで、今度ちゃんと触るときにローカルエミュレートしよー

まとめ

GCPというよりgoの学習に時間割かれてる気がする・・・
とりあえずDatastore触ったから次はCloudSQLかな。

Goでルーティング(gorilla/mux)

概要

GAEのgoランタイムでDatastoreに繋いでCRUDしてみたい。
その前にgo言語自体全然書いてないのでルーティングをザクっと実装してみる。
その後でDatastoreへの接続を書いていく。ちょっとずつ進めてこう。

構築

リクエストのルーティング処理

ハンドラーの登録

とりあえず、GET,POST,PUT,DELETEあたりをやりたい
前回のHello, Worldではnet/httpでリクエストをさばいてたけど、gorilla/muxなるものが人気らしい。 それで書いてみる。

ListenAndServeメソッドの第2引数にルーティング情報を記載したハンドラーを渡してみる。

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
        log.Printf("Defaulting to port %s", port)
    }
    log.Printf("Listening on port %s", port)
    log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), registerHandlers()))
}

ルーティングの定義

メソッド、パスに準じた処理を記載。

func registerHandlers() *mux.Router {
    router := mux.NewRouter()
    router.Methods("GET").Path("/members").Handler(appHandler(listHandler))
    router.Methods("POST").Path("/members").Handler(appHandler(createHandler))
    router.Methods("PUT").Path("/members/{id:[0-9]+}").Handler(appHandler(updateHandler))
    router.Methods("DELETE").Path("/members/{id:[0-9]+}").Handler(appHandler(deleteHandler))
    return router
}

エラー返却時の記述を簡略化

Error handling and Go - The Go Blogに書かれているように自身の定義したエラー(appError)を返却するappHandlerをかましてハンドラー登録すると、http.ResponseWriterに毎度エラーを書き込まなくても、エラーを返却するだけでよろしくエラー返却してくれて冗長性を無くしてくれるらしい!

type appError struct {
    Code    int
    Message string
    Error   error
}

type appHandler func(w http.ResponseWriter, r *http.Request) *appError

ただし、ただエラー返却するだけではnet/httpが理解してくれないので
ServeHTTPメソッドを作成して理解させる必要があるらしい。  

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil {
        log.Printf("Handler error: status code: %d, message: %s, underlying err: %#v",
            e.Code, e.Message, e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

ルーティング毎の処理(一覧表示)

あとはルーティングにマッチした際の処理を記載する。
本当はDatastoreからデータ取りたいが、とりあえず適当に会員一覧を返却させとく。

type Member struct {
    Name string
}

type Members []Member

func listHandler(w http.ResponseWriter, r *http.Request) *appError {
    response, jsonError := json.Marshal(Members{
        Member{
            Name: "Name1",
        },
        Member{
            Name: "Name2",
        },
    })
    if jsonError != nil {
        return appErrorFormat(jsonError, "%s", jsonError)
    }

    w.Header().Set("Content-Type", "application/json")
    _, writeError := w.Write(response)
    if writeError != nil {
        return appErrorFormat(writeError, "%s", writeError)
    }
    return nil
}

実行

あとはコマンド叩いてローカル環境で色々アクセスしてみる。

# go run main.go
go build
./ucwork-go

GETでhttp://localhost:8080/membersにアクセスしたらこんなん返ってきた!!

[
    {
        "Name": "Name1"
    },
    {
        "Name": "Name2"
    }
]

まとめ

GCPのサービスザクっと触ってみるのが目的なのにまた脱線してしまった。。。
まぁせっかくならGoogleにロックインされにかかるためにgoも学習しとこー