今回は、OSSのプロジェクト管理ツール「Plane」をAWSにセルフホストします。設計から見積、EC2の起動、Docker Composeでのアプリ起動、ブラウザで開くところまで、AWSの操作はすべてコーディングエージェントに任せ、人は判断と承認だけを担当します。
きっかけは、使っているプロジェクト管理SaaSの料金プランが2027年1月に改定されることでした。改定を機に、代わりになるカンバンツールをセルフホストで評価してみることにしました。評価の結果は「不採用」でしたが、その判定と片付けは別の記事に分け、ここでは立ち上げるまでの過程を書きます。
AWSの操作を任せるリスクは、前回、参照専用の環境を作ったときに整理しました。今回は構築なので、リソースの作成・変更ができる権限を渡します。その代わりに、破壊的な操作(削除・セキュリティグループの変更)は実行前に人へ確認すること、入口はセキュリティグループの許可リスト(443のみ)を崩さないこと、SSHは開けずSSM Session Managerを使うこと、を最初にルールとして書いて渡しました。
着手前に、手順書と「不採用」の分岐を書かせる
最初にしたのは、作業手順書を作らせることでした。エージェントに目的とルールを読ませて始めたところ、こちらが「フェーズ0が何を指しているのか、どこまでフェーズがあるのか不明」と聞き返すことになったからです。口頭の説明で終わらせず、手順書のファイルとして起こしてもらいました。
| フェーズ | 何をする | 完了条件 | 課金 |
|---|---|---|---|
| 0. 設計 | 公式要件の当日確認・サイズとTLS方式の決定・見積 | 設計書の未確認項目がゼロ | なし |
| 1. デプロイ | VPC/SG/EC2/EIP → SSM接続 → Plane起動 | ブラウザからログインできる | ここから開始 |
| 2. 評価+運用整備 | まず採用・不採用を判定 →(採用なら)バックアップ・招待 | 結論が出ている | 継続 |
| 3. AI連携 | 週報生成・要約など | 終わりを定めない | 継続 |
手順書を書かせると、どこで人の承認が要るかが自動的に洗い出されます。今回は、着手承認、セキュリティグループの作成、採用判断と各社のIP登録、の3か所でした。
もう一つ、この段階で「大きな目的はPlaneを立ち上げて評価すること」と伝え直しました。既存の文書には「業務で使う」と書かれていて、立ち上げれば即本番、という流れになっていたためです。エージェントはフェーズ2の先頭に「評価(判定は人間)」の関門を追加し、不採用ならフェーズ2以降を中止して撤収する、という分岐を手順書に書き込みました。作れてしまうと採用したくなるので、判定基準は着手前に固定しておくのが安全です。
公式ドキュメントの当日確認 — 無料版は別ルートだった
Planeのセルフホスト手順は変化が速いので、着手当日に公式を確認することもルールにしました。エージェントは検索結果の個人ブログを採用せず、公式の開発者向けドキュメントとGitHub Releasesを一次情報として読みました。
ここで最初のつまずきが出ます。公式ドキュメントのDocker Compose・SSL・バックアップ・アップグレードの各ページには「Commercial Edition」のラベルが付いていました。AGPLで無料のCommunity Editionは、GitHub Releasesに同梱された setup.sh を使う別ルートで、内蔵の自動SSL(Let’s Encryptの自動取得・更新)は有料版限定でした。
エディションは、こちらの判断でCommunity Editionにしました。無料でライセンスキーが不要、想定人数(13〜30人)に対してシート上限の縛りがない、という理由です。代償はTLSを自前で用意することで、その分の手間は先に受け入れました。
見積は料金APIに聞く — Gravitonで21%、無料枠は口座に聞く
見積は、料金表の転記や記憶に頼らせず、AWS Pricing API(aws pricing get-products)で東京リージョンの単価を取らせました。2026年9月6日に取得した実値です。
| 項目 | 単価(東京・オンデマンド) |
|---|---|
| t3.large(x86・2vCPU/8GB) | $0.1088/時 |
| t4g.large(Graviton・2vCPU/8GB) | $0.0864/時 |
| t4g.medium(Graviton・2vCPU/4GB) | $0.0432/時 |
| EBS gp3 | $0.096/GB・月 |
| パブリックIPv4アドレス | $0.005/時 |
| データ転送(外向き) | $0.114/GB(月100GBまで無料) |
同じ2vCPU/8GBでも、ARMのGravitonを選ぶとx86より約21%安くなります。ただし「公式がARM対応と書いている」だけでは足りません。エージェントはDocker HubのAPIでPlaneの全イメージを確認し、v1.4.2はamd64とarm64の両対応、ただし preview タグだけはamd64専用、という事実を見つけました。ARMの機械で preview を掴むと起動しません。stable かバージョン固定タグを使う、という制約を設計書に書きました。
無料枠についても、一般論ではなく口座に聞きました。
あ、AWSって無料枠ってありましたっけ?
エージェントは aws freetier get-free-tier-usage を実行し、この口座に残っているのは Always Free の4件(CloudWatch・DynamoDB)だけで、12ヶ月無料の項目はゼロ、と返しました。IAMユーザーの作成日から口座は1年半以上前で、無料枠は使ったかどうかではなく開設からの経過時間で切れます。使えるのは外向きデータ転送の月100GBだけで、これは見積の転送費を$0にしている根拠になりました。
Google Cloudのほうが安いか、も聞きました。t4g.medium が $31.54/月、e2-medium が $31.39/月で、差は月$0.15でした。効き目が大きいのは、使わない時間は停止する(−70%)、ARMを選ぶ(−20%)、適正サイズで始める(−50%)の順で、ベンダー間の差はそれより桁が小さいと分かったので、比較はここで打ち切りました。
料金表を読み違えた — SaaSの料金表とOSSの制限は別物
エージェントは設計の途中で、こう報告してきました。料金表を読む限り、Privateプロジェクト(招待した人だけが見える)はProプラン以上のようだ、今回の要件の核は会社ごとに見えるものを分けることなので、Community Editionで使えないと要件を満たさない、と。最大リスクとして提示されました。
Private プロジェクト(招待した人だけが見える)は Pro($6/seat/月)以上のようです。もちょっと意味がわかりません。単純にアクセス制限をつけるってことではないのですか?
この質問を受けて、エージェントはCommunity Edition v1.4.2のソースを直接確認しました。プロジェクトのモデルには NETWORK_CHOICES = ((0, "Secret"), (2, "Public")) があり、一覧APIは非メンバーにPublicしか返さず、Secretへの個別アクセスは403を返す、という制御が入っていて、そのコードパスに有料版のゲートはありませんでした。
結論は訂正されました。Community EditionでもSecretプロジェクトは使えます。料金表の「Public + Private」はPlane Cloud(SaaS版)のプラン差であって、セルフホストのバイナリの制限ではなかった、ということです。公式ページを読んでいても、それがマーケティング資料なら誤ります。AGPLなのでソースを読めば確定する、というところまで降りていなかった、というのが原因でした。

併せて整理したのが、この3層です。①はネットワークの層で、セキュリティグループの許可リストが止めます。②はPlaneのワークスペースで、招待されていない人には存在ごと見えません。③はプロジェクト単位の可視性で、Public か Secret かを選びます。会社をまたいで一緒に見る場所が要るなら1ワークスペース+Secretプロジェクト、要らないなら会社ごとにワークスペース、という使い分けになります。
小さく始める — t4g.medium と「コンテナとは何か」
見積の初版は t4g.large(8GB)で月約$72でした。公式が本番8GBを推奨していたためです。
もう少し安いインスタンスの利用では難しいですか?
エージェントは方針を「小さく始めて、詰まったら上げる」に変えました。公式の最小要件は2コア/4GBで、8GBは本番推奨の数字です。評価段階で本番推奨を満たす必要はなく、停止→タイプ変更→起動で数分、データはそのまま、なので読み違えても戻せます。EBSは50GBから30GBに減らしました。gp3はオンラインで拡張できますが縮小はできないので、小さく始めるのが正しい方向です。結果、t4g.medium・EBS 30GB・EIPで月約$39になりました。
この途中で、こちらから一つ質問をしました。
12コンテナってのはなんですか? コンテナってなんの単位でしょうか?
エージェントが説明なしで使っていた前提語でした。返ってきた説明は、EC2(仮想マシン)が建物1棟で、コンテナはその中の部屋、OSは建物のものを共有するので部屋は軽く秒で開け閉めできる、というものです。Planeは画面・API・裏方の処理・定期実行・データベース・キャッシュ・伝言板・ファイル置き場と役割ごとにプログラムを分けているので約12個になり、部屋ごとにメモリを使うから合算が効いて、最小4GB・本番推奨8GBという数字になります。

インバウンドを1つも開けずに構築する
フェーズ1に入ります。VPC(10.0.0.0/16)、パブリックサブネット、インターネットゲートウェイ、ルートテーブルを作り、EC2用のIAMロールには AmazonSSMManagedInstanceCore だけを付けました。すべてのリソースに Project=plane のタグを付け、リソースIDは文書に書かず、タグで引き直せるようにしました。
セキュリティグループは、インバウンドのルールがゼロの状態で作りました。SSM Session Managerはインスタンス側からのアウトバウンドで成立するので、構築作業中はインバウンドを1つも開ける必要がありません。443の許可リストは、実際にブラウザで見る段階まで不要です。危険な操作を、必要になる瞬間まで後ろにずらせます。
EC2は t4g.medium、Amazon Linux 2023(arm64)、EBS gp3 30GB(暗号化)、IMDSv2必須、キーペアなしで起動しました。起動からSSMがOnlineになるまでは約10〜20秒でした。22番ポートを一度も開けず、キーペアも作らずに、SSM Run Commandでコマンドが通るところまで到達しています。
小さなつまずきもありました。Run Commandに渡す文字列の括弧をクォートせずsyntax errorになったこと、aws ssm list-commands の出力に余計な None 行が混ざって後続の呼び出しが失敗したこと(| head -1 で回避)です。
Plane本体の導入では、setup.sh を v1.4.2 でURL固定して取得し、先に中身を読ませました。install() の中に read -p "Continue? [y/N]" がありますが、これは選択したアーキテクチャのイメージが無い場合だけ通る分岐です。ARM対応を事前に確認済みだったので、非対話で完走できると判断できました。読まずに叩いていたら、入力待ちで止まって原因が分からなかった可能性があります。対話式インストーラをエージェントに叩かせる前に、スクリプトを読ませる、というのは今回の教訓の一つです。
./setup.sh install と ./setup.sh start を経て、DBマイグレーションが完了しました。stable タグは v1.4.2 に解決され、取得したイメージは合計約3.2GB、ディスクは30GB中10GBの使用でした。設計書には「Redis」と書いていましたが、実際に起動したのは Valkey(valkey/valkey:7.2.11-alpine)でした。コンテナ名は plane-redis-1 のままなので、名前だけ見ていると気づきません。
稼働中のコンテナは12個、定義は13個で、差分はDBの構造を更新して自分で終了する使い捨ての migrator です。説明した数と docker compose ps の表示数は一致しない、という点は、読者が同じ違和感を持つところだと思います。
メモリの実測は 3,830MB中1,698MB、スワップは4,095MB中1MBでした。t4g.medium の4GBで足りています。利用者ゼロの状態の数字なので、人を入れてからの再測定が前提ですが、フェーズ0で8GBに寄せかけたのを4GBで始めた判断は、この時点では正しかったことになります。

ドメインも証明書もなしで、ブラウザから開く
AWSへのアクセスではドメインが必要なんですか?
サーバーへの到達にドメインは要りません。IPアドレスで届きます。ドメインが要るのはHTTPS(証明書)のためだけです。Let’s Encryptは2026年1月15日からIPアドレス向けの証明書も発行していますが、有効期限が160時間の短期プロファイル限定で、IPにはDNS-01チャレンジが使えないため、検証のたびに80番か443番を外に開ける必要があります。「443のみ・登録済みの固定IPのみ」という入口方針に穴が開くので、採用しませんでした。ドメインを使う本当の理由は、証明書そのものより、DNS-01が使えるようになることです。DNSに一時レコードを書いて証明するので、サーバーのポートを一切開けずに証明書を取得・更新できます。
評価の段階ではドメインを買わないことにしました。SSMのポートフォワードを使えば、インバウンドを開けずに手元のブラウザから使えます。トンネル自体が暗号化されています。ドメインが要るのは、各社が直接アクセスする本番投入の時点で、不採用ならドメイン代も発生しない順序です。
ここでもう一つ、エージェント作業らしい壁が出ました。Mac側に session-manager-plugin を入れる brew install --cask が、sudo のパスワード入力で失敗しました。エージェント経由のシェルには対話端末がなく、pkgインストーラがroot権限を要求するためです。回避策は、AWS公式の配布zipから実行ファイルだけを取り出して ~/.local/bin に置くことでした。pkgインストーラを通さないのでsudoが不要で、バージョンはcaskと同じ1.2.835.0です。多くのCLIツールはインストーラが管理者権限を要求するだけで、中身は単一のバイナリです。
aws ssm start-session --target <インスタンスID> \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["80"],"localPortNumber":["8080"]}'
Macの localhost:8080 がEC2の80番につながり、HTTP 200でPlaneの初期画面が返りました。この時点で、セキュリティグループのインバウンドは0個、ドメインも証明書もありません。
最後のつまずきは、管理者アカウントの作成フォームを送信したらボタンがスピナーのまま固まったことです。エージェントに聞くと、APIコンテナのログに POST /api/instances/admins/sign-up/ 302 とユーザーIDが残っていて、作成自体は成功、失敗したのはその後のリダイレクトだと切り分けました。原因は WEB_URL=http://localhost にポート番号が入っておらず、80番へ飛ばされていたことです。WEB_URL と CORS_ALLOWED_ORIGINS を http://localhost:8080 に直して解決しました。ポートフォワードで使う場合、アプリ側の「自分のURL」を転送先のポートに合わせる必要があります。Plane固有ではなく、リダイレクトやCORSを使うWebアプリ全般で起きます。
同じログに SECURITY: SECRET_KEY is set to a known insecure or placeholder value という警告も出ていました。インストーラの既定値のままだったので、データが空のうちにランダムな値へ差し替えました。既定値は動く値であって安全な値ではない、ということをアプリ自身が教えてくれた形ですが、ログを見なければ気づきません。
EC2の起動からブラウザで開けるまで、この日の実作業は約1時間でした。ここまでの費用は、次の記事の最後にまとめて書きます。
まとめ
コーディングエージェントにAWSを操作させて、Community EditionのPlaneをt4g.medium 1台に立ち上げ、ブラウザで開くところまで到達しました。この間、セキュリティグループのインバウンドは0個のまま、SSHもキーペアもドメインも証明書も使っていません。
人がしたのは、手順書と不採用の分岐を先に書かせること、エディションとインスタンスサイズの判断、そして「意味がわからない」と聞き返すことでした。料金表の誤読が訂正されたのも、「12コンテナとは何か」の説明が記事に入ったのも、その聞き返しからです。
この後、実際に触って評価し、不採用と判定して、リソースを削除しました。その過程と、最後に残った費用は「Plane環境の削除」に書いています。
出典・参考: Plane Developers「Self-hosting methods」/同「Editions and versions」/GitHub makeplane/plane Releases(v1.4.2・2026年8月23日)/Let’s Encrypt「6-day and IP address certificates general availability」(2026年1月15日)/株式会社ヌーラボ「Backlog プラン改定に関するお知らせ」(2026年6月17日)。AWSの単価は2026年9月6日に AWS Pricing API(東京リージョン・オンデマンド)で取得した値です。
