シンプルな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経由でいれた。
$ 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等を使うのが良いのかも。
まぁ、ひとまず、開発開始の流れがめっちゃ簡単になった。
以上、これで、locahostに開発環境が立ってソースの編集もできる。 npmもnode入れる必要もないし、npm installもいらない。楽
AWS準備
awsコマンドもインストールしていなかったのでインストール
brew install awscli
aws configureで適切な権限を持つユーザのAccess Key とかSecretとか入れる
また、Elastic Beanstalkのマルチコンテナを使う場合は何かしらのコンテナのレジストリサービスが必要。
シングルコンテナの場合はソースコードをアップロード後にbuildするみたいだが、 今回はNginxのコンテナもフロントに立てたいのでマルチコンテナでECRをつかってみる。
ECR
あらかじめECRのコンソールから適当な名前をつけてリポジトリを作成。
また、ゴミがたまるのも嫌だったのでLifecycle Policyも設定。
これで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フロントサーバー込みのローカルチェック
- docker-compose -f docker-compose.stg.yml build
- docker-compose -f docker-compose.stg.yml up
- ブラウザからhttps://mydomain.localhostにアクセス
- 本番デプロイ
- 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でいけそう。 あとはリザーブドインスタンスを使うってとこかな。
もう少し研究してみます。