Amazon ECSを試したメモ

雰囲気をつかむこと・動かすことを優先に考えてポチポチ。

方針

  • Nginx + PHP-FPM
  • GitHubリポジトリのmasterに変更があったら自動デプロイ
    • pull-req mergeでデプロイ
    • pull-req revertでロールバック
  • デプロイ時にシングルタスク(e.g. database migrate)を実行
  • ログはCloudWatch
  • なんとなく本番っぽいフロー

GitHubリポジトリのmasterを更新して、CodePipelineから自動デプロイされたら成功 :)

ソースコード

github.com

やったことメモ

リポジトリ作成

Elastic Container Service > Amazon ECR > リポジトリ
ポチポチするだけで、特に悩むことはない。
イメージの追加は、あとでCode Pipelineで行う。

ロググループ作成

CloudWatch > ログ > アクション
あとでコンテナで使用する。
ロググループ名はecstestにした。

ECSタスク定義作成

Elastic Container Service > Amazon ECS > タスク定義

ラベル
タスク定義名 ecstest
タスクロール なし
ネットワークモード default
タスクメモリ(MiB) 256
タスクCPU(単位) 256

コンテナの追加

PHPの設定。表に立たないのでポートマッピングはしない。 ヘルスチェックはいったんなし。
ログはawslogsを使用する。ロググループ名は前述で作成したものを指定する。

ラベル
コンテナ名 ecstest-php
イメージ XXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/ecstest-php:latest
メモリ制限(MB) ハード制限, 128
ログ設定 awslogs, awslogs-group:ecstest, awslogs-region:ap-northeast-1, awslogs-stream-prefix:php

Nginxの設定。ポートマッピングをする。
ecstest-phpをリンクする。aliasはNginxの設定に合わせてphpfpm。
ログはawslogsを使用する。prefixだけPHPの設定と違うものにする。

ラベル
コンテナ名 ecstest-nginx
イメージ XXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/ecstest-nginx:latest
メモリ制限(MB) ハード制限, 128
ポートマッピング ホストポート:なし, コンテナポート:80, tcp
リンク ecstest-php:phpfpm
ログ設定 awslogs, awslogs-group:ecstest, awslogs-region:ap-northeast-1, awslogs-stream-prefix:nginx

ロードバランサー作成

EC2 > ロードバランサー
ECSサービスで使用する。

Application Load Balancerを選択。

ラベル
名前 ecstest
スキーム インターネット向け
IPアドレスタイプ ipv4
リスナー プロトコル:HTTP, ポート:80

VPC, アベイラビリティゾーン, セキュリティグループはよしなに。

ラベル
ターゲットグループ 新しいターゲットグループ
名前 ecstest
以下略 デフォルト値

ターゲットの登録では何も選択しない。

ECSクラスター作成

Elastic Container Service > Amazon ECS > クラスター

EC2 Linux + ネットワーキングを選択。

ラベル
クラスター名 ecstest
プロビジョニングモデル オンデマンドインスタンス
EC2 インスタンスタイプ t2.micro
インスタンス数 1
EBSストレージ(GiB) 22
キーペア なし
コンテナインスタンスIAMロール ecsInstanceRole(default)

VPC, サブネット, セキュリティグループはよしなに。

サービス作成

ecstestクラスター > サービス
リポジトリにイメージを追加していないので、タスクは立ち上がらないけど、いったん気にしない。

ステップ1: サービスの設定

最大率200は指定タスク数の200%まで許容する。
うっかりインスタンスのメモリ上限を超えるとデプロイに失敗したりする。

ラベル
タスク定義 ecstest:1
クラスター ecstest
サービス名 ecstest
タスク数 1
最小ヘルス率 50
最大率 200
配置テンプレート AZ バランススプレッド

ステップ2: ネットワーク構成

ラベル
ELBタイプ Application Load Balancer
サービス用IAMロールの選択 AWSServiceRoleForECS
ELB名 ecstest

コンテナの選択からecstest-nginx:0:80を追加。

ラベル
リスナーポート 80:HTTP
ターゲットグループ名 ecstest

ステップ3: Auto Scaling

デフォルトのまま。

CodePipeline作成

AWS CodePipeline

ステップ1: 名前

ラベル
パイプライン名 ecstest

ステップ2: ソース

ラベル
ソースプロバイダ GitHub
リポジトリ utahta/php-ecs-sample
ブランチ master

ステップ3: ビルド

CodeBuildプロジェクトを作成して保存する。

ラベル
ビルドプロバイダ AWS CodeBuild
プロジェクトの設定 新しいビルドプロジェクトを作成
プロジェクト名 ecstest
環境の設定
環境イメージ AWS CodeBuildマネージド型イメージの使用
OS Ubuntu
ランタイム Docker
バージョン aws/codebuild/docker:17.09.0
ビルド仕様 ソースコードのルートディレクトリのbuildspec.ymlを使用
キャッシュ_
タイプ なし
CodeBuildサービスロール アカウントで新しいロールを作成します
ロール名 code-build-ecstest-service-role

VPC, サブネット, セキュリティグループはよしなに。

アドバンストの環境変数を設定。buildspec.ymlで使用する。

Key Value
AWS_ACCOUNT_ID XXXXXXXXX
AWS_DEFAULT_REGION ap-northeast-1

ステップ4: デプロイ

ラベル
デプロイプロバイダ Amazon ECS
クラスター名 ecstest
サービス名 ecstest

ステップ5: サービスロール

ラベル
ロール名 AWS-CodePipeline-Service

IAM編集

IAM > ロール > code-build-ecstest-service-role

AmazonEC2ContainerRegistryFullAccessをアタッチ。
イメージの追加に必要。

AmazonEC2ContainerServiceFullAccessをアタッチ。
ECSのタスク実行に必要。本当はタスク実行のみ許可するポリシーを作成してアタッチした方が良いけど省略。

ECSタスク定義作成(シングルタスク)

Elastic Container Service > Amazon ECS > タスク定義

デプロイするときに合わせて実行するタスク(仮)の作成。
ここではdatabase migrateを想定したタスクを作る。
メモリ・CPUは適当。

ラベル
タスク定義名 ecstest-migrate
タスクロール なし
ネットワークモード default
タスクメモリ(MiB) 256
タスクCPU(単位) 256

コンテナの追加

コマンドの設定。ヘルスチェックはなし。ログはawslogsを使用する。
コマンドにmigrateコマンド(仮)を指定する。

ラベル
コンテナ名 ecstest-migrate
イメージ XXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/ecstest-php:latest
メモリ制限(MB) ハード制限, 128
コマンド php,worker.php
ログ設定 awslogs, awslogs-group:ecstest, awslogs-region:ap-northeast-1, awslogs-stream-prefix:migrate

おわり

ECSよりもNginxの設定でハマった。

参考

Amazon ECS
AWS CodePipeline
Twelve-Factor App

Neovimに移行したしせっかくなのでGoでプラグインを書いてみた

github.com

Google翻訳APIを使ってテキストを翻訳するプラグイン。
作ったあとにVim & Go界隈で著名なhaya14busaさんがほぼ同じプラグインを作ってることに気づきました。

trans_nvim_previe_log

neovim/go-clientを使っていてNeovimでしか動きません。
(python-clientの方だとNeovim/Vim8で手軽に両立するやり方がある模様)
プラグインのインストールにGoが必要です。

サンプルが手薄だったり実装の参考になりそうな他のプラグインがzchee/nvim-goくらいしかみつからなかった(とはいえめっちゃ参考になった)ので、わりとneovim/go-clientのコードとにらめっこしながら書きました。

苦労したところは、previewってどうやってつくるんだろう?とかvim的な知識の足りなさ。一体何をどうするのが正解なのか未だによく分からんです。
ちなみに自分の行き着いたpreviewの作り方は次のとおり。

trans.go#L175:L203

  1. silent pclose
    • previewをとりあえず閉じる
  2. silent pedit +set noswapfile buftype=nofile translated
    • ファイルなしでpreviewを開く
  3. wincmd P
    • previewに移動する
  4. neovim/go-clientのメソッドを使って文字列をクリアして書き込む
  5. wincmd p
    • previewから抜ける

果たしてこれで合っているのか...?という気持ちですが、今のところ動いてるので良し。もしダメだったらpull-reqください。
あと誰かこの泥臭い部分を隠蔽するいい感じのラッパー作ってください!

おわり

Goだけで完結できるわけではない(vimのコマンドを直接叩いたりする)
それでもvim scriptを書くより敷居は低いし楽しい。

twitchtv/twirp を試した

github.com

gRPCのようなフレームワークで、違いはHTTP 1.1で動くこととJSONをサポートしてること。

インストール

protoc-gen-twirpの他にprotocとprotoc-gen-goも必要。

$ go get github.com/twitchtv/twirp/protoc-gen-twirp

$ brew install protobuf
$ go get github.com/golang/protobuf/protoc-gen-go

protoファイル

まずprotoファイルを書く。

$ mkdir proto
$ vi proto/hello.proto
syntax = "proto3";
package utahta.twirp.example.helloworld;
option go_package = "helloworld";

service HellowWorld {
  rpc Hello(HelloReq) returns (HelloResp);
}

message HelloReq {
  string subject = 1;
}

message HelloResp {
  string test = 1;
}

protocする

protoファイルからgoファイルをつくる。

$ mkdir helloworld
$ protoc --proto_path=./proto --twirp_out=./helloworld --go_out=./helloworld ./proto/hello.proto
$ ls helloworld
hello.pb.go    hello.twirp.go

サーバを書く

$ mkdir server
$ vi server/main.go
package main

import (
    "context"
    "fmt"
    "net/http"

    "github.com/utahta/twirp-example/helloworld"
)

type Server struct{}

func (s *Server) Hello(ctx context.Context, req *helloworld.HelloReq) (*helloworld.HelloResp, error) {
    return &helloworld.HelloResp{
        Test: fmt.Sprintf("Subject: %s", req.Subject),
    }, nil
}

func main() {
    s := &Server{}
    handler := helloworld.NewHellowWorldServer(s, nil)
    http.ListenAndServe(":8881", handler)
}

クライアントを書く

$ mkdir client
$ vi client/main.go
package main

import (
    "context"
    "fmt"
    "net/http"

    "github.com/utahta/twirp-example/helloworld"
)

func main() {
    c := helloworld.NewHellowWorldProtobufClient("http://localhost:8881", http.DefaultClient)
    resp, err := c.Hello(context.Background(), &helloworld.HelloReq{Subject: "hello twirp"})
    if err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n", resp)
}

最終的に次のようなディレクトリ構成になった。

.
├── client
│   └── main.go
├── helloworld
│   ├── hello.pb.go
│   └── hello.twirp.go
├── proto
│   └── hello.proto
└── server
    └── main.go

実行する

まずサーバを実行。

$ go run server/main.go

次にクライアントを実行する。 すると結果が返ってくる。

$ go run client/main.go
&helloworld.HelloResp{Test:"Subject: hello twirp"}

curlで実行する

JSONに対応しているのでcurlを使ってさくっとリクエストできる。

$ curl -H 'Content-Type:application/json' -X POST -d '{"subject":"hello curl"}' "http://127.0.0.1:8881/twirp/utahta.twirp.example.helloworld.HellowWorld/Hello"

雑感

シュッと書いたらProtocolBuffersとJSONで会話できるようになってすごい。便利。
学習コストの低さがなによりいい。