aereal.orgをGoogle Cloud Platformに移した w/Cloud DNS, Cloud Storage, Deployment Manager

aereal.org

長らくさくらのVPSホスティングしていたのだけれども運用がめんどうになったのでこれを機にGCPへ移行した。

登場人物は:

……の4つ。

Google Cloud Storage

GCP版S3みたいなオブジェクトストレージ。使い勝手はかなり似ていて、S3同様にウェブサイトとして公開する設定もある。

ここにindex.htmlとか画像を置いて後述するGoogle Cloud CDNのoriginにする。

Google Cloud Load Balancing

AWSでいうALB/NLBにあたるもの。ALB相当がHTTP(S) Load Balancingになる。
余談だけど日本語ドキュメントでは「HTTP負荷分散」と訳されていて、動名詞なのかプロダクト名なのかわかりにくい。

パスやホストに対してルールベースでbackendへリクエストを送ることができる。
使い込んでいないけれどALB/NLBと機能の差異はそんなにないようにみえる。

Google Cloud CDN

GCPが提供するCDNサービス。AWSでいうCloudFront.

invalidation APIがあったり、普通なかんじ。

Cloud CDNに限らないけれど全体的にGCPは安めな気がする。

Google Cloud Deployment Manager

AWSでいうCloud Formationみたいな構成管理するグッズ。

YAMLを使うのはCFnと同じだけれども、変数展開などはJinja2というAnsibleでも使われているテンプレートエンジンを採用している。
既存のよくできた技術を再利用する筋のよさは後発のGCPならではってかんじ。

リソースのプロパティ定義は既存のREST APIのリクエストパラメータと同じ名前・同じスキーマなのでそちらを参照せよというドキュメントになっており親切。
(例: REST Resource: targetHttpsProxies  |  Compute Engine  |  Google Cloud

gcloud deployment-manager types list で使えるリソース定義の一覧が得られるので、これでDeployment Managerで対応しているか確認できる。
前述の通りプロパティ定義はREST APIと一致しているので、このリソース定義名からREST APIのドキュメントを引けばよくて便利。

aereal.orgのデプロイ設定は GitHub - aereal/gcp-deployment-aereal.org に置いたのでご覧ください。

感想

GCPはぼちぼち触っていたけれどDeployment Managerはよく出来ているし筋がいい印象を持った。

会社ではAWSを使っているのでおもしろさ重視でGCPでもっと遊んでいきたい。

aws-cdkを見ている: policyの変換がすごい

aws-cdkを見ている。TypeScriptもしくはJavaAWSの構成管理ができる。CloudFormationのプログラマブルなやつと考えるとよい。

TypeScriptは癒しなので癒されました。今日は感動ポイントをご紹介。

policyの変換がすごい

CFnのなかでもS3のbucket policyを書くのは最もダルい作業のうちのひとつに数えられると思いますが、aws-cdkだとこう書ける:

    const bucket = new Bucket(...);
    const policy = new PolicyStatement(PolicyStatementEffect.Allow);
    policy.addAction("s3:GetObject");
    policy.addResource(this.arnForObjects("*"));
    policy.addPrincipal(new Anyone());
    bucket.addToResourcePolicy(policy);

これが cdk synth でこうなる:

    XxxBucketPolicy81AF88BF:
        Type: 'AWS::S3::BucketPolicy'
        Properties:
            Bucket:
                Ref: XxxBucketEDCC903C
            PolicyDocument:
                Statement:
                    -
                        Action: 's3:GetObject'
                        Effect: Allow
                        Principal: '*'
                        Resource:
                            'Fn::Join':
                                - ""
                                -
                                    -
                                        'Fn::GetAtt':
                                            - XxxBucketEDCC903C
                                            - Arn
                                    - /
                                    - '*'
                Version: '2012-10-17'

すごい!

JavaScriptのtemplate literalでよいかんじに変換されていそう。

よくあるワンソースで適当に変換エンジン作りましたというかんじではなく、ちゃんとTypeScript (ECMAScript) でネイティブにいいかんじに実装していこうという気概が感じられて、今後に期待が持てますね。

その他

cdk synth で出力するYAMLが、既存のCFnのYAMLと完全に互換を保てたら脱出も楽になるからいいな〜とおもっているのだけれど、そういうオプションはざっと見たかんじなさそう。

migration pathみたいなissueはあるのでなにも計画がないわけではなさそうだけど、あれだったらP-Rを送っていきたい。

tslintで有効になっているruleをpreset由来も含めて一覧する

const { resolve } = require('path');
const { Linter, Configuration: { findConfiguration } } = require('tslint');

const tslintJsonPath = resolve(__dirname, '../tslint.json');
const { results: config } = findConfiguration(tslintJsonPath);

const formatRule = (rule) => `${rule.ruleName}:${rule.ruleSeverity}`;

const linter = new Linter({ fix: false });
const rules = linter.getEnabledRules(config, false);
rules.map(r => formatRule(r)).sort().forEach(r => console.log(r));

presetをextendsするのが主流だけど、このpresetってデフォルトでどういう設定を提供するんだっけ? と調べはじめたりしてまあまあ不毛だし、そういうのとってくるAPIあるでしょと思い調べたらやはりあった。

内部向けっぽいAPIを無理矢理使っているわけでもないので、まあまあ安定しそう。

BPMと音価を入れるとディレイタイムを計算してくれるやつをReactで作った

Reactでちょっとした計算機を作ろうと思い、せっかくなのでStorybookを試してみることにした。

できたもの

GitHub - aereal/delay-time-calc

f:id:aereal:20180521112626p:plain

BPMとフィードバックさせたい音の音価を入れるとディレイタイムがミリ秒で出力される。

BPM 120で8分ディレイをかけたかったら250msec.にすればいい、みたいなことが手軽に計算できる。

いま触って気付いたけど付点8分のときの計算まちがっている気がする……。

使ったもの

create-react-appで雛形を作り、Storybookのドキュメントを読んでセットアップをした。
TypeScriptでstoryを書くのに一手間必要だったが、ものはできた。

変更内容は以下の通り。自動生成するコマンドの実行結果と手作業で加えた変更はコミットを分けているので、わりとわかりやすいのではないかと思う:
https://github.com/aereal/delay-time-calc/compare/94b844e12a73fd91b52d21a542b3566e19ab9151...eb106a6dda7e606a40de1453a56d4e642554ae14

TypeScriptでstoryを書くためのあれこれは typescriptでReact Storybookを試す。 - Qiita が参考になった。

感想

  • storybookは便利
    • コンポーネントがたくさんある・たくさん作っていくプロジェクトではカタログとして十分便利そう
    • storybookがあることで、ここで触るのに過不足ない
    • addonを入れるとMaterial-UIのスタイリイングをstorybook上でプレビューできるのめちゃくちゃ便利
  • とりあえずはじめるときはcreate-react-appを使うとよさそう
    • `npm run eject` できるしWebpackの設定をだいたいいいかんじにしてくれるし
    • まあこの先メンテしていけるかっていう話はあると思うが、少なくとも初速は出る・差分アップデートで習得できる、というところをよしとできるなら
    • scripts/test.jsとか、テスト実行をいじりたいと思ったときにメンテできるのかは既に不安ではあるが……

HTTP::Message#content($bytes)を呼んでもcontent-lengthは計算されない

HTTP::Request#content($bytes) もしくは HTTP::Request#add_content($bytes) を呼んでもcontent-lengthは自動で計算されず、0になる。

なのでそのままリクエストを送ると、ちゃんとcontent-lengthだけbodyを読む実装はボディが空だとみなすのでちゃんとcontent-lengthを計算しなければいけない。

Plackの実装に慣れていたのでびっくりした。

    my $req = HTTP::Request::Common::POST($url);
    my $json = JSON::XS::encode_json($payload);
    $req->content($json);
    $req->as_string;

HTTP::Request#as_string した結果:

POST http://example.com/api/issuance
Content-Length: 0
Content-Type: application/json

{"status":"success","expires_at":"2018-04-09T12:19:26+09:00"}

Content-Length: 0 になっていることが確認できる。

AWS CloudFormationメモ

  • aws cloudformation package はS3にfunctionとかいいかんじにアップロードして、そのARNが埋められてそのまま aws cloudformation deploy 実行可能なテンプレートを出力してくれる
    • そのパスを指定する引数が --output-template-path
  • cloudformation自体はS3になにをアップロードするのは不可欠ではない、lambda functionとか追加のファイル (local artifacts) が必要なときはpackageするとよい
  • lambdaとかつかわない場合はいきなりdeployできる

id:dekokun ++

参考:

Go: 外部ライブラリを使ったコードをいいかんじにDIしてテストする話

結論

  • 実装ではなくインターフェースに依存させる
  • Goのinterfaceは実装の明示が必要ないので便利

aws-sdk-goのスタブ

外部ライブラリの例としてaws-sdk-goを使ったこういったコード例を考える:

type CertificateFetcher struct {
  client *dynamodb.Client
}

func New(client *dynamodb.Client) *CertificateFetcher {
  return &CertificateFetcher{client: client}
}

func (f *CertificateFetcher) GetCertificate(domain string) (string, error) {
  resp, err := f.client.GetItem(...)
  if err != nil {
    return "", fmt.Errorf("...")
  }
  key := resp.Item["key"]
  if key == nli {
    return "", fmt.Errof("...")
  }
}

DynamoDBと通信させずにエラーのハンドリングや *dynamodb.GetItemOutput から値を取り出す処理などについてテストしたいが、そのためには *dynamodb.Client を差し替える必要がある。

実装を差し替える範囲が広すぎると実際に実行されるコードに対するカバレッジが低くなりテストの意味がなくなるので、適切な境界を見つけてそこで差し替えられるとよさそう。

実際にどうやるか、結論としては CertificateFetcher*dynamodb.Client ではなく必要なメソッドが定義されたinterfaceに依存させるとよい。

例:

type DynamoDBClient interface {
  GetItem(item *dynamodb.GetItemInput) (*dynamodb.GetItemOutput, error)
}

type CertificateFetcher struct {
  client DynamoDBClient
}

func New(client DynamoDBClient) *CertificateFetcher {
  return &CertificateFetcher{client: client}
}

func (f *CertificateFetcher) GetCertificate(domain string) (string, error) {
  resp, err := f.client.GetItem(...)
  if err != nil {
    return "", fmt.Errorf("...")
  }
  key := resp.Item["key"]
  if key == nli {
    return "", fmt.Errof("...")
  }
}

テストコード例:

func TestGetCertificate(t *testing.T) {
  stubClient := &StubDynamoDBClient{}
  fetcher := New(stubClient)
  // ...
}

type StubDynamoDBClient struct {}

func (c *StubDynamoDBClient) GetItem(item *dynamodb.GetItemInput) (*dynamodb.GetItemOuput, error) {
  // ...
}

Goのinterfaceは所与のメソッドを実装していれば特別なアノテーションをつけずとも構造体がinterfaceを実装しているとみなされるので、実装を変更できない外部ライブラリに対してアドホックにinterfaceを定義できるので便利。

このようにインターフェースに依存させてDIしやすくするというのは、たとえばScalaにおけるcake patternなど他にも例があります。

吉祥寺.pm mini #012 に参加した

kichijojipm.connpass.com

 

参加した。酔った勢いで参加登録し、新幹線と宿を予約をし、東京は吉祥寺に舞い降りました。

ライブと仕事でしか東京に行かないので西のほうに行くのは新鮮。

 

設計というテーマもさることながらしんぺいさん (id:nkgt_chkonk) を見に行くチャンスなので京都から参加登録した。

 

設計という抽象的な話題についてうまく他人とコミュニケートできる・それだけのプレゼンテーションができるというのはすごいことだと思っている。

参加して設計についてもちろん深い考察が得られたらいいと思っていたけれど、それ以上に自分が感じる「設計についていいかんじに他人にプレゼンテーションするために必要な能力は何なのか」について手掛かりが得たいと思っていた。

 

結論から言うと、それは得られた。簡単にまとめてみると:

  • 議論に参加する人間が解決できる (結論を出せる) 単位まで問題を分解する
  • 分解された子孫テーマ間の繋りを補強・説明する知識を持っている

……という能力およびそれの効果的な発揮に裏打ちされているのだと理解した。

具体的には:

  • 「良い設計とは何か」というテーマを、たとえば「DRY原則をいいかんじに適用する例とはなにか」くらいに分解する
  • さらに「DRY原則を適用していいかんじになっている」状態を「他の設計原則に違反せず達成されているか」くらいにさらに分解する

……という風に表れている。

「今あえてDRY原則に向き合う」というスライドはよかったです。

speakerdeck.com

こうして考えてみるとティーチングに対しても問題解決の原則を持ち込んでいるということであって、目新しさはない。ないが、当たり前のことを当たり前にやるということを実践できることは非凡ではないと思う。

 

京都からいきなりやってきたけど非常に楽しく参加させてもらえてありがたかったですし、個人的にはインターネットで見かけたおもしろそうな人を見に行くために出かけるのはインターネット原体験っぽさがあってよかった。

終電がなくなって吉祥寺から宿をとった京橋までタクシーに乗ったら「京橋? (舌打ち)」って言われたり「京橋ってどこですか?」って言われたり、深夜の急カーブ高速道路を120km/hで駆け抜けて恐怖を感じたり久しぶりにメチャクチャな目にあったけどそれもよかった、また吉祥寺いきます。

GoのデバッグはdelveとVisual Studio Codeが便利

delveとは

Go向けのデバッガで、ステップ実行とかブレイクした行のレキシカル変数が見えたりといった基本的な機能を提供しつつ、dlv debugコンパイルしつつ実行 (go run) したりgo(1)とほどよく統合されている。

後述するheadlessモードがあっていわゆるリモートデバッグが可能。

delveをmacOSにインストールする

公式のドキュメントではbrew install go-delve/delve/delve一発で済むように書かれているが実際はうまくいかなかった (´°̥̥̥̥̥̥̥̥ω°̥̥̥̥̥̥̥̥`)

==> Installing go-delve/delve/delve
==> Downloading https://github.com/derekparker/delve/archive/v1.0.0.tar.gz
==> Downloading from https://codeload.github.com/derekparker/delve/tar.gz/v1.0.0
######################################################################## 100.0%
security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.
==> Generating dlv-cert
==> openssl req -new -newkey rsa:2048 -x509 -days 3650 -nodes -config dlv-cert.cfg -extensions codesign_reqext -batch -out dlv-cert.cer -keyout dlv-cert.key
==> [SUDO] Installing dlv-cert as root
==> sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain dlv-cert.cer
Last 15 lines from /Users/aereal/Library/Logs/Homebrew/delve/02.sudo:
2018-02-28 11:11:35 +0900

sudo
security
add-trusted-cert
-d
-r
trustRoot
-k
/Library/Keychains/System.keychain
dlv-cert.cer


If reporting this issue please do so at (not Homebrew/brew or Homebrew/core):
https://github.com/go-delve/homebrew-delve/issues

These open issues may also help:
Upgrade to delve fails https://github.com/go-delve/homebrew-delve/issues/20

紹介されているissueを見てみると証明書を生成してインポートするスクリプトを手で実行したらうまくいったと書いてある。

実際に実行した手順はこういうかんじ:

✘>﹏<✘ < cd $(brew --cache)/
✘╹◡╹✘ < tar xzf delve-1.0.0.tar.gz
✘╹◡╹✘ < cd delve-1.0.0
✘╹◡╹✘ < bash ./scripts/gencert.sh
✘╹◡╹✘ < brew install go-delve/delve/delve

delveをインストールしたら sudo pkill taskgated を実行してtaskgatedを再起動する。
プロセス間通信を監視するデーモンらしく、bash ./scripts/gencert.shで生成・インポートした証明書を読み込みしなおす必要がある。

Visual Studio Codeとvscode-go

Visual Studio Codeは便利、GoやTypeScriptを書くとき最近はもっぱらこれ。

vscode-goというGoを書くときの各種便利サポートを追加してくれる拡張を入れる。

Debugging Go code using VS Code · Microsoft/vscode-go Wiki · GitHubが詳しい。

Visual Studio Codeでデバッグ

Visual Studio Codeでリポジトリを開きつつ Debug → Open Configurations でデバッガの設定 (JSON) を開く。

設定中の moderemote にする。remoteにすると別プロセスで既に起動しているデバッガーに接続する。

Debugging Go code using VS Codeにあるように、シェルで dlv --listen=:2345 --headless -- などと実行してデバッガーを起動する。

あとはVisual Studio Codeの Debug → Start Debuggingを押すとデバッガー開始できる。

gyazo.com

行頭を押すとブレイクポイントを設定できまるでIDEのよう〜。

ステップ実行もできるしレキシカル変数も見れて便利!!!!

Dockerのコンテナ内でapt-cache searchしてパッケージが見つからないとき

rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* してないか確認する。

コンテナのプロビジョニングを試行錯誤しているときに docker-compose run --rm app bash とかやりがちだけど、最初の方に apt-get update && apt-get install && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* しているとaptのキャッシュもリポジトリも消えるので、あるべきリポジトリが見つからないように見えることがある。

探すときはpackages.debian.orgを見るとかするとよい。

unzipは展開先のディレクトリ・ファイルが存在したら上書きしていいか聞いてくる

スクリプトで自動化したいときは困る。

そういうときは unzip -o /path/to/kure.zip のように -o オプションを使うとよい。

       -o     overwrite existing files without prompting.  This is a dangerous option, so use it with care.  (It is often used with  -f,  how-
              ever, and is the only way to overwrite directory EAs under OS/2.)

Google Calendarについてあなたが知るべきたった1つのこと

「予定あり」「予定なし」という概念があり、これが「予定あり」の予定が入っているときのみ「時間を探す」などでブロックされる。 言い換えると予定が「予定なし」だと他の人からは空いているように見える。

ちなみに「終日」にするとデフォルトが「予定なし」になる挙動 (現時点) のようなので留意しましょう。

「予定なし」の予定

https://i.gyazo.com/bb257c0c9e4ae4f39863bd75f8126a0b.png

「予定あり」の予定

https://i.gyazo.com/18f82f38ab7afa33bd1956110ebf522e.png

いかがでしたか?

「予定なし」の予定とかむずかしい日本語ですね。

LWP::Protocol::PSGI 0.10以降でLWP::UserAgentの:content_file引数が使えるようになっている

- Fix mirror() (haarg) #9

https://metacpan.org/changes/release/MIYAGAWA/LWP-Protocol-PSGI-0.10#L4

changesには `LWP::UserAgent#mirror` についてのみ言及があるけど、`get()` メソッドの `:content_file` 引数にも影響がある。

`mirror` メソッドも `:content_file` 引数も紆余曲折あり `LWP::Protocol#collect` を呼ぶことでレスポンスボディがファイルに保存される。

fix LWP::UA::mirror calls by haarg · Pull Request #9 · miyagawa/LWP-Protocol-PSGI

上記Pull Requestはつまるところ適切に `collect` を呼ぶようにする変更なので、 `mirror` も `:content_file` も適切に処理される、ということだった。

日記を書くWebアプリケーションを書いている

GitHub - aereal/nikki

昨年末からぼちぼち書いている。

モチベーション

  • いわゆるページを動的に生成するWebアプリケーションのパフォーマンスチューニングの練習台
    • サーバサイド、クライアントサイド双方に渡る改善を試みる余地がある
  • 諸々のWeb技術の遊び場
    • TypeScript
    • Google Cloud Platform
      • Kubernetes
    • HTTP/2
    • GraphQL
    • SPA = Single Page Application
  • 以上が目的としてではなく、手段として求められ高度にバランスされること
    • 普段から自分が使うアプリケーションであること
    • (技術採用など意思決定の練習)

……を目的に考えて日記を書くためのブログシステムを書きはじめた。

Web技術で遊び続ける定まった場所がないと、技術を使うための技術の域を出ない学習が続くなという問題意識から何か作ることにし、自分はWebでテキストを書くのが大好き人間なので、ブログシステムを作ることにした。

それでも自作するか
もしウェブプログラマなら自作すべきであると常に主張したい。

ブログシステムは自作すべきか? ブログサービス VS 自作システム | tech - 氾濫原

自分もこの意見に賛成で、まあブログシステムであることにこだわらなくてもいいけれど、趣味なら車輪の再発明など恐れずにとにかく遊び続けるのがいいと思う。

また完全無欠のASP型のサービスなどありえないわけで、その点では自作するとき自分が求めていてASPサービスにないものを作れるならば、それは完全な車輪の再発明とはいえないし、得るものは大きいと思う。

目的

パフォーマンスチューニングを目的に挙げているのは、仕事でもWebアプリケーションを作っているがそもそもパフォーマンスについて考える時間が少ないので知識も技術も身に付かない、けど求められるものではあるというギャップを埋めるためでもある。

そしてこうした興味・好奇心だけを目的とせず、使う自分自身の満足度・快適さに対する解決の手段として自分の興味がある技術を選べるような題材という点でも、ブログシステムは適している。

ソフトウェアエンジニア一般が趣味の開発でこうしたモチベーションでいるべきと考えているわけではなく、単に自分が趣味で作ることを考えるとき自分がよく使うもので、かつうまく動いていると自分が一番に喜ぶものでないと、継続して触りつづけるモチベーションが保たないだろうということを見越した、合理的な理由による。

技術

インフラはGoogle Cloud Platform、configuration managementを考えるのがだるいのでランタイムはコンテナに任せようということでk8s (GKE) を選んだ。
仕事でAWSを使っているので、では趣味ならGCPというくらい。あと料金計算が明快なのがいい、AWSリザーブインスタンスとか考えはじめると勘定がめんどう。
いまVPSに月におよそ1000円くらい払っているので、これを解約することを見越してプラス勉強代として月2000円まででやりくりしようと思っている。

パフォーマンスチューニングを深追いしたいということで、ポピュラーになったCDNは採用せず自分でリバースプロキシを運用することにした。

サーバサイドはRuby, DBはPostgreSQL, 記事の編集画面はTypeScriptで書いている。

インフラとフロントエンドが弱いと感じていて、そちらにリソースを割きたいのでサーバサイドは使い慣れたRubyにした。
Rubyはぱっと作ることができて安定感があるものの、型がなかったりもうちょっと冒険してもいいかなと思っている。
とはいえパッケージ管理とかでむやみにハマっていたくないし、予算上、ホストのメモリとかは潤沢にはできないのでScalaは選びづらいし、仕事でも趣味でもGoをよく書いており食傷気味なので、やはり落とし所として妥当かと思っている。

本当はサーバサイドはホットスポットではないのでGAEにしてもよかったんだけれども、予算の都合でStandard Environmentしか使えず、StandardだとJavaPythonかGoかPHPで、どれも気乗りしなかったのでGKEにした。

フロントエンドはそのうちSPAにしてみたい。
昔、仕事でSPAを触っていたけどhistory APIにバグのある地獄みたいな環境向けだったので、今のまともなブラウザと充実してきたと思われるライブラリ類で、リソース管理などの煩雑さが減ったのかそうでもないのか確かめてみたい。

いまは最低限、記事を書いてそれを配信するところまではできたのでGKEにデプロイしようとしている。
Kubernetesは耳慣れない概念がたくさん出てくるので難しいけど、GCPのドキュメントが充実しているのでチュートリアルを見様見真似でやりつつ要件・予算にあわせてあれこれ手探りで変えて試している。

おわり

VPSで遊ぶのも楽しいけれど、IaaS/PaaSが充実してきたことで予算の範囲内でマネージドサービスを利用し趣味の開発のボトルネックを解消することができそうでなかなか楽しい。
クラウド事業者にロックインされた範囲の中では、マネージドサービスから自前運用に切り換えることもできるわけなので、そういった点では上位互換といえそう。