nykergoto’s blog

機械学習とpythonをメインに

ECSのHealthCheckがDjangoの動的ページのときに失敗する

Django を ECS with Fargate に Deploy しているようなアプリケーションがあり、Django の ECS task には ALB が紐付いていている状況を考える。

TargetGroup はつながっている先のサーバーが動作しているかどうかをテストして Healthy or Not を判断するような仕組みで、はじめはこの確認先を静的なファイル /health_check.html で運用しようと思っていたのだけれど、ふとこれを動的に生成するページに変更するとステータスコード 400 が帰ってくるため health check に失敗するようになった。

ちなみになんで動的なページにしたいかというと静的ファイルだと nginx で止まってしまうので Django の処理まで到達させたい、という気持ちがあったためである。具体的には nginx の config には以下のような設定が入っているので, Django の static file を指定すると $uri で match して Django server までリクエストが到達しないので、なんか嫌だなーという気持ち。nginx だけ動いていて Django が動かないことってまあないとは思うのだけれど、動きとしては確認したいじゃない?

server {
  # 中略
  # django の public (collect static のファイルが保存されるディレクトリ) が root に指定されている
  root   /var/www/django-app/public;

  location / {
    # なので $uri で static file に match するため `@proxy_to_app` にたどり着かない
    try_files $uri $uri/ $uri/index.htm @proxy_to_app;
  }

  location @proxy_to_app {
    # ここまで来れると, django につながる (正確には gunicorn server)
    proxy_pass http://app_server;
  }
}

health check の該当の path にブラウザから直接アクセスすると問題なく画面は表示されていて、ステータスコードは 200。ちなみに health check 先を静的ファイルに変更すると 200 が帰るようになるので、単純に静的・動的によって TargetGroup からのリクエストが成功する失敗するが切り替わることがわかった。

今の ECS Container で動く Django サーバーの構成は nginx + gunicorn で起動していて、上に書いたように静的ファイルは nginx 側で処理されるようになっている。したがって

  • nginx からのレスポンス = 200
  • nginx → gunicorn 経由の django のレスポンス = 400

ということ。

色々調べていると Allow Domains の設定の影響であるという stack overflow にたどり着いた。

stackoverflow.com

要約すると

  • Target Group から送られる health check のリクエストは、ECS のコンテナが存在している IP を直で叩きに行く。
  • Django がそのリクエストを受け取ると IP は Allowed Host にないためエラー (400) を返す。

ということらしい。解決方法はいくつか提示されていて、一つは Django が起動したときに今動いている ECS Container のメタ情報を AWS へリクエストして Allowed Host を増やすというもの。なかなか斬新なアイディアだが、今実行している container の状況については、AWS の公式が提供している機能としてあるらしい。初耳。

python の package もあって, 実装はこういう感じ. app が読み込まれるタイミングで meta data から取得できる local-ipv4 を allowed hosts 追加するという動きになっている.

https://github.com/sjkingo/django-ebhealthcheck/blob/master/ebhealthcheck/apps.py

from django.apps import AppConfig

class EBHealthCheckConfig(AppConfig):
    name = 'ebhealthcheck'

    def ready(self):
        """
        Fix for the EB health check host header:

        https://aalvarez.me/posts/setting-up-elastic-beanstalk-health-checks-with-a-django-application/
        https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
        """

        from django.conf import settings
        import requests

        def get_ec2_instance_ip():
            metadata_url = "http://169.254.169.254/latest"
            n = 0
            while n < 10:
                try:
                    token = requests.put(metadata_url + "/api/token", timeout=0.1, headers={
                        "X-aws-ec2-metadata-token-ttl-seconds": "60",
                    }).text
                    ip = requests.get(metadata_url + "/meta-data/local-ipv4", timeout=0.1, headers={
                        "X-aws-ec2-metadata-token": token,
                    }).text
                except (requests.exceptions.ConnectionError, requests.exceptions.InvalidHeader):
                    n += 1
                    continue
                else:
                    return ip
            return None

        public_ip = get_ec2_instance_ip()
        if public_ip:
            settings.ALLOWED_HOSTS.append(public_ip)

これを入れて問題なく動くかなーと思ったら, health check は一向に直らない。

request に失敗しているのかも? とおもい ECS execute command を使って実行中の container 内部で http://169.254.169.254/latest にアクセスすると 400 エラーになった。ということはこの metadata 取得方法ではうまく行かないということのよう。よくわからないが、多分別のエンドポイントにアクセスしないと行けないんだろうなと思いつつ調べていると Fargate などの仮想マシン上では別の取得方法があるらしいことがわかった。

Use appropriate ECS credentials on CodeBuild maven job: https://stackoverflow.com/questions/42794486/use-appropriate-ecs-credentials-on-codebuild-maven-job/47028691#47028691

これによると

When using AWS Containers (Like CodeBuild does). The instance metadata is at a different location to the usual http://169.254.169.254/latest/meta-data/

Instead. AWS sets an Environment variable $AWS_CONTAINER_CREDENTIALS_RELATIVE_URI which points to the correct URI to obtain metadata. This is required by the AWS SDK's and other tools in order to assume an IAM Role.

とのこと。なるほどこれは CodeBuild だけど仮想マシンという意味では似ているしこの筋でいけるかも? と思ってとりあえず書いてあるエンドポイント http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI に container からリクエストするとデータが取れた! 結論から言うと Fargate では https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task-metadata-endpoint-v3.html このドキュメントにあるようなエンドポイント経由でメタデータを取るのが正解らしい。具体的には以下の ECS_CONTAINER_METADATA_URI に入っている URL を利用するのが正解。

${ECS_CONTAINER_METADATA_URI}
このパスはコンテナのメタデータ JSON を返します。

レスポンスは typescript のスキーマとかがあるわけではないのでレスポンスを見て類推。

{
  "DockerId": "43481a6ce4842eec8fe72fc28500c6b52edcc0917f105b83379f88cac1ff3946",
  "Name": "nginx-curl",
  "DockerName": "ecs-nginx-5-nginx-curl-ccccb9f49db0dfe0d901",
  "Image": "nrdlngr/nginx-curl",
  "ImageID": "sha256:2e00ae64383cfc865ba0a2ba37f61b50a120d2d9378559dcd458dc0de47bc165",
  "Labels": {
    "com.amazonaws.ecs.cluster": "default",
    "com.amazonaws.ecs.container-name": "nginx-curl",
    "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-2:012345678910:task/9781c248-0edd-4cdb-9a93-f63cb662a5d3",
    "com.amazonaws.ecs.task-definition-family": "nginx",
    "com.amazonaws.ecs.task-definition-version": "5"
  },
  "DesiredStatus": "RUNNING",
  "KnownStatus": "RUNNING",
  "Limits": {
    "CPU": 512,
    "Memory": 512
  },
  "CreatedAt": "2018-02-01T20:55:10.554941919Z",
  "StartedAt": "2018-02-01T20:55:11.064236631Z",
  "Type": "NORMAL",
  "Networks": [
    {
      "NetworkMode": "awsvpc",
      "IPv4Addresses": ["10.0.2.106"]
    }
  ]
}

見た感じ NetworksIPv4Addresses の配列を全部入れれば OK っぽい。ということで実装。

# `elb_healthcheck/apps.py

import os
from typing import List

import requests
from django.apps import AppConfig


class ESCHealthcheckConfig(AppConfig):
    """Fargate上で動く instance の local ip を allowed hosts に追加する"""

    name = "ecs_healthcheck"

    def ready(self):
        from django.conf import settings

        current_allowed_hosts = [*settings.ALLOWED_HOSTS]
        print("current allowed hosts: {}".format(", ".join(current_allowed_hosts)))

        url = os.getenv("ECS_CONTAINER_METADATA_URI", None)

        if url is None:
            print("uri is not found")
            return

        res = requests.get(url, timeout=1)
        if res.status_code != 200:
            print("fail to fetch")
            return
        data = res.json()
        local_ips: List[str] = data.get("Networks", {}).get("IPv4Addresses", "")

        if len(local_ips) > 0:
            print(f"add local ip: {', '.join(local_ips)}")
            settings.ALLOWED_HOSTS = [*current_allowed_hosts, *local_ips]

この app を INSTALL_APPS に追加すれば OK

# settings.py

INSTALLED_APPS = [
    "ecs_healthcheck",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    ...
]

余談

これで一見落着、と思ったが Health Check は最低限のリクエストとするべき、という記事を見つけてしまった。

https://cha-shu00.hatenablog.com/entry/2022/08/20/144747
ELB によるヘルスチェックでは、「アプリケーションが新規のリクエストを受付できること」だけを検証するべき。例えば、"ok"などの単純な文字列を返したり、小さな静的コンテンツを返すようなパスを指定する。

確かに DB への接続を watch したければサービスの正常な動作を見るような仕組みを入れたほうがいいよね。例えば betteruptime とか。ただ nginx の動きが OK ならアプリケーションが新規のリクエストを受け取れる状態なのか、は僕にはなんともわからなかった。気持ち的には Django まで動いているぞ! がわかったほうが気分はいいけどそれはやりすぎかもなー。宿題です。

コンテナ物語 世界を変えたのは「箱」の発明だった・を読む。

西梅田にあるジュンク堂でこの本を買った。珍しく友達との飲み会の約束よりも早めについてしまったので本でも仕入れていこうかとより、適当にさまよっていると「コンテナ」という単語が目に入った。ソフトウェアエンジニアにとってコンテナは大変身近な概念だ。といってもこの本で言うところの運送のコンテナではなく、コードの実行環境を仮想化して管理する技術のことであるが、目に入ったのはなにかの縁だろうと思いカゴへ入れた。実店舗ではこのような不思議なマッチングがあるので楽しい。

https://www.amazon.co.jp/%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E7%89%A9%E8%AA%9E-%E4%B8%96%E7%95%8C%E3%82%92%E5%A4%89%E3%81%88%E3%81%9F%E3%81%AE%E3%81%AF%E3%80%8C%E7%AE%B1%E3%80%8D%E3%81%AE%E7%99%BA%E6%98%8E%E3%81%A0%E3%81%A3%E3%81%9F-%E5%A2%97%E8%A3%9C%E6%94%B9%E8%A8%82%E7%89%88-%E3%83%9E%E3%83%AB%E3%82%AF%E3%83%BB%E3%83%AC%E3%83%93%E3%83%B3%E3%82%BD%E3%83%B3/dp/4822289931

この本は運送業で使われるコンテナの歴史を記述したものだ。

ただの箱・コンテナ

巨大な港の写真などで無数のコンテナを積んだ船にクレーンが近づいていって、載せたりおろしたりしている映像は見たことがあるし、JR 四国でも貨物列車でコンテナが運ばれているのを見たことがある。僕たちがいつも買っている商品がコンテナを使って海外から送られていることも事実として知っている。ただその歴史や意味合いについて考えたことは一切なかった。なんなら一番はじめにコンテナという概念を聞いたときには、特にそれはすごいとかは思わなくて、そりゃ運ぶんだから箱ぐらいいるだろうという程度だったように思う。だって固定の箱に詰めるだけなのだから。

しかしこの本を読むとそのようなイメージは一新され、これがとてもインパクトのある概念であることが理解できるだろう。そして、面白いのは、コンテナという概念自体がとんでもない発明かというとそうではないところにある。

もちろん一番はじめにコンテナを使い始めた人は、既存の運送方法とは違う方法を取り入れるわけなので、先見性だったり実行力はあるだろう (マルコフマクリーンというおじさんがそれに該当する)。

ただマクリーンさんがすべてを生み出したわけではなく、コンテナが生まれるにはある種の必然性があり、それに彼は乗ったというかタイミングが良く出会ったようなところがあって、とうのマクリーン氏も「これやれば効率が上がるんじゃないか」程度の意識だったようだ。要するに既存の運送方法の延長線としてコンテナはあり、ちょっと効率よくなって嬉しいね程度のものということだ。

実際に当時の資料等が紹介されているが、一時しのぎのコスト削減方法としてしか見られていなかったし、専門家はコンテナ船は長距離航路では役に立たないと考えていた。専門家や経営者だけではなくて働いている労働者もそれは同じで、当時の海運業は荷物は港で働く労働者が載せたりおろしたりするのが基本だったので、労働者側からみたコンテナは荷物の一つであり、なにか交渉するにしても「そのコンテナの荷降ろしをやらせてくれ」という対象であった。みんなあまり大事だと思っていなかったのだ。

しかし、みなが侮ったコンテナは、界の物流の状況をまるっきり変えた。

世界の運送コストはそれ以前より劇的に減少して届くまでの日数・安全性も格段に向上した。その波及効果も大きくて、工場は最終消費地につくる必要はなくなったて産業構造や雇用は激変したし、東アジアが貿易拠点になったりでそれがない世界と比べて全く違うものになってしまった。読むとほんとにすげーなと思うのでぜひ読んでほしい。

なんでわからなかったのだろう

やはり気になるのは、これをなんでみんなわからなかったのだろうか? というところだ。

一つには今ある制約条件に縛られすぎたことがあると思う。

ちゃんとコンテナに対応した港は必要だし出荷する人がコンテナを意識した荷詰めをする必要もあるし、各国の規制当局との交渉もあるしで、当時はハードルはたくさんあったことが原因だけれど、結局このコンテナをフル活用したときの世界をみなが想像できなかったことがその過小評価につながったのだろうと思う。

もう一つ、こちらはコンテナゆえかもしれない原因なのだが、単なる箱なので侮ってたのでは? というところ。これは反対にすごそうに見える最先端技術が別に対して生活を変えないし利益を生まないパターンと対照的かもしれないのだけれど、やっぱり箱は箱なので今度からコンテナになるよと言われても直感的にすごいと思えないというのがあるようにも思う。

合理的予測の限界

もう一つ印象的なのは、予測が尽く外れていく様である。

p.6: 「コンテナ物語」が教えてくれるいちばん重要なものは「よきせぬ結果」が果たす役割だと思えてならない。

と著者はまえがきに書いている。全部終わった今から見るとなんでわからなかったのかとなるけれどもちろん当時の人達も皆なりに合理的に考えた結果見誤ったわけで、それは未来から見た今と重なるようにも思う。

何らかプロジェクトをしようと思うと、過去のデータをもとに定量的な予測をして計画を建てる。これは大変合理的できれいなんだけれど、その限界がわかりやすく提示されているように思える。計画から降りて今の状態についていくしなやかさでしか対応できないことの大事さを感じた。

他にも具体的にコンテナの規格を広げるときの大変さとか、荷降ろしの過去の常識とかいろいろな知識に触れられるとてもいい本だった。単なる箱だけどすごいんだね。

Vercel で 502 OPTIMIZED_EXTERNAL_IMAGE_REQUEST_UNAUTHORIZED Error に遭遇する。

前提

  • Next.js を利用したアプリケーションを vercel へデプロイして利用している。
  • 一部 item 詳細ページでは SSG を使っていて、一覧ページでは逐次別でホスティングしている API へリクエストを送る。
  • 取得した item ごとに画像が紐付いていて、画像は cloudfront でホスティングされている。
  • 画像の表示には next/image Image を利用している。したがって next.js の API 機能によって最適化がされている状態。

発生したこと

  • ローカル環境での dev では問題なく画像が表示されていて、そのまま vercel 上へ build & depoly した。deploy までは特にエラー無く実行できた。
  • 変更へ紐づくURLを preview でみてみると、殆どの画像が 502 OPTIMIZED_EXTERNAL_IMAGE_REQUEST_UNAUTHORIZED エラーになり表示できていないことがわかった。

対応した (が上手く行かなかった) こと

  • そもそも設定が間違っている可能性を考えてローカル環境で vercel がやっている(であろう)ことと同じ構成を試す。
    • 具体的には同じ env を設定したのち npm run build & npm run start をし localhost に立ったサーバにブラウザでアクセスした。すると、問題なく画像は表示された。
    • _next/iamges/ へのリクエストも問題なく通っている。
    • このときの画像ドメインはさきほどエラーがあったドメインと同じもの。
  • Web上で同じエラーメッセージで検索しても何も出てこない。vercel 公式のエラー一覧 https://vercel.com/docs/errors にもこのエラーコードがない (どういうこと?)

ここまでで困って twitter でつぶやいていたところ vercel の @jrsyo さんに tweet を見つけていただき DM で対応してもらいました。

(やさC)

結論何が問題だったか

画像の cloudfront にアクセス制限がかかっており local = ja のみアクセス可能になっていたことが問題でした。terraform の設定的に言うと、以下のような記載をしていました。

  restrictions {
    # 日本以外は許可しない設定
    geo_restriction {
      restriction_type = "whitelist"
      locations        = ["JP"]
    }

ローカル環境は日本なので、問題なくアクセスできます。ただし vercel のエンドポイントは日本以外のどこかに立っています (確かアメリカのハズ)。

したがって、ある画像@cloudfront をフロントエンドから vercel の画像最適化用のエンドポイント _next/image へリクエストすると, vercel → cloudfront へリクエストが飛びますが, vercel は日本にはないためアクセスが遮断され, vercel → cloudfront のリクエストが 403 error になり、結果フロントエンドには 502 OPTIMIZED_EXTERNAL_IMAGE_REQUEST_UNAUTHORIZED が帰る、ということでした。

日本から利用することをメインに想定するアプリを作ることが多く上記設定をテンプレートに入れていたのですが、それを忘れていたことが原因なのですが cloudfront を疑うパターンが少なかったのでハマってしまいました。

余談

vercel の管理画面やエラー詳細ではそのような表示は見つけられなかった (ゆえに cloudfront の設定を疑っていなかった…) ので、エラーの細かいものを見る画面はないですかー? と @jrsyo さんに聞いたところ、今現在は提供されていないとのことでした。エラーの中身が見えるようになると嬉しいな🦈( ‘-’ 🦈)。vercel の追加開発に期待です。

システム運用アンチパターン・本読み section2&3

社内本読みの記録です。GOTO・MEMOが僕のメモ/Discussionがその内容を社内で議論したときの記録になります。

§2: パターナリスト症候群

GOTO・MEMO

キーワード: ゲートキーパー

  • 承認における「決定権を持つ人」の比喩
  • ゲートキーパーはなにかイベントがあって発生するがあってもプロダクトのクオリティ向上には繋がっていない
    • 歴が長いグループだとゲートキーパーたくさん居るイメージ。
    • クオリティのうちのリスクの重み付けがとても大きい会社だとゲートキーパーは必須になってしまうのではないか? この作者の重み条件では最適化するとゲートキーパーは要らない、と言っているに過ぎないような (例えば医療系などミスったら大変なものはゲートキーパー必要と思う)
  • この作者が大事にしているところ
    • 人間のコスト / 煩雑さによる生産性低下
    • 人間のリスク / 「ルールを守らなかっただけでしょ!」とはいえ誰でもまちがえるくない?

自動化の方法

  • p:20 どうやってやるかの最初のステップは「みんなでメリットを共有すること」。
    • 気持ちの統一が大事。
    • 解決の方法には注目しない。
    • [atma的には] お客さんに提案するときもなにやるか先行じゃなくて先に問題を認識してもらうフェーズ入れたりしたほうが話早いかも。
  • エラー処理はこらず、全情報を出すことに集中する
    • [atma的には] フロントエンド実装とかにも言えるかも?

Discussion

ソフトウェア会社に対しての調査

  • 開発外の人が品質保証で入っても品質が上がらない
  • ウォータフォールでもアジャイルでも変わらない
  • 車の開発:外部に不必要・内部に必要なのでは? 現場の人が最終の品質担保に関わるのが良いのでは?

具体例: 開発におけるレビューというフェーズ

  • レビューはもっとも近いゲートキーパーなのではないか。
  • となると、本論通りゲートキーパーが不要なのならば、レビューも究極の形では自動のほうが良い?と言えてしまう
  • ただしレビューはゲートキーパー以外の役割(例えばコミュニケーションや指導など)もあり、正の外部性があるため採用されていると考えるのが良い。
  • レビュー基準として「レビューは早くやる」とよく言われる。これはゲートキーパー的な機能を減らすためともいいかえることができる。
  • 結論レビューに置いても、ゲートキーピング的な役割は少ないほうが良い。できる限り人のタスクを止めないように確認作業をしてマイナスの効果を少なくし、正の効果を受け取れるよう運用するべき。
  • だめな例: 他のPJからきたあまりContextがわかっていない人のレビュー
    • 単なるゲートキーピング化しており正の外部性が小さい

§3: 盲目状態での運用

GOTO・MEMO

  • アプリが何しているかを確認して共有しろ。意外と知らないよ。
    • 難しいドメインのものもそうだし、かんたんでもフローを完全に理解していないとメンタルモデルが間違っていることがあるので、注意しないといけないと思った。
  • 見るものを見ろ。取れてるものを見るな。
    • はい。
    • [atma的には] とくにぐるぐるではアプリが動いていることと問題ないことに差があるので、メトリクス定義して見るようにしたほうが良いかも。たとえば…
      • 1時間あたりの submission の増加数 (submissionが急に無くなったりするともしかすると submission POST のエンドポイントが止まっているかもしれない。)
      • あるいは、1時間あたりの submission エラーの増加数 (もしかしたらスコア計算のコードはおかしくないが、答えのデータなどの設定側がおかしくってエラーが増えているかもしれない)
  • とりあえず取れるログはいっぱい取れ。何かあったらすぐその良さに気づく。
    • 著者、なんかあったんかな…
    • [atma的には] そもそも取ってないやつが多い (cloudwatchには吐き出している?) 500エラーに対してのログなどを定期的に見る癖をつける必要があるかも。
  • logを取る系のサービスは使え。メンテコスト払えるのか?
    • 確かに。作らないのが一番バグらないと通じるところがある。
    • 自分の作業単価を考えてどの作業をするかを考える。

ref:

Discussion

よく使っているログツールのこと

  • cloudwatchlogs単体だとドメインの情報が全く乗っていない
  • アプリのコア部分・ステータスの状態遷移が多いものは何処で何が起こるかわからなくなる
  • 状態変更時にログを吐くような設計のほうが、あとあとで何が問題だったか追いやすい (ex: 対象のオブジェクトが時系列で更新される場合、ログがないとエラー時の状態を復元できない)

ref: ログをかんたんに取るツール

  • cloudwatch / amplify
  • sentry

ログを取る基準って何?

  • 複雑度が高い難しめのところ
  • 状態がころころと変わるもの (mutableに変更される object)
    • [キケン] status column を持つ
  • バッチ処理 (multi-tenant)
    • いろんなテナントまたいで処理するため何処でエラーになっているかわかるようにする
    • テナントごとにちゃんとできた・できなかったがわかるようにログを取る必要がある

どこからログを取る?

  • 全部でなくて大事なところからはじめていく
  • ビジネス的に大事なところ

ちょっと抽象的なのでルールを作りたいかも…

  • 案1: 更新系・登録系は全部対象とする
    • request / response
    • nginx / uwsgi のログに仕込む
    • AWSの ALB (ただし body は見れない)
  • ただしログイン系はマズイので除外する必要がある
  • 「全部でなくて大事なところからはじめていく」の原則に乗っ取ると、全部を一気にする〜というのはヨクナイのかも。ビジネスの重みを考えて、徐々にはじめていく。「みんなでメリットを共有すること」大事。
  • なんか結局著者が言っているところに落ち着く…この著者、天才なのでは…?!