Amazon ECSを試したメモ
雰囲気をつかむこと・動かすことを優先に考えてポチポチ。
方針
- Nginx + PHP-FPM
- GitHubリポジトリのmasterに変更があったら自動デプロイ
- pull-req mergeでデプロイ
- pull-req revertでロールバック
- デプロイ時にシングルタスク(e.g. database migrate)を実行
- ログはCloudWatch
- なんとなく本番っぽいフロー
GitHubリポジトリのmasterを更新して、CodePipelineから自動デプロイされたら成功 :)
ソースコード
やったことメモ
リポジトリ作成
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の設定でハマった。
参考
Neovimに移行したしせっかくなのでGoでプラグインを書いてみた
Google翻訳APIを使ってテキストを翻訳するプラグイン。
作ったあとにVim & Go界隈で著名なhaya14busaさんがほぼ同じプラグインを作ってることに気づきました。
neovim/go-clientを使っていてNeovimでしか動きません。
(python-clientの方だとNeovim/Vim8で手軽に両立するやり方がある模様)
プラグインのインストールにGoが必要です。
サンプルが手薄だったり実装の参考になりそうな他のプラグインがzchee/nvim-goくらいしかみつからなかった(とはいえめっちゃ参考になった)ので、わりとneovim/go-clientのコードとにらめっこしながら書きました。
苦労したところは、previewってどうやってつくるんだろう?とかvim的な知識の足りなさ。一体何をどうするのが正解なのか未だによく分からんです。
ちなみに自分の行き着いたpreviewの作り方は次のとおり。
silent pclose
- previewをとりあえず閉じる
silent pedit +set noswapfile buftype=nofile translated
- ファイルなしでpreviewを開く
wincmd P
- previewに移動する
- neovim/go-clientのメソッドを使って文字列をクリアして書き込む
wincmd p
- previewから抜ける
果たしてこれで合っているのか...?という気持ちですが、今のところ動いてるので良し。もしダメだったらpull-reqください。
あと誰かこの泥臭い部分を隠蔽するいい感じのラッパー作ってください!
おわり
Goだけで完結できるわけではない(vimのコマンドを直接叩いたりする)
それでもvim scriptを書くより敷居は低いし楽しい。
twitchtv/twirp を試した
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で会話できるようになってすごい。便利。
学習コストの低さがなによりいい。