読者です 読者をやめる 読者になる 読者になる

試行錯誤のおと

日々の試行錯誤した結果です。失敗することが多い記録、それだけでっす!

私的 CloudFormation ベストプラクティス

最近 CloudFormation を触っていて、よくある初期構築のベストプラクティスについて意見がほしいので自分の考える CloudFormation の設計や使い方についての考えを書いた。

CloudFormation のメリット

CloudFormation を利用するためメリットはリソースの参照を簡単に記述できることと、べき等性の保証だと思っている。

以前、 EC2 インスタンスのプロビジョニングを Ansible で書いたことがある。

べき等性を確保したく、 サブネットの作成、 EBS のマウント、アンマウント、 EIP の付け替えなどの変更操作を Ansible だけで操作したい + その後のプロビジョニングの操作も Ansible にお任せしたいということを考えたときに、 Ansible の YAML ではなく以下のような Python のコードを書いた。

# ansible_playbook は data に playbook のデータを取ってタスクを実行する関数
data = [
    {
        'connection': 'local',
        'tasks': [
            {'ec2_vpc_subnet': {
                'vpc_id': 'vpc-xxxxxxxx',
                'state': 'present',
                'az': 'ap-northeast-1a',
                'cidr': '10.0.0.0/24',
                'resource_tags': {
                    'Name': 'jessie'
                }
            }, 'name': 'create subnet'}],
        'hosts': 'localhost'
    }
]
ansible_playbook(data=data)

subnets = get_all_subnets(filters={"vpcId": vpc_id,
                                     "tag:Name": "jessie"})
subnet_id = subnets[0].id

data = [
    {
        'connection': 'local',
        'tasks': [
            {
                'ec2':
                {
                    'assign_public_ip': 'yes',
                    'count': 1,
                    'ebs_optimized': False,
                    'image': 'ami-dbc0bcbc',
                    'instance_type': "t2.micro",
                    'key_name': 'kizkoh',
                    'monitoring': 'no',
                    'placement_group': False,
                    'private_ip': '10.0.0.4',
                    'tenancy': 'default',
                    'vpc_subnet_id': subnet_id,
                    'wait': 'yes'
                },
                'name': 'run jessie'
            }
        ],
        'hosts': 'localhost'
    }
]

ansible_playbook(data=data)

こんなコードを書いたのは Ansible の Playbook では YAML で EC2 API の実行の応答結果を扱えないためだ。 たとえば、インスタンスを作成した時に得られるインスタンス ID を引数に他の API を実行することができない。

CloudFormation はテンプレートと呼ばれるファイルに記述することでそれぞれのマネージドサービスのプロパティを記述することができる。 マネージドサービスのプロパティでは他のマネージドサービスのプロパティを参照できる。 先の Python のコードを CloudFormation のテンプレートに書き換えると以下のようになる。

AWSTemplateFormatVersion: 2010-09-09
Description: jessie stack
Resources:
  Subnet:
    Type: "AWS::EC2::Subnet"
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: 10.0.0.0/24
      Tags:
        - Key: Name
          Value: jessie
      VpcId:
        Ref: vpc-xxxxxxxx
  Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      EbsOptimized: False
      ImageId: ami-dbc0bcbc
      InstanceType: t2.micro
      KeyName: kizkoh
      Monitoring: False
      NetworkInterfaces: 
        - AssociatePublicIpAddress: "true"
          DeviceIndex: "0"
          SubnetId: 
            Ref: !Ref Subnet
      Tenancy: default

依存関係ごとにスタックを分けて、記述するのが個人的なベストプラクティスだと思っている。 特に依存関係がないものはマネージドサービスごとにスタックを分けていて、以下のようなフォルダ構成を取っている。

$ tree
.
├── cloudtrail
│   ├── parameters
│   │   ├── production.json
│   │   └── staging.json
│   └── template.yaml
├── iam
│   └── template.yaml
├── README.md
...

ステージング環境と本番環境で一部のパラメタを変えて API の引数を変えているのでオペレーションを環境間でそのまま適用できるのも CloudFormation の魅力だと思っている。 スタックの動作について環境間の差分としてパラメータ以外は変えることはできず If 文がサポートされていないのもそういった用途のマッチしていて、環境間で整合性をとるための方法としてユースケースを限定してくれているのがよいなという印象を持った。

リソースの参照以外にも最新の API への対応がすぐにされるあたりは公式ならではだと思う。 Terraform などのサードパーティ製ツールを使うとどうしても API への対応が遅れることがあるので、公式がサポートしている恩恵も大きい。 CloudFormer を使えば YAML を書かなくてもテンプレート化できるので、深くドキュメントを読み込む必要がないところもよいと感じた。

テンプレートフォーマットの選択

テンプレートフォーマットに YAML がサポートされるまではテンプレートは JSON で記述するか、サードパーティ製モジュールの SparkleFormation, kumogata, CoffeeFormation 等を用いて独自の DSL で記述するといった選択肢があった。 これらはナンセンスだと思っていて、 JSON を使って記述するとフォーマットとしての可読性はそこそこ高いのだけどコメントが書けない致命的な問題、サードパーティ製モジュールの DSL を利用するとサードパーティモジュールへの依存や、公式ドキュメントのサンプルを参照しにくい問題につまずいた。 YAML がサポートされるようになってからはこれらの問題が解決されて、 YAML が選択肢として良いと思っている。

しかし、 YAML を採用するとなると新たに考慮すべき点がある。例えば、ポリシードキュメントの記法を考慮しないといけない。 IAM グループを作成するときのテンプレートを、すべて YAML で記述すると以下のようになる。

...
  Groupadmin:
    Type: "AWS::IAM::Group"
    Properties:
      GroupName: admin
      Policies:
        - PolicyName: AllAllow
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action: "*"
                Resource: "*"
...

しかし、ポリシードキュメント部分は以下のようにして JSON (文字列扱い) で書くことができる。

...
  Groupadmin:
    Type: "AWS::IAM::Group"
    Properties:
      GroupName: admin
      Policies:
        - PolicyName: AllAllow
          PolicyDocument: |
            {
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": "*",
                  "Resource": "*"
                }
              ]
            }
...

どちらが見やすいかという議論ではなくて、ポリシードキュメントは AWS のシステム上では JSON で記述される。 そのため、ポリシードキュメントのドキュメントを見ても、 JSON のサンプルはあるものの YAML のサンプルはない。 また、変更したい時にマネジメントコンソール上で validate して確認した後にテンプレートに貼り付けることで Stack 更新時のエラーにハマることなく更新できる。 なので、自分はポリシードキュメント部分に関しては JSON で記述するようにしている。

どこまでを CloudFormation で扱うか

今回、 CloudFormation のテンプレートを書いていて困ったのは一部の API が扱えないこと。 例えば、 EC2 の KeyPair の登録や CloudFormation 以外で作成したリソースのプロパティの取得ができない。

EC2 の KeyPair の登録は AWS アカウントごとあるいはリージョンごとに繰り返す作業なので、 CloudFormation にまとめて定型化したい。 特に CloudFormation が API をサポートしてくれない理由はないと思うだけど、 CloudFormation が対応しない API に対しては無理に CloudFormation で対応するのではなく別のツールを使うのが良いと思う。 外部のリソースに関してはパラメータに埋め込むのがよいと思う。 テンプレートを動的に生成するような対応を取ると、リソースの依存解決が CloudFormation 外に依存することや環境ごとにテンプレートが異なる可能性 (この環境ではリソースを作成するが別の環境では作成しない等の If 文のような場合分け) が発生する。 テンプレートの生成次第でなにもかもできるようになってしまうと、結局のところ冒頭で書いた Ansible の Python コードと何ら変わりはないので、CloudFormation は CloudFormation がサポートする範囲内でオペレーションを収めたい。 AWS に CloudFormation の API のサポートを要望を出していって、それまでのつなぎとして使うには動的テンプレート生成はありかなと思うけれども、避けられるようなら避けたい課題かなと思う。

まとめ

CloudFormation の個人的に思っているベストプラクティス知見について書いた。もっとこうしたほうがいいとか、知見がほしい。

株式会社はてなにジョインしました!

1 月末付けで前職を退職し、 2 月から株式会社はてなで Web オペレーションエンジニアとして働いています。

転職の理由

前職も今と同じようにインフラレイヤの設計、構築、運用を担当していて、業務でやりたいことや技術的に取り組みたいことは自由にやらせてもらっていたり、上司や周りにいる方々も高い技術力を持っていて、かつ運用に対する改善等の考え方も技術的に前向きで働きやすく、特に大きな不満があったわけではありません。

一方で昨年の秋には新卒で入社して 3 年弱が経過しようとしていて、自身のキャリアについて振り返ると転職を考え始める時期かと考え始めていました。 前職は 1 社目ということもあり構築するサービスの内容やそれに応じて要求されているシステム構成の考え方、開発のスピード感、新しい技術に対するモチベーションが鈍ってしまうことが怖いという思いや、自社以外のそれらの様子をまだエンジニアとして若い間に見ておきたいという思いがありました。それと同時に、昨年から少しずつ自身のアウトプットを発信していく過程で、ブログや勉強会や OSS に成果物をアウトプットしている同年代の技術に明るいエンジニアと一緒に働きたいという希望もありました。

はてなを考えたきっかけ

はてなを転職先として考えるに至ったのは今までお会いして話を聞いた方々の影響があります。

学生時代の出来事で、今ではもうはてなにはおられませんが、大学に来られた際に id:ninjinkun さんや id:htomine さんとはてなの技術に対する考え方、プロダクトに対するモチベーションといったカルチャーについて話したり、勉強会では id:r_kurain さんとフロントエンドの技術についての苦労や新しい取り組みについて楽しく話したのを今でも覚えています。

話を聞く中でカルチャーや技術に対するモチベーションに憧れ、はてなで働きたいと思っていたのですが、当時はアプリケーションエンジニアを目指していて、言語は Perl より Python が好きでした。 そういったところもあって、前職の会社で働くに至ったということもあるのですが、社会に出てインフラレイヤのエンジニアとして働き始めると言語や既存の技術に縛られることなく働くことが求められ、言語に対する苦手意識は薄まっていきました。

転機があったのは昨年で、勉強会や技術系のイベントに参加する中で id:y_uuki さんや id:Songmu さんと今のはてなのカルチャーや技術に対する思いを聞いて、以前に思い描いていたはてなと変わらないなと確信できたこと、そのタイミングで自身のキャリアと成長について考え始めたことが今回の転職につながっています。

これから

はてなで働き始めて 1 ヶ月が経ち、システムのポリシーや構成、そしてカルチャーにもなじんできたところで自分の転職当時の気持ちを整理してみました。 今までお会いしたいろいろな方の影響を受けて今はてなで働いているんだなと考えるとなかなか感慨深いです。

今ではミドルウェアの構成やネットワークの運用ポリシーについて技術に尖ったメンバと真剣に議論ができ、一人のエンジニアとしてはてなで働くことができて嬉しく思っています。

つらつらと書きましたがこれからもよろしくお願いします!

ISUCON6 本戦の Server-Sent Events を Rust で!

この記事は Rust Advent Calendar 2016 - Qiita の 20 日目の記事です。(3 日ぐらい遅れてしまいました。スミマセン><)

ISUCON6 本戦 Rust 実装

さて、以前に ISUCON6 で優勝したことを記事に書いたのですが、自分はメンバの中で実装が遅い & プログラミングよりインフラの方が得意ということで実際の競技では肝心のコードは触ってないです。

kizkoh.hatenablog.com

そこで、今回は復習と早くも来年の ISUCON7 (未定) に備え、練習も兼ねてアプリのチューニングに挑戦することにしました。 Go でチューニングに挑戦してもよかったのですが、今回は Rust Advent Calendar 2016 のネタと Rust でチューニングしていくためのスタートラインとして、 Rust 実装を用意することにしました。

リポジトリは以下になります。

github.com

webapp/rust に Rust の実装があります。動作を確認している環境は以下の通りです。

$ rustc --version
rustc 1.15.0-nightly (ac635aa95 2016-11-18)
$ cargo --version
cargo 0.13.0-nightly (806e3c3 2016-10-26)

# Linux でのみビルドと動作を確認しています。

ISUCON6 本戦の問題といえば、お題はお絵かきアプリでフロントエンドの NodeJS + React のバックエンドにアプリサーバがあり、フロントエンドのリクエストに対してバックエンドが Json を返すというモダンな構成の問題でした。

isucon.net

お絵かきの過程をほぼリアルタイムに共有するために、フロントエンドはバックエンドからの Server-Sent Events を受け取ってブラウザに応答を返します。

つまり、 Rust で Server-Sent Events を扱う必要があるのですが、実装について設計を考慮すべき箇所やハマりどころがあったため、今回取り上げて解説します。 (その他もハマりどころがあったのですが、コードから読み取れると思いますので、特段取り上げません)

Server-Sent Events の実装

Rust 実装を始める際、 WAF の選定から行いましたが、今回は軽量であるということと Hyper に強く依存していることから Hyper の API を扱いやすいという観点で、 nickel.rs を採用しました。

github.com

Server-Sent Events を実装しているエンドポイントは get_api_stream_rooms_id() 関数になります。

624 fn get_api_stream_rooms_id<'mw>(req: &mut Request, mut res: Response<'mw>) -> MiddlewareResult<'mw> {

https://github.com/kizkoh/isucon6-final/commit/d76b3c5d09035c0384fd43052520d566e8e6534c#diff-792cca0794d088b75ffbaef6e6aa8715R624

Server-Sent Events ではストリームに対して書き込みを行います。

nickel.rs では Write トレイトを実装する Response<'a, D, Streaming> から write(&mut self, buf: &[u8]) メソッドを呼び出すことで、 Hyper を経由してストリームに書き込みを行うことができます。 Response<'a, D, Streaming> を得るには Response<'a, D, Fresh> から start(self) メソッドを呼び出して、型を変更します。

694     let mut streaming = match res.start() {
695         Ok(streaming) => streaming,
696         Err(_e) => {
697            let _result = writeln!(&mut std::io::stderr(), "nickel streaming start error");
698            exit(1);
699         }
700     };

https://github.com/kizkoh/isucon6-final/commit/d76b3c5d09035c0384fd43052520d566e8e6534c#diff-792cca0794d088b75ffbaef6e6aa8715R694

しかし、この時点で res の move が発生するため、以降 res を参照することができなくなります。 つまり、ストリームには書き込めるのですが res を使って HTTP レスポンスを返すことができません。 そのため、他の実装ではエラー処理の際に HTTP レスポンスを返す箇所で Rust 実装では HTTP レスポンスを返すことができていません。 この問題は今後の課題です。

ストリームに書き込めるようになったところで、実際の Server-Sent Events 実装 について触れていきます。 Server-Sent Events の実装に利用するメソッドは write(&mut self, buf: &[u8]) メソッドと flush(&mut self) メソッドになります。 これらのメソッドをまとめ、エラー処理を含んだ処理が print_and_flush<'mw, T: AsRef<str>>(mut streaming: Response<'mw, (), Streaming>, msg: T) 関数になります。 なお、write, flush はいずれの処理も move するため、 streaming を return することで所有権の返却を行い継続してストリームに対して書き込みできるようにしています。

704     streaming = print_and_flush(streaming, msg).ok().unwrap();

https://github.com/kizkoh/isucon6-final/commit/d76b3c5d09035c0384fd43052520d566e8e6534c#diff-792cca0794d088b75ffbaef6e6aa8715R704

最後にこのエンドポイントでは以下のようにして bail() を呼び出して、ストリームを中断しています。

777     streaming.bail("")
778     // Ok(Action::Halt(streaming))

https://github.com/kizkoh/isucon6-final/commit/d76b3c5d09035c0384fd43052520d566e8e6534c#diff-792cca0794d088b75ffbaef6e6aa8715R777

Ok(Action::Halt(streaming)) を return することで、ストリームを終了することもできるのですが、これではストリームを再開する時に Last-Event-ID がリクエストのヘッダに付与されないため、 bail() を呼び出して中断しています。

ここまでが Server-Sent Events の実装の一連の流れです。

まとめ

この記事では Rust で Server-Sent Events を扱う方法について触れました。

内容としては前節でまとまっているので、Rust 実装について少し触れておきたいと思います。 今回やったこととしては他言語の実装を Rust 実装に置き換えるだけなのですが、所有権や Trait という概念がある以上、やはり他の一般的な言語間での移植以上には時間がかかりました。 しかし、これらの概念を理解するためには、もとのコードも洗練されておりとても良い課題だったかと思います。 Web アプリケーションを Rust に移植するなどや ISUCON6 の他言語の実装を参考に Rust を学習される方がおられましたら、自分のコードが役に立てば幸いです。

また、この記事ではパフォーマンスについて触れることはできませんでしたが、トップページのロードは Go 実装より Rust 実装の方が高速のようです。 簡単な計測としてキャッシュを無効にしてブラウザのプロファイリング結果だけ見たところ、 Go 実装で 7.5s 前後だったロード時間が Rust 実装だと 6.7s 前後に短縮される結果が得られました。

この結果を踏まえ次回以降の記事では、さらにチューニングやプロファイル過程を紹介できればと思っています。

この記事は Rust Advent Calendar 2016 - Qiita の 20 日目の記事として書かれました。 21 日目は 11Takanori さんで RustでCSVを操作する - Qiita です。