DjangoのタスクフレームワークでAmazon SQSを利用するバックエンドを作った

Django 6.0で入ったタスクフレームワーク向けのバックエンドを、これまでに3つ作っていました。

tokibito.hatenablog.com tokibito.hatenablog.com tokibito.hatenablog.com

今回はAWSの Amazon SQS をキューに使うバックエンドを作りました。

AWSでアプリケーションを動かす場合、メッセージングにはSQSを使いたいことがあるので、そのときのために作っておきました。

django-tasks-amazon-sqs

これまでの3つと同じく、仕様を起こしてClaudeCodeにコードを書かせました。

コードはGitHubに置いています。

github.com

django-tasks-amazon-sqsというパッケージでPyPIにもアップロードしておきました。

django-tasks-amazon-sqs・PyPI

機能は次の通りです。

  • Djangoのタスクフレームワークで定義したタスクをAmazon SQS経由で実行できるバックエンドを提供
  • タスクの引数はSQSのメッセージ(JSON)に含める。データベースは使わないので、モデルもマイグレーションもない
  • タスクの queue_name がそのままSQSのキュー名になる
  • 管理コマンド run_sqs_tasks でワーカーを起動し、SQSからメッセージを受信して実行する
  • run_after による遅延実行(最大15分)
  • asyncのタスクにも対応
  • ワーカーは SIGTERM / SIGINT を受けたら実行中のタスクを終えてから終了する

8月に出たDjango 6.1にも対応していて、Django 6.0と6.1、Python 3.12〜3.14でテストしています。

tokibito.hatenablog.com

アーキテクチャ

sequenceDiagram
    participant App as Application
    participant Backend as SQSBackend
    participant SQS as Amazon SQS
    participant Worker as run_sqs_tasks

    Note over App,Worker: Task Enqueue
    App->>Backend: task.enqueue(args, kwargs)
    Backend->>Backend: Validate & serialize to JSON
    Backend->>SQS: SendMessage (payload, DelaySeconds)
    SQS-->>Backend: MessageId
    Backend-->>App: TaskResult (id, status=READY)

    Note over App,Worker: Task Execution
    Worker->>SQS: ReceiveMessage (long poll)
    SQS-->>Worker: Message (payload, ReceiptHandle)
    Worker->>Worker: Import the task, run it
    alt Success
        Worker->>SQS: DeleteMessage
    else Failure
        Note over Worker,SQS: Message left in the queue.<br/>Visible again after the visibility timeout.<br/>The redrive policy moves it to the dead-letter queue
    end

Cloud Tasks版はCloud Tasksからアプリのエンドポイントを呼び出してもらう形でしたが、SQS版はワーカーがSQSにメッセージを取りに行く形です。アプリ側に実行用のエンドポイントを公開する必要はありません。

ワーカーはロングポーリング(メッセージが届くまで最大20秒待つ受信方法)でSQSからメッセージを受け取ります。

使い方はこれまでのものとほぼ同じで、settings.pyでバックエンドを指定します。

TASKS = {
    "default": {
        "BACKEND": "django_tasks_amazon_sqs.SQSBackend",
        "QUEUES": [],
        "OPTIONS": {
            "AWS_REGION": "ap-northeast-1",
        },
    },
}

SQSのキューは事前に作っておき、ワーカーを起動します。

aws sqs create-queue --queue-name default
python manage.py run_sqs_tasks --continuous

AWSの認証情報はboto3の通常の探し方に従うので、環境変数やIAMロールで設定しておけば追加の設定は不要です。

失敗したタスクの再実行

タスクが失敗した場合、ワーカーはメッセージを削除せずにキューに残します。

SQSは受信されたメッセージを可視性タイムアウト(visibility timeout、受信後に他のワーカーから見えなくなる時間)の間だけ隠しておき、削除されなければまた受信できる状態に戻します。これで失敗したタスクが再実行されます。

何回まで再実行するかは、SQSのキューのredrive policy(再配信ポリシー)で決めます。指定した回数を超えたメッセージはデッドレターキュー(処理できなかったメッセージを移しておく別のキュー)に移されます。redrive policyを設定しないと失敗したタスクが延々と再実行されるので、設定は必須だと思ってください。

Cloud Tasks版で再送制御をCloud Tasks側に任せたのと同じ考え方で、パッケージ側では再実行の回数などは管理していません。

SQSの制約

SQSの仕様上できないことは、エラーにしています。

  • 遅延実行はメッセージの DelaySeconds を使うため、15分より先の run_after を指定すると InvalidTask になる
    • 実行時刻になるまでメッセージをキューに入れ直す実装を、将来的に追加する予定です
  • SQSにはメッセージの優先度がないので、priority を指定したタスクも InvalidTask になる。急ぎの処理は別のキューに分けて、専用のワーカーを動かす想定
  • FIFOキューは今のバージョンでは非対応

また、実行結果はどこにも保存しないので、get_result() で結果を取得することはできません。django-database-taskやdjango-tasks-redisは結果を保存していたので、ここは違いになります。

可視性タイムアウトは、一番長く実行されるタスクよりも長くしておく必要があります。タスクの実行中にタイムアウトすると、別のワーカーが同じタスクを実行し始めてしまいます。SQSの配信は「少なくとも1回」で、同じメッセージが2回届くこともあるので、タスクは何度実行されても問題ないように作っておくのが良いです。

django-database-taskのSQS対応

django-database-taskのほうにも、SQSをメッセージングに使うバックエンド(SQSDatabaseBackend)を入れています。現状はこちらのほうがフル機能です。

タスクの引数や結果はデータベースに保存して、SQSにはtask_idだけを送ります。15分より先に実行するタスクはSQSに送らずデータベースに置いておき、ワーカーがデータベースを見に行って実行するので、SQSの遅延の上限を気にせずに使えます。

Amazon SQS Integration・tokibito/django-database-task

データベースを使わずにSQSだけで済ませたい場合はdjango-tasks-amazon-sqs、という使い分けです。

ローカルでの開発

SQS_ENDPOINT_URL で接続先を変えられるので、AWSのモックライブラリの moto をサーバーモードで起動すれば、AWSのアカウントなしで試せます。

pip install "moto[server]"
moto_server -p 5000
aws --endpoint-url http://localhost:5000 sqs create-queue --queue-name default

パッケージのテストでもSQSの代わりにmotoを使っています。

READMEではLocalStackも紹介していますが、LocalStackは2026年3月からアカウント登録と認証トークンが必要になり、無料プランは非商用に限られるようになったので、手軽に使うならmotoのほうが良さそうです。

terapyon channel podcast #125 ゲスト参加

terapyon channel podcast にゲスト参加で話してきました。前回から1年ぶりです。

podcast.terapyon.net

トークの内容は近況(オープンコレクターの取締役退任など)、PyCon JP 2026でのDjangoブースの手伝い、Django 6.0のタスクフレームワーク向けライブラリやcssertの開発、Claude Codeの使い方、AI時代のエンジニアの育成についてなど。

話の中で出てきたものはこちら。

前回

tokibito.hatenablog.com

ビルド後のCSSにクラスが残っているかを検査するcssertを作った

TailwindCSSを使っているプロジェクトで、テンプレートには書いてあるのにビルド後のCSSにそのクラスが入っていない、という不具合を何度か踏んだので、それを検出するツールを作りました。

github.com

npmには @cssert/cli という名前で公開しています。cssert のままだと assert に似すぎているとnpmに拒否されたので、スコープ付きにしました。インストールされるコマンド名は cssert です。

@cssert/cli・npm

何を検出するか

TailwindCSSは、ソースコードをスキャンして、そこで見つかったクラスのぶんだけをCSSに出力します。

Tailwind v3では、設定ファイルの content にスキャンするパスを書きます。v4だとプロジェクト内が自動で対象になり、追加や除外を @source で指定する形になりました。

なので、こういうときにテンプレート側のクラスがCSSに出てきません。

  • テンプレートがスキャン対象のパスから漏れている
  • テンプレートがインストール済みのパッケージ(node_modules の中など)に置かれていて、スキャンの対象外になっている
  • text-{{ color }} のように実行時にクラス名を組み立てている

厄介なのは、ソースファイルは1つずつ見るとどれも正しい、という点です。テンプレートの書き方もCSSの書き方も間違っていないので、リンターや型チェッカーでは見つけられません。不具合があるのはビルド成果物の側です。

cssertは、ビルド済みのCSSと(できればレンダリング済みの)HTMLの2つを受け取って、HTMLで使われているのにCSSのどのセレクタにも出てこないクラスを報告します。

未使用CSSを削除するツールとは逆方向のものです。CSSの書き換えは一切せず、読み取り専用です。

使い方

npm install --save-dev @cssert/cli
npx cssert check --css "dist/**/*.css" --html "build/rendered/**/*.html"

出力はこんな感じです。

  ✗ lg:my-10          templates/pricing.html:42:18
                      templates/pricing.html:88:10
  ✗ bg-brand-500      templates/base.html:12:6
  ⚠ text-{{ color }}  templates/card.html:7:22   (dynamic class construction suspected)

  2 missing, 1 dynamic-suspect  ·  scanned 128 document(s) / 3 stylesheet(s)

✗ のmissingは、HTMLで使われているのにCSSのどのセレクタにも存在しないクラスです。これが1つでもあると終了コード1で失敗します。

⚠ のdynamic-suspectは、class 属性の中に {{ }} や {% %} のようなテンプレート構文が入っていて、最終的なクラス名が確定しないものです。存在するともしないとも言えないので、missingとは分けて報告しています。デフォルトでは失敗扱いにせず、--fail-on-dynamic を付けたときだけ失敗します。

クラスがどれか1つのセレクタにでも出現すれば「定義済み」として扱います。祖先側に出てくる .group:hover .foo も数えるので、Tailwindの group や peer が誤検出にならないようにしています。

想定する利用シーン

主な用途はサーバーサイドレンダリングのアプリケーションだと考えてます。CSSのビルドとテンプレートが別々のツールチェーンにあるので、気づかないうちにずれます。

テンプレートをそのまま検査することもできますが、class 属性の中にテンプレート構文が入っているとdynamic-suspect止まりになってしまいます。CIの中で対象ページをHTMLファイルに書き出してから、それを検査するのがおすすめです。

Django、Rails、Laravel向けのレシピをdocsに置いています。Djangoのものが一番詳しいです。

docs/recipes・tokibito/cssert

既存プロジェクトへの導入

手元のDjango + Tailwind v4 + daisyUIのプロジェクト(テンプレート35個、レンダリング27ページ)に入れてみたところ、初回で死んだクラス名が7つ、約200箇所で使われているのが見つかりました。daisyUIをv4からv5に上げたときの残骸で、ページは表示できていて見た目が少し変なだけだったので、誰も気づいていませんでした。

これだけの数を一度に直すのは大変なので、baseline でその時点の検出結果を固定して、新しく出たものだけで落とせるようにしました。

npx @cssert/cli baseline create --css "dist/**/*.css" --html "build/**/*.html"
npx @cssert/cli check ...          # .cssert/baseline.json が自動で適用される
npx @cssert/cli baseline prune     # 直したぶんを削る

prune は既に解消したエントリを落として N resolved, M remaining と出すので、修正のコミットと一緒に入れていけます。

CI

GitHub Actionsのアノテーションと、code scanning向けのSARIF出力に対応しています。

- run: npx @cssert/cli check --format github

--format github はクラス1つにつき1アノテーションを、最初の出現箇所に出します。GitHubは1回のチェックで表示できるアノテーション数に上限があるので、初回のような指摘が多いときに、1つのうるさいクラスで他が埋まらないようにしています。

HTMLとCSSを別のジョブで作っている場合、アーティファクトのダウンロードに失敗すると空のディレクトリが残って、そのまま緑になってしまいます。--min-documents で最低ドキュメント数を指定しておくと、これを失敗にできます。

緑になっても保証されないこと

検出できるのは「クラス名が存在するか」だけです。

  • セレクタの詳細度で負けている、マッチしないメディアクエリの中にある、値が間違っている、といったものは見ていません
  • class 属性を書き忘れた場合は、そもそも見えません
  • 検査していないページのことは何も分かりません

「存在して、かつこの値に解決される」を確認したい場合用に、ライブラリとして expectClass を用意しています。

import { expectClass, loadStylesheet } from "@cssert/cli";

const sheet = loadStylesheet(css);

expectClass(sheet, "bg-brand-500").toExist();
expectClass(sheet, "bg-brand-500").toResolveTo("background-color", "#0f62fe");
expectClass(sheet, "md:flex").under("(width >= 48rem)").toDeclare("display");

toResolveTo は var() を解決してから比較するので、Tailwind v4の @theme で定義した値まで追えます。

これで p-4 が 1rem になることを確認してもTailwindをテストしているだけなので、自分で定義した色やデザイントークンに対して使うものです。

作りについて

CSSを検査するツールなので、フロントエンドのビルドが動いている環境、つまりNode.jsでそのまま使えるのが汎用性が高いと考えました。CSSのパース用のパッケージもNode.js向けのものが多くあるので、TypeScriptで実装しています。パーサーにはPostCSSを使いました。

実装はClaudeCodeにやらせました。今回は先に仕様書を書かず、対話しながら仕様を固めていく形で進めています。

テストのフィクスチャは実際にTailwind v3 / v4 / UnoCSSでビルドしたCSSを使っています。

使っていって気になったらインターフェースは今後変える可能性あり。

PyCon JP 2026に参加しました

2026/8/21と8/22に広島市で開催された PyCon JP 2026 に参加してきました。

pyconjp.connpass.com

会場は昨年と同じ、広島国際会議場。

8月なので、暑くて大変かも?と予想してましたが、今年は思ったより気温が上がらなかったのか、湿度がそこまで高くなく、そんなに苦はなかったです。よかった。

色んな人に会えて、話せて楽しかったです。

来年は、沼津で開催とのこと。参加しよう。

コミュニティブース

Djangoのブースを手伝っていました。グローバル側のDjangoコミュニティ名義でApoorv Gargさんがブースを出すとのことでしたので、お手伝いをしていました。

ブースでは、ステッカーを配布し、来てくれた方と話していました。

OpenSpace

会場内には、プロジェクターと椅子、机のあるオープンスペースが設置されていました。

イベント期間中に早いもの勝ちで、予約して使える形でした。

空き枠があったので、予約をして、2日目の午前中に最近のDjangoについての雑談する会をやっていました。

tokibito.hatenablog.com

オープンスペースの枠はイベントアプリでは表示されていましたが、大きく告知されるわけではないのと、通常のセッションの時間帯でもあるため、人に来てもらうためには、自分で宣伝、人集めが必須です。

事前に声掛けなどをして、参加者10名ぐらい、思ったよりも集まってワイワイ雑談できたので良かったです。

来て頂いた方、ありがとうございました。

雑多な感想

  • 海外コミュニティから継続して来てくれてる方たちもいて、英語を引き続き話せるようにやっていく必要がある
    • 片言の英語でも飲み会は楽しかった、みんなやさしい
  • 交流のSNSが日本人だとXかDiscord、海外だとつながりはLinkedIn、コミュニティはDiscordという感じなのか、LinkedInのプロフィールをもう少し整備してもいいか、と感じた
  • 韓国のDjangoコミュニティの方が来てくれて、オンラインミートアップなどできるといいね、みたいな話があった
  • 基調講演で紹介されていたPyxelがすごいと感じた。触っておこう。
  • コミュニティ活動をしていきたい気持ちが向上した
  • もっと写真を撮っておけば良かった

PyCon JP 2026 Django雑談用

PyCon JP 2026のオープンスペースが空いていたのでDjangoの雑談会をやります。

場所: PyCon JP 2026 OpenSpace Torii 日次: Day2 (8/22) 12:00-12:45

最近のDjangoの機能やサードパーティライブラリ雑談 django.tasksやDjangoのリリースサイクルが変わる話、Djangoでの開発の話など雑談しましょう!

自己紹介

@tokibito https://x.com/tokibito

2006年末ごろからDjangoを使っています

Djangoの紹介

軽く説明 or 省略

Djangoは全部入り(フルスタック、Batteries included)のWebアプリケーションフレームワーク

  • FlaskやFastAPIとの違い
    • DjangoはORMを含む
      • DBマイグレーションも含む
    • Djangoはユーザー認証を含む
    • Djangoは管理画面(CRUD画面)を含む
    • 設定ファイルの仕組みやメール送信の仕組みも含む

最近のDjango

  • Django 6.0
    • Template Partials
      • テンプレートの部分レンダリング、HTMXなどで便利
    • Background Tasks(組み込みタスクフレームワーク)
    • CSP(Content Security Policy)のサポート
    • メールAPIのモダン化
      • Unicode標準、依存先のPythonのメールAPI変更
  • Django 6.1
    • Model Field Fetch Modes
      • フィールドの遅延取得を設定できる
    • settings.MAILERS
      • メールバックエンドを複数設定できる
    • ForeignKey.on_delete のDB側処理オプション
      • DB_CASCADE 、 DB_SET_NULL 、 DB_SET_DEFAULT

Djangoのリリースサイクルが変わる話

LTS(Long Term Support、長期サポート)バージョンが廃止される。

現行: 8ヶ月ごとのリリース、x.2バージョンがLTS(24ヶ月ごとにLTSがリリース)、LTSのサポート期間は約3年

2028から: 1年ごとのリリース、LTSバージョンは廃止、すべてのバージョンが3年サポート

django.tasks

Djangoでバックグラウンドタスクを実装するためのフレームワーク

https://docs.djangoproject.com/en/6.1/topics/tasks/

Async対応は今後される予定

Djangoにあると良さそうな機能

これは雑談ネタ

  • 私が思っているもの
    • .env のサポート
      • django-environとかみんな使ってるでしょ!
    • 権限フレームワークが使いづらいので改善されるといいなあ
      • サードパーティだとdjango-rulesとか
    • JavaScriptビルドとの協調性
      • ビルドしたファイルの取り込みを自分で調整して組むの面倒
      • キャッシュバスティングとか考えたり(ManifestStaticFilesStorageなど)
    • バイブコーディングのサポート
      • スキルかMCPか

参考

Django 6.1がリリースされました

タイトルの通り、Django 6.1がリリースされています。

www.djangoproject.com

これによりDjango 6.0はメインストリームサポートが終了、セキュリティサポートのみとなります。

Djangoフレームワークは現状、8ヶ月ごとに大きなリリースが出ます。

参考: Django’s release process | Django documentation | Django

現在メンテナンスされているのは Django 5.2 LTS、 Django 6.0、 Django 6.1となります。

次のDjango 6.2はLTSの予定で、通例なら2027年4月にリリースされる可能性が高いです。

バージョンアップは計画的にやっていきましょう!

Google App EngineでDirect VPC egressを使えるようになった

たまたまGCPコストの壁打ちでClaudeにリサーチしてもらっていたところ、Google App EngineでDirect VPC egressが使えるようになったという情報を見つけました。 プレビュー機能としてつい最近リリースされたみたい?です。リリースの情報は見つけられていない。

ドキュメント

ドキュメントは整備されていました。AppEngineのStandardランタイム環境で利用できます。

Direct VPC egress | App Engine standard environment | Google Cloud Documentation

従来のVPCコネクターからの移行手順のドキュメントもあります。

Migrate standard VPC connector to Direct VPC egress

VPCコネクターとDirect VPC egressについて

AppEngineのインフラから、VPC内にあるComputeEngineインスタンスなどにアクセスする場合、従来はVPCコネクターが必須でした。

このVPCコネクターを作成すると、最小構成でもe2-microインスタンスが2つ常時起動されるため、インスタンス費用がかかるかたちになっていました。 また、通信量が増えると、スケールアウトによりインスタンス数が増え、それにともなってコストがかかる形でした。

Direct VPC egressは、このブリッジ用のインスタンス無しでVPC内のリソースにアクセスできる仕組みです。Computeのインスタンス費用がかからない分、費用も削減できます。

Cloud Runでは正式リリース(GA)で提供されてきた機能でしたが、App Engineでも使えるようになりました。

試してみて普通に使えた

App Engineのデプロイの際に beta の指定が必要になりますが、ドキュメントの通り app.yaml を設定することで、東京リージョンにデプロイしているアプリでも問題なくVPC内のComputeEngineに接続できました。

IPレンジまわりの考え方はCloud Run向けのDirect VPC egressと同様なので、その辺はドキュメントを参照するとヨシ。

VPCコネクターを削除した状態で、確認して問題ないことを確認できたので、多少ですがコスト削減できました。ありがたい。