節約(月1500円)しながらECS on EC2でWebアプリケーションを動かすメモ
⚠️WIPかつ書きなぐりメモなのでご注意ください⚠️
最後にまとめて記事にする予定です。
ECS on Fargateには慣れているがECS on EC2は全然触ったことがないので触ってみる。
※なんなら、EC2自体あんまり触ったことがないのでちょうどよい
なお制約
- コストをなるべく減らす
- ALBは構えない。VPCEndpointも構えない。
- パブリックサブネットの使用が確定(セキュリティ注意)
ECSタスクのネットワークモードでawsvpc
を使用できない(タスクのENIがプライベートIPしか持たず、S3やその他AWSリソースにアクセスするためにVPCEndpointが必要になってしまうため。NAT Gatewayなんて立てませんよ高いし。)
- EC2は1台のみ
- m6g.mediumを使用する(T系怖い。なおもう少し安いのもあるがまぁいいだろうと。)
- 1vCPU, 4GB
- スポットインスタンスを使用する
- $0.0183
- m6g.mediumを使用する(T系怖い。なおもう少し安いのもあるがまぁいいだろうと。)
- 深夜1時~早朝7時は停止する
- ECRは最新以外消す
- ALBは構えない。VPCEndpointも構えない。
0.0183 * 18h * 30days * 130yen = 1285円
そこから「ログ」やら「ECR」やら。。。
頑張れば月1500円くらいまでで抑えられる予感。。(計算適当)
VPCEndpointやENIとかの前提知識間違ってたらすみません。
ECSクラスターの作成
ECS on EC2の場合はオートスケーリンググループ
、アーキテクチャ
やインスタンスタイプ
、そしてキーペアの有無
を指定する必要があるらしい。
ASGを指定しなければ勝手に作成されるらしいが、どうせなら自分で作りたいので、
まずはオートスケーリンググループを作ってみる。
ECSクラスターはそれらが終わってから作成する。
オートスケーリンググループを作成する
と思ったら今度は起動テンプレートが必要らしい。
先に起動テンプレートを作成する。
「オートスケーリンググループ」と「起動テンプレート」
ドキュメント
- オートスケーリンググループとはオートスケールの具体的な構成(どんなEC2をどんな感じでオートスケールさせます??)の設定のこと
- どんなEC2を
- 起動テンプレートで設定する。
インスタンスタイプ
AMI
セキュリティグループ
-
キーペア
などが定義されている。
- 起動テンプレートで設定する。
- どんな感じでオートスケール?
- 最小数
- 最大数
- 理想数
- どんなEC2を
1. 起動テンプレートを作成する
起動テンプレートとは、どんなEC2を立てるかを定義したもの。
OSおよびマシンイメージ(AMI)の選択
Amazon Linux 2023を使いたい。また、m6g.medium
を使おうと思っているので、アーキテクチャはArm
を選択する。
↑この写真ミスってます。このあと書いていますがECS対応のAMIを選択しないと詰みます
インスタンスタイプ
m6g.mediumを使う。理由は汎用の中で新しめの世代で、その中でも安いから。T系はバーストが怖いから。
キーペア
お好みで。
…パブリックに置くんだよ。
ネットワーク設定
ECSの方で設定しているので、多分いらないと思う。
いるなら後で編集しに帰ってきます。
ストレージ
特に気にしていないので無視。
高度な詳細
スポットインスタンスを使ってさらに安くしたいので、スポットインスタンスをリクエスト
にチェックを入れる
あと、ユーザーデータに以下を記述しておく必要があるらしい。
これをすることで、このインスタンスが起動すると自動的にECSクラスターに参加することができる。
#!/bin/bash
echo "ECS_CLUSTER=__MyClusterName__" >> /etc/ecs/ecs.config
というわけで作成する。
間違っていたり足りなければ後で編集すればいいし。
2. 今度こそオートスケーリンググループを作成する
起動テンプレートを作れたので、今度こそASGを作る。
さっき作成した起動テンプレートを選ぶ。
ネットワーク
VPCやSubnetは適切なものを選ぶ。privateが基本だろう。
→後で気づいたがprivateだと大量のVPCEndpointが必要になってコストで詰む。publicを選ぶ。
ロードバランシング
今回の目的は
- 遊びの個人制作
- ECS on EC2ちょっと使ってみるか
くらいなのでインスタンスは1台のつもりだし、そもそもALBなんて構えていたらコストが跳ね上がる富豪の遊びになるので、ALBは構えない。
代わりにLambdaを構えることで何とかする(あとで)
publicサブネットに置くことにしたうえに、ネットワークモードもbridge
にするので、Lambdaを構える必要すらなくなった。
モニタリング
別に要らない。必要そうなら後で有効化すればよい。
グループサイズとスケーリングポリシー
動いてほしくないので、いったん希望0台の最大1台で設定する。
1台なのでスケーリングポリシーもなしでよい。
なお、ここで設定しているのはECS on EC2
の基盤となるEC2のオートスケールであり、ECSタスクのオートスケールではない。
逆に言うと、Fargateと違ってECS on EC2は基盤のオートスケールも考えないといけないというだるさがある。これが管理コストとやらか。
よし。作成する。
今度こそECSクラスターの作成
- 名前を入力。
-
インフラストラクチャ
でEC2にチェックを入れ、さっき作ったASGを選択する。 - 作成
一瞬じゃないか。
ECSタスク定義の作成
ECSサービスを作る。
そのためにはECSタスク定義が必要なので、まずはタスク定義を作る。
そしてタスク定義を作るにはイメージが必要なので、まずはECRを作る。
ECRの作成
まずはリポジトリを作る。
ECRにいって適当に作る。
Dockerイメージのpush
ECSでコンテナを動かすために、コンテナイメージをECRにpushする必要がある。
右上のpushコマンドを表示をみながら、イメージをpushする。
※今回はARM
アーキテクチャを使用しているので注意。
ECSタスク定義の作成
- ファミリーには任意の名前を。
- コンテナのイメージに先ほど作成したイメージのURIを入れる。
- ポートマッピングも適切に。
- アプリに環境変数があるなら登録しておく。
- ヘルスチェックパスがあるなら登録しておく。
- アプリケーション環境には
Fargate
ではなくAmazon EC2
を選択。今回の趣旨。 - アーキテクチャは
ARM
- タスクサイズは
m6g.medium(1vCPU, 4GB)
を超えないように - タスクロール
- タスクに付与されるロール。アプリがAWSサービスを使用する場合はそれに応じた権限が必要。
- ECS on EC2の場合だと基盤であるEC2にもロールがついているが、あれはECRからイメージを落とすときやCloudWatchへのログ出力などで使われるらしく、だいぶFargateとは違う。
- タスク実行ロール
- タスクを実行するのにECSが使うロール。Secretなどのパラメータの取得などはこっち。
- Fargateの場合はこれがイメージプルやCloudWatchへのログ出力を担っていた。
EC2インスタンスを起動する…起動しない
ASGの最小、最大、理想をすべて1に変更し、EC2の動作確認をする。
ステータスが2/2のチェックに合格しました
と出ているのに、ECSの方で認識されない…。
ユーザデータの設定はちゃんとしている。ロールが怪しいのでEC2のIAMRole
を使うように起動テンプレートを変更する。
また、ECSがprivate subnetにいるので、VPC Endpointが必要らしい。
うーん。。あれはとんでもないコストになるので、VPC Endpointを指定しないようにpublic subnetに作り直す。
起動テンプレート
publicIPアドレスが付くようにする
ASG
publicサブネットを指定する
ECS
publicサブネットを指定するようにする必要があるのか…?ECS on EC2は両方に設定があるからよくわからん……。
とりあえず、ECSもpublicサブネットで作り直しておく。
後で調べる。
publicに置くとなるとセキュリティグループも気になってくるので、作り直して起動テンプレートに盛り込む。デフォルトだとなんでも受け付けてしまうので注意。
ECSコンテナエージェントを持つAMIを使うように修正
そういうAMIを使わないとダメ見たい。
AMIを変えたらいけた。きたぜ。
もしかしてpublicIPいらなくないか
このEC2に外から直接アクセスされたくないので、publicIPを外し、セキュリティグループも強固にしたい。
問題ないかどうか確認する。
無理でした。どういうわけかパブリックIPは必要です。。
ECSからDynamoDBにアクセスできない問題
ECSサービスは適当に作成した。エラーだらけで解決に集中してしまい、残せなかった。
ECS on EC2に立てたタスクで動くWebアプリケーションを使って、DynamoDBにアクセスができなかった。
Connection timed out
エラーが出る。
パブリックサブネットにあるのにおかしい。。
調べてみると、ECSタスクのネットワークモードをawsvpc
に設定している場合は、タスク一つ一つにENIがアタッチされる。
このENIはパブリックIPを持たないのでIGWを出ることができず、DynamoDBにアクセスができなかったようだ。
回避策の一つはVPC Endpoint
を作成することだが、毎月1300円がプラスされるので無理。
そのため、ネットワークモードをbridge
にしてホストインスタンス(EC2)のENIを使えば行けるのではないか、という予想を立てて試してみる。
※ただ、これだと複数のタスクを立てるのは本当にあきらめになるだろう。ポートフォワーディングの管理がややこしすぎるからだ。
ネットワークモードをbridgeにする
タスク定義をちょちょいと変えて、サービスで新しいリビジョンのタスク定義を指定しなおして、ちょちょいとデプロイするだけ。
予想通りいけました!
EC2のリソース制限を超えない(等しくてもダメ)ように注意
サービスのデプロイはローリングアップデートで行われ、2台目ができたら1台目が停止する。
今回、私のEC2はm6g.medium: 1vCPU, 4GB
だが、1つのタスクに0.5vCPU, 3GB
を与えてしまっていたので、どうやってもデプロイが完了しなかった。
タスクに0.45vCPU, 1.8GB
を与えることで、2台分起動できるようにした。
…と言いたいところだが、ポート番号が重複しているので無理だった。
やはりネットワークモードはawsvpc
する必要があるのか……。
しかしそれだとVPC Endpointの料金が……。
一応、タスク定義を2つ使いまわすという選択肢もあるが……さすがにナシだろう。
bridge
のまま上手くデプロイさせる仕組みを考える
ネットワークモードは可用性をあきらめる場合
別に個人制作だし、そもそもEC2は1台(=単AZ)でいろいろすでに可用性低いのでどうでもいいが。。
まず、ECRに新しいイメージをpushする。
- latestでpushする場合
- 今動いているタスクを停止し、タスクの再作成を促す
- 何らかのタグをつける場合
- タスク定義を変える必要がある。新しいタグを使用するタスク定義を作成する
- サービスを更新する。新しいタスク定義を使う。
- 今動いているタスクを停止する。タスクの再作成は新しいタスク定義で行われる
可用性を少しでもマシにする場合
2つのタスク定義を使って2台のタスクを立てることで実現できる。
でも個人制作だしいいや、
ECRのライフサイクルルールを設定する
コスト対策
wip
深夜2~早朝6時はASG設定を0,0,0にする
ちょっとでもコストを減らしたい。
- ASGを書き換え
- サービスの必要数も書き換え
WIP
SSL化する
SSL化するぞ。
まずAWSを使った基本的な方法はこちら
- ACMで発行した証明書をアタッチしたALBをEC2の前に構える
- 高いので却下
- ACMで発行した証明書をアタッチしたAPI Gateway + NLBをEC2の前に構える
- 高いので却下
…だめだ。正攻法が使えない。その他の方法としては、
ACMではない正規の(?)認証局が発行した証明書をEC2にインストールする。Let's Encrypt
などのサービスを使えば無料で行える。
これありかも、と思ったが、
- EC2は頻繁に削除される
-
Let's Encrypt
の期限は90日
ということを踏まえて却下。
どうすればいいんだ。
ここは、少々複雑だがやはりLambdaをリバースプロキシとして構えて、
API Gateway -> Lambda -> EC2という構成にすることで、安く済ませようと思う。
そんなにアクセスは来ないと思うし、Lambdaは無料枠があるので基本問題ない。
一応コストを毎日通知する仕組みも作っておくか。
1. リバースプロキシ用Lambdaを作成する
まずはPoCを行う。
ECS(実質EC2)に置いているWebアプリケーションのヘルスチェック用のパスをたたくだけのLambdaを用意し、API Gatewayに紐づける。
Lambdaのコードはこちら。
import json
import urllib.request
def lambda_handler(event, context):
url = 'http://PRIVATE_DNS/health'
req = urllib.request.Request(url)
with urllib.request.urlopen(req) as res:
body = res.read()
return {
'statusCode': 200,
'body': json.dumps('Hello from Lambda!')
}
PRIVATE_DNS
の部分だが、EC2にはElasticIPをつけているわけではないので、IPを指定せずにうまくEC2を見つけ出してアクセスしなければならない。
これを
- EventBridge
- Lambda
の2つで実現する
…いや、結局privateIPでアクセスできるのであれば、ECSタスクのネットワークモードをbridge
にする必要はなく、awsvpc
でいい気がする。
いや違う。Lambdaからのアクセスは全く問題ないが、awsvpcモードで起動するとpublicIPがアタッチされないので、WebアプリからS3などのリソースにアクセスできないんだった。(NATもVPC Endpointもコスト対策で存在しない。)
2. AutoScaleしたEC2を自動的にRoute53に登録する
ALBがあればいろいろ楽なのだが、コスト的に使用しないのでこの仕組みを作っていくぞ。
作っていて気付いたが、このアプリは0台または1台のEC2で動くことを考えると実現が難しい。
Aレコードを用意していても、
- EC2が0台になってもAレコードの値を空にすることはできない。
- 代わりに変なIPを登録するにしても、何も登録できるものがない(127.0.0.1)ならあるいは。
- 127.0.0.1を仮の値とするなら
- EC2が起動したらAレコードの値をEC2のIPにする
- EC2が停止したらAレコードの値を127.0.0.1にする
- じゃあEC2の起動と停止がほぼ同時に起こり、起動の方が若干早かったら…
- 127.0.0.1で上書きされてしまうかも。。
というよくわからない仕様にすれば一応実現可能。
その他の方法としては、DynamodBなどにEC2のIPを登録しておく。
値が返ってきたらそれを使う。返ってこなければEC2は存在しないのでエラーを返す。
などだろうか。。
いったん127.0.0.1でごり押す。
やってみたが、別に旧EC2のprivateIPが残ってもアクセスできないという問題は一緒なので、
EC2がLaunchされたらそのIPでAレコードを上書きするだけで十分だった。
これでいく。
3. Lambdaを呼び出してテストする
問題なかった。