シンプルなNodeアプリをDocker化してElastic Beanstalkに移行してみた

DBも使わない超シンプルなnodeアプリをHerokuで管理していたが、 いつぞやからか無料枠では月終わりにスリープするように体系がかわってしまったので移行を考えていた。

とりあえず勉強もかねて、AWSでDocker化してみることに。

今回のゴール

  • HerokuのHobbyプラン7$/月よりも低コストでホスティングする
    • その代わり最低限のスペック・冗長化でよい
  • 基本的なDockerのサービス使ってみることで使いまわせる枠組みを得る
    • ミニマムなデモアプリでの利用
    • 開発環境構築とワークフローの汎用化
  • Herokuレベルな触りごごちで簡単にデプロイ・管理できるようにしておく
  • HTTPS化する

AWS上のサービスや構成の検討

とりあえず、どんな構成で行くかをちょっと使ってみたりして比較

今回はコストが最優先。 なので、インスタンスはt3.nano1台くらいでいきたい。 ロードバランサーはいらない。お遊びでやるには高い(立てとくだけで月2000円くらいか)。

これじゃDockerのメリットなくないって? 現状はあまり無いが、負荷が増えた時にスケーリングできる構成に柔軟に変えられる点がいいかも。

次はどんなサービスを使うか。以下検討

  • Fargate
  • ECS
    • 細かく設定をできそうだが、その反面自前でいろいろやる必要が出てきそう。今回は生で扱うのはオーバースペック感。特に構成管理やデプロイフロー周りをしっかり検討する必要がありそう。
  • EKS
    • 未調査。Kubernetesは今後のメインストリームに?今回はまずシンプルな構成の勉強も兼ねるので一旦考慮外
  • Elastic Beanstalk
    • マルチコンテナで利用する場合ECS上で動作するが、その管理あたりを丸っと任せられる。デプロイも容易。

ということで、今回つかうもの&ざっくり構成。

  • 複雑なことしない+管理を楽にしたいのでElastic Beanstalkを使う。
  • せっかく&独自イメージをプライベートなところに置きたいのでECR(Elastic Container Registry)もつかう。
  • シングルインスタンスロードバランサーは使わない。
  • LBなしでSSL化に伴う証明書の管理がAWS上でできないので、インスタンス内にNginxのコンテナを立てそこに証明書を配置する。
  • ドメインはお名前.comで取得しているが、こちらもせっかくなのでRoute53のDNSサービスを利用することに。

Local Docker Install

そもそもMacを新調してローカルにDockerが入っていなかったので、以下を参考にインストール。 登録とかめんどかったのでbrew経由でいれた。

qiita.com

$ brew install docker
$ brew cask install docker

Node App コンテナ化

まずは、アプリ自体をコンテナ化。 いろいろ検討した結果、すこし古い記事だが、基本が抑えられていたので以下を参考にDockerfileや開発フローを構築。 postd.cc

ベースとなるImageは設定の楽さとサイズの兼ね合いでnodeのslimを選択

これでも220MBくらいになるんで本当はもっと最適化した方が良さそうだが。

以下のようなDockerfileを構築

また、Imageのbuild時にアプリケーションのソースを丸っとコピーするが、 モジュールのインストールはbuild時に行うのでnode_moduleはコピーの対象外にしておく。 (ローカルでnpm installしてた場合とか、問題は無いだろうが冗長なので)

.dockerignoreを作成

node_modules
npm-debug.log

この状態でも、 docker buildしてdocker runすればコンテナが起動してアプケーションに接続できる。

SSL化の準備・ローカルでのチェック環境

とりあえずSSL化のことについてちょっと考える。 まず証明書は無料で発行したい。ただALBは使わないので、とても楽なCertificate Managerとの連携がいまいち。

そこで以下を参考にhttps-portalを利用してみることにした。 qiita.com

基本はnginxのプロキシWebサーバーだが、Let's encryptから自動で証明書を取得・更新をしてくれる。 デモレベルのアプリであれば十分そう(自前で管理できないのはやりにくい面もあるが)

まずは、ローカルでhttps接続を確かめられる環境としてdocker-composeファイルをつくる。 ちなみに本番デプロイ前の確認用ということでdocker-compose.stg.ymlという名前にしている。

Let's encryptは証明の発行上限が週ごとにあるので、開発段階やチェック等で本番接続していると動かなくなる。 そこでenvironmentのSTAGEをlocalにしておく。

こうしておくと、オレオレ証明書を自前で発行して代わりに使ってくれる。

またローカルでhttps接続のチェック様にhostsを修正しておく

sudo vi /etc/hosts
...
127.0.0.1       mikinya.net.localhost
...

これでとりあえず、以下コマンドでImageのビルドとコンテナの起動ができる。

$ docker-compose -f docker-compose.stg.yml build
$ docker-compose -f docker-compose.stg.yml up

でブラウザからこのローカル用のドメインにアクセスした時に、https-portalとcontainerの連携のチェックができる。

開発環境・開発フロー

現状のままだとソースコードを編集するたびにdocker-compose buildしないといけないのでめんどい。 開発用のフローを別途用意する。

そもそも、node起動後ファイル更新検知・リロードが自動でできていなかったので、nodemonを導入した。 package.jsonのdependenciesにnodemonを追加

...
  "dependencies": {
    "nodemon": "^1.18.9",
...

そして、普段づかいのローカル開発環境用docker-composeを別途つくる。

こっちのポイントは以下

  • ローカル開発環境ではnginxのプロキシを利用しないでシンプルに単体で立てる
  • 起動コマンドを上書き、nodemonが起動する様にする
  • gitリポジトリローカルにあるソースをvolumesとしてマウントし、ローカルのソースを直接起動・コンテナ内の起動アプリをすぐに更新する
  • mode_modulesのvolumeを別途作成し、コンテナ内の領域からインストールしたmode_modulesを参照する

これで、package.jsonを更新後、コンテナ内からpackage-lock.jsonの更新が可能で、ローカルのリポジトリにコミットできる。しかもnode_modulesは開発環境では動的に更新が可能で、imageにビルドするときはpackage.json、package-lock.jsonできちんとインストールされる。

詳細はこちらを参考に

DockerでのNodeアプリ構築で学んだこと | POSTD

なお、Macだとファイルシステムの都合上、動作がめちゃくちゃ遅くなるという問題があるみたいだが、アプリケーションが小さいせいか、実用上全く気にならなかったのでそのままにしている。

もし問題がおきたら、docker-sync等を使うのが良いのかも。

qiita.com

まぁ、ひとまず、開発開始の流れがめっちゃ簡単になった。

  1. Macを買う、gitとdockerを入れる
  2. リポジトリをcloneする
  3. docker-compose up
  4. コード編集 5 git add & commit

以上、これで、locahostに開発環境が立ってソースの編集もできる。 npmもnode入れる必要もないし、npm installもいらない。楽

AWS準備

awsコマンドもインストールしていなかったのでインストール

brew install awscli

aws configureで適切な権限を持つユーザのAccess Key とかSecretとか入れる

また、Elastic Beanstalkのマルチコンテナを使う場合は何かしらのコンテナのレジストリサービスが必要。

シングルコンテナの場合はソースコードをアップロード後にbuildするみたいだが、 今回はNginxのコンテナもフロントに立てたいのでマルチコンテナでECRをつかってみる。

ECR

あらかじめECRのコンソールから適当な名前をつけてリポジトリを作成。

また、ゴミがたまるのも嫌だったのでLifecycle Policyも設定。

f:id:miki05:20190118171720p:plain これで3つ以上のタグが無いImageは消えるかな?(たぶん)

でローカルのビルドとpushが面倒だったのでスクリプトを作成(scripts/push-image.shに配置)。

Elastic Beanstalk準備

Elastic Beanstalkのコンソールから新しいアプリを作成。

プラットフォームをMulti Container Dockerにしておく。 とりあえずサンプルのアプリをデプロイする設定で、ロードバランサーなし、他はだいたいデフォルト。

構築が終わり提示されたURLにアクセスするとNginxが立っている様が見れる。 EC2に指定したインスタンスが立ち、EIPが振られ、ECSに項目として現れる。

Dockerrunファイル作成

Dockerrunファイルをつくる。基本docker-compose.ymlを単純に置き換える感じ。

ただ、独自にbuildしたアプリのimageの取得先がECRのURLになり、https-portalの証明書の取得を本番環境設定してやる必要がある。

Elastic Beanstalkロール権限の設定

Elastic Beanstalkの管理下でインスタンスからECRにアクセスしImageを取得できるようにしないといけない。

Elastic Beanstalkを利用し始めると、IAMにaws-elasticbeanstalk-ec2-roleが追加されているので、 こいつにAmazonEC2ContainerRegistryReadOnlyポリシーをアタッチする。

Route53設定

https-portalを利用していることもあって、おそらくDNS設定をしてからじゃ無いと証明書取得がこける気がする(未確認)。まぁ大したアプリじゃ無いので、一旦停止を良しとしDNSを最初に切り替えておく。

Route53にて新たに取得したDomainのCreate Hosted Zoneを作成。さらにCreate Record SetからRecordを追加

  • Type: A
  • Alias: Yes
  • Alias Target: Elastic Beanstalkで作成したアプリケーションURL

あとはお名前.comのDNS設定にNSレコードの情報を追加

セキュリティーグループ設定

Elastic Beanstalkがインスタンスに設定したセキュリティグループにのインバウンドにHTTPS/443を追加

Elastic Beanstalkデプロイ

さて、あとはデプロイ。基本的にコマンド一つでデプロイしたい。基本はebコマンドでできる。

ebコマンドをインストール&初期設定

brew install awsebcli
eb init

ただデフォルトのeb deployは、gitのアーカイブをアップロードする。ImageはすでにECRにpush済みなので、リポジトリのソースファイルはデプロイに必要ない。

大した容量にはならないが、必要なのはDockerrun.aws.jsonだけなので冗長なものはアプロードしたくない(s3にたまる)。

調べてみるとebコマンドはデプロイするものを任意指定でき、独自にデプロイのフローを制御できるらしい。ということで、自前でDockerrun.aws.jsonをzip圧縮してそれだけあげることに。

まずは、.elasticbeanstalk/config.ymlに以下を追加する

deploy:
  artifact: .elasticbeanstalk/artifact.zip

これでeb deployコマンドで.elasticbeanstalk/artifact.zipがアップロードされるようになる。

さらに独自のdeployスクリプトを用意する(scripts/deploy.shに配置)。

#!/bin/bash

set -eu

zip .elasticbeanstalk/artifact.zip Dockerrun.aws.json 
eb deploy

zip圧縮後deployコマンドを叩くかんじ。

ただ現状のデプロイフローはまだ問題がある

  • ERCへのpushスクリプトとdeployスクリプトを二回叩く必要がある
  • .elasticbeanstalk/config.ymlがignoreされているのでartifactの設定を他の端末でやり直す必要がある
  • gitのリビジョンとデプロイが紐づかない(Dockerrun.aws.jsonはコミット前でもデプロイできちゃう)

もろもろ考えると、さっき作ったimageをビルドしてECRにあげるスクリプトもデプロイスクリプトに組みこむといいのかも。 また、独自zipをつくるより.ebignoreファイルを作って、Dockerrun.aws.json以外は無視するほうがよいのかも。

開発とデプロイのまとめ

  • ローカル開発
    • docker-compose up
    • ブラウザからlocalhost:3000にアクセス
    • ファイルはそのまま編集して、コミットすればよい
  • Webフロントサーバー込みのローカルチェック
  • 本番デプロイ
    • scripts/push-image.sh
    • scripts/deploy.sh

目標コストに関して

さて、そんなこんなで、AWS上にアプリを移行しましたが、コストを計算。 ちなみに東京リージョンです。

  • EC2 : 0.0068USD/Hour * 24 * 30 = 4.896USD
  • EBS : 0.12USD/GB/Month (gp2) * (12 + 8)GB= 2.4USD
  • Route 53 : 0.5USD/HZ = 0.5USD
  • ECR : 0.10USD/GB/Month * 0.1GB = 0.01USD
  • EIP : 0USD
  • ECS : 0USD

合計:7.806USD

おっと...細かい費用がかさんで目標のHeroku Hobbyプラン7$より高くなってしまった...w

まぁあとは、 リージョンをオレゴンにするともう少し安くて、6.726USDでいけそう。 あとはリザーブインスタンスを使うってとこかな。

もう少し研究してみます。