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に受験して何とか合格したので日記を書いておく
何か審査してるみたいだけど合格とは書いてある!
勉強法
以下3点に取り組み、最終的には問題系を2往復して頭に叩き込んだ
- Google公式模擬試験
- 無料で受けれる模擬試験
- これは確実に理解しておいた方がいい
- Coursera
- Architecting with Google Cloud Platform 日本語版
- Cloud OnBoard参加時にゲットした1ヶ月無料プランで実施
- 基本的に動画見てるだけ
- 問題系に関しては2周して100%解答できるようにした
- Cloud Architecture with GCP プロフェッショナル認定
- 7日間の無料学習期間でやり切る
- 問題系を2周して頭に叩き込む
- Architecting with Google Cloud Platform 日本語版
- Udemy
- Google Cloud - Professional Cloud Architect Practice Exams
- サイバーマンデー割引の時に1,200円で購入
- 50問×5セット=250問を2周ほど実施
- Google Cloud - Professional Cloud Architect Practice Exams
かかったコスト
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)周りの勉強も踏まえて自分で色々設定してみる!
こんな感じの物を作るのだ!
ほぼ無料でと書きましたが独自ドメイン(数円〜数十円)の料金がかかります
Cloud Identityの設定
フォルダ使うためには組織が必要
組織使うためにはGSuiteかCloud Identityが必要
GSuiteは最低でも1ユーザー/月680円するけど Cloud Identityは無料。無料!!こっち使おう
GCPコンソールにて「IAMと管理」->「IDと組織」->「お申し込み」を押下して ぽちぽちと適当にアカウント情報を入れていく
ドメインの所有権の確認
途中で「ドメインの所有権の確認」というステップがある ドメインが自分のものであることを証明する必要がある
独自ドメイン購入
とりあえずお名前.comで適当にドメインを買う
(今回関係ないけどWhois代行設定は初回購入時無料なので設定が吉)
無料サーバーの準備
今回は「ドメインの所有権の確認」の方法としてhtmlのmetaタグを用いた認証を行う。そのために、自身のドメインにhttpでアクセスできる環境が必要になる。
無料で独自ドメイン対応したwebサーバー構築調べたら XFREEなるサーバーが使えそうだったのでこれで行く。無料プランを申し込み。
独自ドメインの向き先を自身のサーバーに向ける
「お名前.com」のネームサーバーでAレコードを設定して 購入したドメインの向き先ipを無料で購入したサーバーに設定する必要がある。
まずはXFREE管理パネルのドメイン設定追加画面に行き
「Aレコード認証」のコンテンツIPをコピーする。
コピーしたIPアドレスを、お名前.comのDNS設定でAレコードとして設定する。
以下コマンドの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タグをコピーして保存
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の画面とか行くと権限エラーになる
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コンソールを見るとプロジェクトにアクセスできるようになってる!!
所感
フォルダを使った運用イメージがつかなかったので自分で実際に手を動かしてみた。
フォルダを適切に区切れば、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
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"}]%
コンソール上
コンソール上でも登録されてる!!
というか、ローカルから叩いても本物に登録されるのか...
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も学習しとこー