Open10

節約(月1500円)しながらECS on EC2でWebアプリケーションを動かすメモ

takamin55takamin55

⚠️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
    • 深夜1時~早朝7時は停止する
    • ECRは最新以外消す

0.0183 * 18h * 30days * 130yen = 1285円
そこから「ログ」やら「ECR」やら。。。

頑張れば月1500円くらいまでで抑えられる予感。。(計算適当)
VPCEndpointやENIとかの前提知識間違ってたらすみません。

ECSクラスターの作成

ECS on EC2の場合はオートスケーリンググループアーキテクチャインスタンスタイプ、そしてキーペアの有無を指定する必要があるらしい。

ASGを指定しなければ勝手に作成されるらしいが、どうせなら自分で作りたいので、
まずはオートスケーリンググループを作ってみる。

ECSクラスターはそれらが終わってから作成する。

takamin55takamin55

オートスケーリンググループを作成する

と思ったら今度は起動テンプレートが必要らしい。
先に起動テンプレートを作成する。

「オートスケーリンググループ」と「起動テンプレート」

ドキュメント
https://docs.aws.amazon.com/ja_jp/autoscaling/ec2/userguide/auto-scaling-groups.html

  • オートスケーリンググループとはオートスケールの具体的な構成(どんなEC2をどんな感じでオートスケールさせます??)の設定のこと
    • どんなEC2を
      • 起動テンプレートで設定する。
        • インスタンスタイプ
        • AMI
        • セキュリティグループ
        • キーペア などが定義されている。
    • どんな感じでオートスケール?
      • 最小数
      • 最大数
      • 理想数

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は基盤のオートスケールも考えないといけないというだるさがある。これが管理コストとやらか。

よし。作成する。

takamin55takamin55

今度こそECSクラスターの作成

  1. 名前を入力。
  2. インフラストラクチャでEC2にチェックを入れ、さっき作ったASGを選択する。
  3. 作成

一瞬じゃないか。

takamin55takamin55

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へのログ出力を担っていた。
takamin55takamin55

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は必要です。。

takamin55takamin55

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つ使いまわすという選択肢もあるが……さすがにナシだろう。

takamin55takamin55

ネットワークモードはbridgeのまま上手くデプロイさせる仕組みを考える

可用性をあきらめる場合

別に個人制作だし、そもそもEC2は1台(=単AZ)でいろいろすでに可用性低いのでどうでもいいが。。

まず、ECRに新しいイメージをpushする。

  • latestでpushする場合
    • 今動いているタスクを停止し、タスクの再作成を促す
  • 何らかのタグをつける場合
    • タスク定義を変える必要がある。新しいタグを使用するタスク定義を作成する
    • サービスを更新する。新しいタスク定義を使う。
    • 今動いているタスクを停止する。タスクの再作成は新しいタスク定義で行われる

可用性を少しでもマシにする場合

2つのタスク定義を使って2台のタスクを立てることで実現できる。
でも個人制作だしいいや、

takamin55takamin55

深夜2~早朝6時はASG設定を0,0,0にする

ちょっとでもコストを減らしたい。

  • ASGを書き換え
  • サービスの必要数も書き換え

WIP

takamin55takamin55

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を呼び出してテストする

問題なかった。