【失敗記録・原因判明】 Django を AWS Fargate で動作させる


うまく動かなかったという記録です

本記事は「非エンジニアがこういうことを試したけど動かなかった」という記録です。

本当はバッチリ動いた後に公開したかったのですが、まァ、途中くらいまでは誰かの参考になると思いますし、どこかからスーパーエンジニアが降臨してアドバイスをくれるかもしれないので、「動きません記録」として公開します。

(バッチリ動いた暁には「動いた!」で更新したい)
無理でした。

(11/24 追記)動かない原因が分かった

↓↓本記事の末尾に追加しました↓↓

私の「動かなかった」とその原因を教訓にして、皆さんには「動く本番環境」を作っていただきたい。
俺の屍を越えて行け!

動作環境とこれまでの経緯

ここまでで上記構成の開発環境をローカルに用意できたので、次は AWS に本番環境を用意したい……が、Docker を使うとはいえ、EC2 インスタンスを管理するなんて面倒なことはしたくない。

そこで、Fargate を使って、開発環境からコンテナイメージを ECR にプッシュするだけで Django が動く環境を作ろうと思います。いわゆるひとつのサーバレスだ!マネージドサービス万歳!ガハハ!!(…と思っていた)

全体像

簡単に絵を描くと、だいたいこんな感じをイメージしました。CloudFront のオリジンサーバとして Fargate を配置するという、いたってノーマルな構成です。今回は赤点線の部分の構築に取り組んでいきます。

Makefile の作成

開発環境では前回書いたとおりにコンテナを3つ動かすわけですが、本番環境では Fargate で Django のコンテナを動かすだけです。そのため、開発環境と本番環境とで docker-compose の設定が異なってきます。

そのため、開発用・本番用にそれぞれ docker-compose.dev.ymldocker-compose.prod.yml とを作って使い分けるのですが、いちいちファイルをオプションで指定してコマンドを打つのが面倒なので、まずは make でこれを簡略化したいと思います。

Makefile
main:   
    docker tag app_python:latest ************.dkr.ecr.ap-northeast-1.amazonaws.com/app_python:latest
    docker push ************.dkr.ecr.ap-northeast-1.amazonaws.com/app_python:latest
dev:
    docker-compose -f docker-compose.dev.yml build
prod:
    docker-compose -f docker-compose.prod.yml build
up:
    docker-compose -f docker-compose.dev.yml up -d
down:
    docker-compose -f docker-compose.dev.yml down
stop:
    docker-compose -f docker-compose.dev.yml stop
login:
    aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ************.dkr.ecr.ap-northeast-1.amazonaws.com
clean:
    docker-compose -f docker-compose.dev.yml rm
    docker-compose -f docker-compose.prod.yml rm

本番用の docker-compose.prod.yml は、次のとおりシンプルな設定です。

docker-compose.prod.yml
version: '3.7'

services:
  python:
    build:
      context: ./python
      dockerfile: Dockerfile
    restart: unless-stopped
    container_name: Django
    volumes:
      - ./src:/code
      - ./static:/static
    expose:
      - "80"    # http の待ち受け
      - "443"   # https の待ち受け
      - "3306"  # Aurora との接続

docker-compose.dev.yml は、前回書いた docker-compose.yml と同じなので、そっちを見てね。

AWS の準備

事前準備として、ECS 以外の設定がいろいろと必要になります。ブラウザでポチポチやるのは面倒だけど、全部をコマンドライン(AWS CLI)だけでやり切るほどスキルはないので、仕方なくポチポチやることにします。

1. IAM ユーザの作成とグループの設定

IAM のコンソール画面から IAM ユーザを作成し、そのユーザを適当なグループに所属させておきます。もちろん、そのユーザのアクセスキー ID とシークレットアクセスキーをゲットしておきましょう。
参考:AWS の IAM ユーザー作成を CLI で解説してみる

ちなみに、私は権限まわりでハマるのが嫌だったので、グループの権限は最強の AdministratorAccess に設定しました。

2. AWS CLI のインストールと ECR へのログイン

まず、AWS Command Line Interface(CLI)をインストールします。あとでコンテナのイメージを ECR にプッシュするのに使うから。

$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install

次に、aws configure で、IAM ユーザのアクセスキー ID とシークレットアクセスキーを設定します。「profile_user」の部分は、作成した IAM ユーザの名前に置き換えてね。

$ aws configure --profile profile_user
AWS Access Key ID [None]: *****        ## Access Key ID を入力する
AWS Secret Access Key [None]: *****    ## Secret Access Key を入力する
Default region name [ap-northeast-1]:  ## 東京リージョンならそのままエンター
Default output format [None]:          ## そのままエンター

そして、Docker で ECR にログインします。「*****」の部分は、AWS のアカウント ID(12桁の数字)に置き換えてね。

$ aws ecr get-login-password --region ap-northeast-1 | \
> docker login --username AWS --password-stdin *****.dkr.ecr.ap-northeast-1.amazonaws.com

「Login Succeeded」と表示されれば認証成功。

ちなみに、このコマンドは前述の Makefile を作っていれば、$ make login で済みます。

3. レポジトリの作成とイメージのプッシュ

まずは、本番用のイメージをビルドします。
(前述の Makefile を作っていれば、$ make prod で済みます。)

$ docker-compose -f docker-compose.prod.yml build

次に、ビルドして完成したイメージ名と同じ名前のレポジトリを ECR に作ります。docker-compose.yml でイメージ名を明に指定していなければ、イメージ名はデフォルトで{プロジェクト名}_{サービス名}になるので、前回の流れであれば、今回の場合は「app_python」になっています。

$ aws ecr create-repository --repository-name app_python

イメージにタグ(latest)を付けて、ECR にプッシュします。
(前述の Makefile を作っていれば、$ make と打つだけでプッシュされます)

$ docker tag app_python:latest ************.dkr.ecr.ap-northeast-1.amazonaws.com/app_python:latest
$ docker push ************.dkr.ecr.ap-northeast-1.amazonaws.com/app_python:latest

4. 証明書の発行

実は、だいぶ前にお名前.com でドメインを取って、そのままなんとなく持ち続けている謎の独自ドメイン(hajimepat.jp)があります。今回はそのドメインを使って HTTPS でアクセスしたいので、AWS Certificate Manager から証明書をもらいます。

「証明書のリクエスト」からドメイン名を追加して、メールか DNS の CNAME 設定で本人確認すると、そのドメインで証明書が発行されます。レジストラ(お名前.com)には、CloudFront の URL を後で CNAME 登録することになります(後述)。

独自ドメインは使わないよっていう場合は、このプロセスをスキップして CloudFront の URL に直接アクセスしましょう。Route53 で持っている人は、適当にそっちを使ってください。

5. VPC とサブネットの確認(新規作成)

Virtual Private Cloud(VPC)のコンソールにアクセスして、デフォルトの VPC が設定されていることと、その VPC が3つのサブネットに分割されていることを確認します。CIDR が /16 はデカすぎる気がしますが、そこは気にしない。


ちなみに、ALB は2つ以上のサブネットを必要とするので、新しく VPC を作る場合は自分で適当にサブネット分割しましょう。

6. セキュリティグループの作成

「80」と「443」のインバウンドを許可する「Django」という名前のセキュリティグループを作ります。

また、Aurora(MySQL)との接続用に、「Django」のセキュリティグループのみから「3306」のインバウンドを許可する「MySQL」という名前のセキュリティグループを作ります。

7. Aurora の設定

Aurora でデータベースを作ります。開発環境ではコンテナで MySQL を動かしているので、本番環境でもそうしようかなと思ったのですが、よく考えたら、使い捨てが前提のコンテナで永続化が前提のデータベースを作るのは設計思想としてどうなんだと思ったので、普通に Aurora を使うことにしました。

VPC のセキュリティグループには、もちろんさっき作った「MySQL」のセキュリティグループを指定します。

8. ターゲットグループの作成

ALB を設定するために、先にターゲットグループを作ります。ALB 側でリクエストを 443 番ポートで待ち受け、それを Fargate 側の 443 番ポートに流すようにターゲットグループを設定します(ターゲットグループの名前の末尾に「-2」が付いているのは、試行錯誤したからです。。)

ターゲットタイプは「IP」、プロトコルは 443 番、VPC は先に作ったものを指定します。

9. ロードバランサーの設定

Application Load Balancer(ALB)を設定します。VPC、証明書、セキュリティグループ、ターゲットグループは、先に作ったものをそれぞれ指定します。

今回は、わざわざ証明書を発行して HTTPS 通信をしようとしているので、リスナーに「HTTPS:443」を追加します。また、「HTTP:80」にリクエストがきたときは、「HTTPS:443」にリダイレクトする設定をしておきます。

10. CloudFront の設定

「Origin Domain Name」には、先ほど設定した ALB の URL を指定します。それ以外はデフォルトでオーケー(たぶん)。

設定すると「Domain Name」がもらえるので、これをレジストラ(お名前.com)の CNAME に登録します。
(下の画像は お名前.com の設定画面です)

(本記事の執筆時点では Django が動いていないので、https://django.hajimepat.jp/ は 403 が出ます。。)

ECS の設定

さて、ここからようやく ECS の設定に入ります。

1. タスク定義

まずはタスクを定義します。ECS のコンソール画面の左側メニューから「タスク定義」を選び、「新しいタスク定義の作成」から「起動タイプの互換性の選択」と進みます。もちろん、ここでは「Fargate」を選択します。

次の「タスクとコンテナの定義の設定」の画面では、いろいろと設定項目が並んでいます。まず、「タスクサイズ」ですが、メモリも CPU もミニマムで十分でしょう。次に、「コンテナの定義」の欄で、先ほど ECR にプッシュしたコンテナを追加します。

「イメージ」の欄には ECR に登録したレポジトリの URL を指定します。また、コンテナが開放しているポート番号を「ポートマッピング」欄で指定します。

さらに、「環境」の欄では、環境変数を上書きすることができます。ここで、Django の settings.py に記載されている DATABASES の設定を上書きします。

基本的に settings.py をそのまま転記するだけですが、「HOST」の項目には Aurora のエンドポイントを入力しておきます。

2. クラスターの作成

次に、クラスターを作ります。ECS のコンソール画面の左側メニューから「クラスター」を選び、「クラスターの作成」から「AWS Fargate を使用」に進んで、クラスター名を入力するだけです。

VPC は先ほど作ったものを使うので、このクラスター用に新しく作る必要はありません。

3. サービスの設定

作成したクラスターで、サービスを設定します。クラスターを選択し、「サービス」タブで「作成」を選択すると、「サービスの設定」画面が開きます。

「起動タイプ」は、もちろん「Fargate」、「タスク定義」は、先ほど定義したタスク定義を選びます。「タスクの数」は、とりあえず1でいいと思います。

次の「ネットワーク構成」の画面では、「VPC とセキュリティグループ」を設定します。まず、「クラスター VPC」には先ほど作成した VPC を選択し、「サブネット」にはその VPC に含まれる3つのサブネットをすべて追加します。

「セキュリティグループ」では、なぜか新規のセキュリティグループがデフォルトで入力されていますが、「編集」を押して先ほど設定したセキュリティグループ「Django」を選択します。

「ロードバランシング」の欄では、「Application Load Balancer」を指定すると、「ロードバランサー名」には自動的に先ほど作成したロードバランサーの名前が入ります。

「ロードバランス用のコンテナ」の欄では、「app_python:443:443」を選択し、「ロードバランサーに追加」します。ここで、「ターゲットグループ名」に、先ほど作成したターゲットグループ(試行錯誤した結果「-2」が付いた「Django-target-2」)を指定すると、他の項目も自動的に埋まります。

その結果、こんな感じのサービスができます。

動かない

ここまで設定すれば、CloudFront から ALB を経由して、Fargate でコンテナが動いて、何かしらページが見られるはずなんですが、、、依然として無慈悲なエラーが返ってきます。

https://django.hajimepat.jp/ にアクセスした場合:Chrome が「この接続ではプライバシーが保護されません」のエラーを返します。
http://django.hajimepat.jp/ にアクセスした場合:CloudFront が「403 ERROR」が返します。

バグの原因として心当たりがあるのは、次の3つです。

そもそも docker-compose.prod.yml が悪いんじゃないか疑惑

「こんなもんでええやろ」的に作った docker-compose.prod.yml ですが、本当にこれで合ってるのか?

証明書が悪いんじゃないか疑惑

HTTPS:443 でアクセスして「プライバシーが保護されません」が出るってことは、Certificate Manager で証明書がうまく検証されてないのでは?…と思ったのですが、きちんと「状況」は「発行済み」、「更新資格」は「使用可能」、「検証状態」は「成功」なので、ここはバグの原因ではないと思うんですよね…。

ALB のターゲットグループが悪いんじゃないか疑惑

ターゲットグループのヘルスチェックでステータスを見ると「healthy」になっておらず、「draining」になっています。これは明らかにおかしい。いかにも不健康そう。

…が、だいぶ試行錯誤をしたものの、結局どこをどういじったらいいかが分からん。

やっぱ分からん

誰か教えて。ここまでの情報で見事バグを特定して解決してくれたスーパーエンジニアには、ウチの近所にあるスーパー銭湯の無料招待券を差し上げます

(11/24 追記)動かない原因が分かった

当社が誇るスーパーエンジニア「uwsgi がどこにもないんじゃないすか?」

そう、前回の記事で「Django が nginx からリクエストを受け取るために、uwsgi : 8001 を使うよ」と自分で書いておきながら、それがどこにもなかった。

軽く言い訳をすると、そのあたりは CloudFront が謎のテクノロジーでヨロシクやってくれるものかと思ってた。甘かった。そりゃダメだわ。

というわけで、前回作った nginx のコンテナも一緒に ECR にプッシュして、Fargate で動かせば、ちゃんと動くと思う。たぶん。しらんけど。

そして、衝撃の事実が。

泣いたね。RDS たけえよ。勉強代としてはこんなもんかもしれんけど、おもちゃとしては高くついたな。。