AWS
ElasticBeanstalk

新規開発におけるElastic Beanstalkという選択

この記事は DMM.com #1 Advent Calendar 2017 の9日目の記事です。
前日の記事は @anbhts さんの 社内勉強会を運営してみてわかった勉強会成功の3つのコツ! でした。

下記がヘイシャのAdvent Calendarです。
https://qiita.com/advent-calendar/2017/dmm
https://qiita.com/advent-calendar/2017/dmm2

tl;dr

  • 新規開発のプロジェクトへJOIN
  • 初めてElastic Beanstalk(以降EB)を採用した
  • 慣れるとすごい楽だぞ

まえがき

8月からとある新規サービスの開発に携わっております。
AWSを利用する事は決まっていて、そこから先の戦略をゼロから考えていきました。
最終的にはEBに落ち着いたのですが、その選択の経緯や苦労を書いていきます。

ちなみに私はEBの利用経験はありません。先が思いやられます。

スクラムチーム

イカれた開発チームのメンバーを紹介するぜ!

  • 社内最強のスクラムマスター
  • お菓子神社の管理人である技巧派デザイナー
  • 新卒でとても活躍してるフロントエンドエンジニア
  • 新卒でとても活躍してるサーバサイドエンジニア
  • 仮想通貨と共に生きるphpの神
  • フロントからインフラまでなんでもできるエンジニア
  • 写真撮影が本業のインフラエンジニア(わたし)

各々が得意分野を持ちつつ、他の分野にも挑戦できるとてもいい感じのチームだと思います。
新卒が活躍できているチームってのが非常にポイント高いです。

プラットフォームと言語選定

インフラ

DBはRDSを使うとして、Webサーバどうしようね?っていう話からしていきました。
選択肢としては通常のEC2(+プロビジョニングツール)、EB、ECS(コンテナ)が挙がりました。

名前 メリット デメリット
EC2 とにかくスタンダード! 自前で諸々の仕組みを用意するのダルい・・・
EB 開発者はデプロイするだけ! カスタマイズが面倒・・・
ECS 開発環境を(ほぼ)そのままデプロイ! 本番環境でちゃんと運用できるん・・・?

開発環境にDockerを使用している事もあり、みんなECSに興味津々でした。
ただ、コンテナ環境を本番運用した経験が無かったのと、チーム内にEB経験者が2人いたのでそちらを採用しました。
EBはコードをcliでデプロイするだけなのでとても楽ちんです。
AWS歴の浅い新卒の2人がすぐに慣れてデプロイできたのは最高🎉

Infrastructure as Code

私個人としては前職で初めてterraformによるInfrastructure as Codeを経験したのですが、
現時点ではチームではそのような運用は行っておりません。

理由は terraform運用を継続できない懸念がある という点です。

私達は少人数のチームなので、専任のインフラがいなくても運用できるような仕組みを優先しました。
terraformでの運用を始めるとAWSの知識とterraformの知識の両方が必要です。

役割 AWS経験 terraform経験
フロントエンドエンジニア ほぼ無し 無し
サーバサイドエンジニア ほぼ無し 無し
phpの神 有り 無し
なんでもできるエンジニア 有り 無し
インフラエンジニア 有り 有り

万が一インフラエンジニアの私が抜けた場合、今後の運用が回らなくなる可能性があります。
ですので、 現時点では terraformを利用した運用はしていません。
個人的に変更履歴を管理していたりはしますが、あくまでその程度といったところです。

実際、自分も前職ではtfstateのコミット漏れ(当時はtfstateをgit管理してた)やら、
手作業で試行錯誤->後でterraformを書いてapply->実は設定漏れというテンプレやらかしなどもありました。
書いていて辛さと申し訳無さが蘇ってきます。

フロントエンド

新卒のフロントエンジニアがJavaScriptの経験があり、EBのプラットフォームにもNode.jsがあったため、そのまま採用しました。
React を使っていますが、私はフロントエンド全くわからないマンなのでこの話はここで終わり。

サーバサイド

GoKotlin といった最近流行りの言語も含めて色々と検討しましたが、
チームにphpが超詳しい人がいるため、php7.1 + Laravel5.5を採用しました。

builderscon というカンファレンスでSlackのエンジニアの方が登壇されていたのですが、
そこでこんなやり取りがありました。

Q.「なぜHHVMを採用しているのか」
A.「HHVMのコントリビューターがいて、知見もいっぱいある」

なので、その道に明るい人がいるからその技術を採用するというのは間違ったアプローチではないと思います。
※ 採用等の戦略が絡む場合はこの限りではないですが・・・。

EBってなんぞ

EBのキホン

EBはAWSが提供するPaaSです。以上。

EBは一番大きな論理単位であるアプリケーションと、その中にある複数の環境
環境にデプロイするソースコードであるバージョンで構成されています。

image.png
(AWS Black Belt Online Seminar 2017 AWS Elastic Beanstalkより)

とても雑に書くとこんなイメージになるかと思います。
もしかしたらアプリケーションの時点でdev/stg/prdみたいに分けるのかもしれません。
俺達は雰囲気で分けている。

アプリケーション 環境 バージョン
frontend frontend-dev development
frontend-dev-hoge-test hoge-test-branch
frontend-stg master
frontend-prd master
backend backend-dev development
backend-stg master
backend-prd master

EB(PaaS)の良さはいくつかあって、いずれも自分たちで仕組みを考えようとすると面倒なのばかりです。

  • シュッとデプロイができる
  • デプロイ周りのポリシーが充実している
  • シュッとオートスケールの設定ができる

特に、デプロイの仕組みを自分たちで考える必要がないというのは気持ち的にとても楽です。

$ cd /path/to/work
$ git clone hoge
$ eb use frontend-dev
$ eb deploy

ラッパースクリプトの作成なども比較的簡単に作れました。
今ではそのスクリプト等を元にCircleCIで自動的にデプロイできている状況です。

とはいえ、EBが用意しているプラットフォームだけで十分かというとそうでもないわけです。

.ebextensions

  • ALBのヘルスチェック条件を変更したい
  • Security Groupの設定を変更したい
  • EC2のカーネルパラメータを変更したい
  • ミドルウェア設定を変更したい
  • etc...

こんな時は.ebextensionsというEBの拡張機能で対応していきます。
例えばタイムゾーンをデフォルトのUTCからJSTに変更したい場合はこんな感じ。

$ vi .ebextensions/00.timezone.config

# ファイル編集のブロック
files:
  "/etc/sysconfig/clock" :
    mode: "000644"
    owner: root
    group: root
    content: |
      ZONE="Asia/Tokyo"
      UTC=false

# コマンド実行のブロック
commands:
  timezone:
    command: ln -sf  /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

基本的にEBはebextensionsとの戦いになります。
めっちゃ参考にした記事:ElasticBeanstalk .ebextensions 逆引き辞典

EBについて勘違いしていたこと

「Security Groupはebextensionsでカスタマイズする」

AWSを使う場合、これまではこんな感じの順序で構築していました。

1. VPCやSubnetといったネットワーク周りの構築
2. Security GroupやIAMといったセキュリティ周りの構築
3. EC2の中身の構築

EBが作成するEC2には事前に作成したSecurity Groupを設定しようと考えていて、ebextensionsでゴニョゴニョしていたけど上手くいかず、必ずSecurity Groupが生成されるのがすごいモヤモヤしてました。

AWSさんに相談したところ、EBについてはEB内で完結させるほうが良い(事前に用意しなくても大丈夫)というアドバイスを頂けたので、事前に作成したものは削除・ebextensionsの中で設定するようにしました。

.ebextensions/01.sg.config

Resources:
  AWSEBLoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: SecurityGroup settings for ALB
      SecurityGroupIngress:
        - {IpProtocol: "tcp", FromPort: "443", ToPort: "443", CidrIp: "<社内のIP制限>" }
      SecurityGroupEgress:
        - {IpProtocol: "tcp", FromPort: "0", ToPort: "65535", CidrIp: "0.0.0.0/0" }

  AWSEBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: SecurityGroup settings for EC2
      SecurityGroupIngress:
        - {IpProtocol: "tcp", FromPort: "0", ToPort: "65535", SourceSecurityGroupId: {"Ref": "AWSEBLoadBalancerSecurityGroup"}}
        - {IpProtocol: "tcp", FromPort: "22", ToPort: "22", SourceSecurityGroupId: "<踏み台のCIR>"}

「phpプラットフォームのWebサーバはapacheである」

phpプラットフォーム、Webサーバがapacheでした。nginxだとずっと勘違いしてました。
「phpだったらnginx + php-fpmやろなぁ・・・」という自分の思い込みです・・・。

ある時、「ローカルとAWSとで同じ設定でも挙動が違う。」という話を聞いて調べていたわけです。

$ less /etc/nginx/nginx.conf
/etc/nginx/nginx.conf: No such file or directory

おや?🤔
ああそうか、パスが違うんだな。

$ ps aux | grep nginx

🤔🤔🤔🤔🤔🤔🤔

いやまさかそんな

$ ps aux | grep httpd

にゃーん。

参考:Elastic Beanstalk でサポートされているプラットフォーム

EBで辛かった事

「プラットフォームフック」

EBはそのライフサイクルで色々とスクリプトが走ります。(プラットフォームフック)
EBで作成されたEC2の /opt/elasticbeanstalk/hooks あたりに色々とスクリプトが配置されています。

例えば、nginx.confを書き換えるためにこんな感じのebextensionsを書いたとします。

.ebextensions/03.nginx.config

files:
  "/etc/nginx/nginx.conf" :
    mode: "000644"
    owner: root
    group: root
    content: |
      user                        nginx;

      pid                         /var/run/nginx.pid;
      worker_processes            auto;
      worker_rlimit_nofile        65536;

      events {
          worker_connections      8192;
          use epoll;
          multi_accept            on;
      }

      http {
          include                 /etc/nginx/mime.types;
          default_type            application/octet-stream;
          charset                 UTF-8;

          log_format  ltsv  'host:$remote_addr\t'
                                  'vhost:$http_host\t'
                                  'port:$server_port\t'
                                  'hostname:$hostname\t'
                                  'local_time:$time_local\t'
                                  'method:$request_method\t'
                                  'uri:$request_uri\t'
                                  'protocol:$server_protocol\t'
                                  'status:$status\t'
                                  'size:$body_bytes_sent\t'
                                  'ua:$http_user_agent\t'
                                  'referer:$http_referer\t'
                                  'apptime:$upstream_response_time\t'
                                  'resptime:$request_time\t'
                                  'upstream_status:$upstream_status\t'
                                  'upstream_size:$upstream_response_length';

          access_log              /var/log/nginx/access.log ltsv;
          error_log               /var/log/nginx/error.log warn;

                    〜中略〜

          include conf.d/*.conf;
      }

すると、デプロイ後にconfigのエラーでnginxが起動しない事がありました。

プラットフォームフックの中にhealthdというサービスの設定をnginx.confに追加する箇所があり、
include conf.d/*.confが重複して立ち上がらないという事が原因でした。

/etc/nginx/nginx.conf

access_log              /var/log/nginx/access.log ltsv;
error_log               /var/log/nginx/error.log warn;

# Elastic Beanstalk Modification(EB_INCLUDE)
healthdの設定
healthdの設定
healthdの設定
include conf.d/*.conf;
# End Modification

include conf.d/*.conf;

スクリプトの中身を見ると、 EB_INCLUDEがなければ追加するような処理が入っていたので、

/opt/elasticbeanstalk/containerfiles/ebnode.py

def process_main_contents(self, contents):
    if 'EB_LISTENER' not in contents:
        listen_regex = re.compile(r'^(\s*)listen(\s+)80;(\s*)$', re.MULTILINE)
        contents = listen_regex.sub(self.get_mod_line(r'\1listen\2 %s;\3' % self.port, 'EB_LISTENER'), contents)
    if 'EB_INCLUDE' not in contents:
        # insert this after keepalive timeout so that include *.conf happens after all log_formats have been defin
ed
        include_regex = re.compile(r'(^\s*keepalive_timeout(\s+)[0-9]*;\s*$)', re.MULTILINE)
        # insert healthd logging format always as it is harmless
        contents = include_regex.sub(r'\1 \n' + self.get_mod_line(NginxProxyManager.HEALTHD_LOGGING_FORMAT + '\ninclude /etc/nginx/conf.d/*.conf;', 'EB_INCLUDE'), contents)
    return contents

予め下記のように記述することで逃れることができました・・・。

# Elastic Beanstalk Modification(EB_INCLUDE)
include conf.d/*.conf;
# End Modification

この辺でなんとなく「EB辛いな」っていう空気が出てきました。
そこでカスタムAMIを使うか、Docker Multi Containerを使うかという議論が発生します。

そしてDockr Multi Containerへ

同じチームのフロントからインフラまでなんでもできるエンジニア @sssinsi さんが11日目に書いてくれます!乞うご期待!

まとめ

チームでElasticBeanstalkを採用した背景と現状をつらつらと書いてみました。
インフラ基盤やデプロイの仕組みをシュッと作れるので、新規開発の際に選択肢の1つになってくるでしょう。
将来的にはFargateに置き換えてみたいなぁと思っているので、東京リージョンへの対応楽しみにしてます。

明日は @yu-kgr さんの チーミングを頑張る話 です。
お楽しみに!