:wq!

ブログ(Ghost.org)のバックエンドをAWS Fargate & Amazon EFSにしてみた

ども、takiponeです。
2年振りにブログの構成を変えたので紹介します。前回と比較すると、エッジのCloudFrontとDockerコンテナは継続、ScalewayのArmベアメタルサーバーをAWS FargateAmazon EFSに置き換えました。

インフラ構成

まずは図で示します。

1日数アクセスという個人ブログなので、維持費用を極力抑える構成を考えています。Scalewayのベアメタルサーバーが月額300円ちょっと(日本円概算)という破格でそれを下回るのは無理ですが、月額1,000円以下を目標に、Fargateタスク数は最低数の1、鉄板構成のELBをあえて省きサーバー/コンテナが障害で落ちているときはCloudFrontのキャッシュで乗り切るという戦略です1。以下2点を工夫しました。

  1. Fargateを割引料金で実行できるFargate Spotを設定
  2. CloudFrontのオリジンとしてECS(Fargate)タスクのPublic IPを指定するために、ダイナミックDNSのような仕組みをLambdaとRoute53で作成

項目2の部分は以下の構成です。

  • ECSタスクの状態変更イベントをEventBridgeで検知し、Lambdaを実行
  • Lambda関数でECS APIからECSタスク(に紐付くENI)のPublic IPを取得、Route53のリソースレコードを上書き

EventBridgeのイベントパターンでは、以下の感じでECSの当該クラスタの状態変更イベントを絞り込みました。

Amazon_EventBridge-2

Lambda関数のコードを以下に示します。

import json;
import boto3;

def lambda_handler(event, context):
    
    if event['detail']['desiredStatus'] == 'RUNNING' and event['detail']['lastStatus'] == 'RUNNING':
        
        eniId = event['detail']['attachments'][0]['details'][1]['value'];
        client = boto3.client('ec2');
        enis = client.describe_network_interfaces(NetworkInterfaceIds=[eniId]);
        publicIp = enis['NetworkInterfaces'][0]['Association']['PublicIp'];
        
        client = boto3.client('route53')
        res = client.change_resource_record_sets(
            HostedZoneId='XXXXXXXXXXXX',
            ChangeBatch={'Changes': [{
                'Action': 'UPSERT',
                'ResourceRecordSet': {
                    'Name': 'XXX.XXX.XXX', # オリジンのリソースレコード
                    'Type': 'A',
                    'TTL': 60,
                    'ResourceRecords': [
                        {
                            'Value': publicIp
                        }
                    ]
                }
            }]}
        )
        print(res['ChangeInfo'])
    return {
        'statusCode': 200,
        'body': 'OK'
    }

ポイントはECSタスクのステータス評価の部分です。状態変更イベントはタスクの終了やPENDINGRUNNINGの遷移も拾うので、タスクの実行時のおおむね1回に絞るために「desiredStatuslastStatusの両方がRUNNINGのとき」という条件にしてみました。詳細なステータス遷移を把握しているわけではないのですが、タスクの実行直後だとPublic IPが未アサインで空振りすることがあるかな?くらいの感じで設定してみたのですが、何度か手動でタスクを停止してECSサービスによって再実行される様子を見た限り、意図した通りに動いていました。

ECSの構成

Dockerコンテナは、前回と同じく nginx:latestghost:3 の2コンテナをタスク定義に含め、EFSボリュームを各々でマウントし nginx.conf/var/lib/ghost/content をEFSに配置して永続化しています。

Amazon_ECS-3

Amazon_ECS-4

EFSボリュームの書き込みに排他制御がかかるわけではないので、タスク数1にしておくのが結果的にトラブルを回避できるかなとも思っています。

また、Nginxの proxy_pass は、FargateではDockerのUser Defined NetworkやLinkが使えない代わりにlocalhostで他のコンテナのポートにアクセスできるとのことなので、proxy_pass http://localhost:2368; としました。

とはいえ、User Defined Networkは便利なので、実装要望のスレッドが伸びている現状だったりはします。

まとめ

本ブログの構成をご紹介してみました。あくまでケチケチな最小構成なので、安定稼働を指向するならELBを噛ませて複数タスク構成にする方が良いと思いますよ!


脚注

1: と思ってたら、Ghostからcache-control: public, max-age=0 が返って来ててキャッシュが効かないことに気づきました。オリジンのレスポンスヘッダ大事。直さねば。。。