さて、世の中ではAI開発全盛期という感じで、とにもかくにも全盛期という感じになってきているので、今回は私自身の個人的な取り組みについて書いていく。
あらかじめ断りを書いておくと、世の中の人ほど活用できてはいない。
LLMをどのような開発に使っているか?
今のところは簡単なツール作りや、実装の部品作りといった、小規模用途に利用している。
LLMをなぜ開発に使っているか?
単純な話、開発工数の圧縮だ。
実際にLLMを使ってみた事例
事例1:MarkdownをYAMLに変換するスクリプトの作成
私は職務経歴書をWordで作っているのだが、WordそのままだとLLMに読ませるのはしんどい。
そこでLLMに読ませる形にしたいが、Markdownはそこまで構造的ではない。なのでYAMLにすることにした。
単刀直入にやるならWordファイルの中身をLLMに渡し、「これをYAMLにしてください」というのが恐らく一般的で、最も手っ取り早いだろう。
しかし私はそれを避けた。私の経歴書はかなり長く、LLMがどこかでミスをしていた場合、それをレビューするのは至難の業だからだ。例えば経歴の一部がLLMによって書き換えられていたりするリスクは拭えない。
なので私はLLMにWordファイルから切り出したプレーンテキストをYAMLに変換するスクリプトを書くことを依頼した。これであればレビューするのはコードそのもので良いし、静的コードは基本的に文脈を見て判断して勝手に書き換えることはしないし、仮にそんな処理があったとしてもそんなものはコードを見ればわかるからだ。
作らせたのはSESや受託系の経歴書にありがちな、死ぬほど繰り返される案件ごとの経験セクションだ。職務経歴書の中でも職務要約だとか、活かせる経験みたいな単純な文章セクションは手作業でどうにもなるので、そこは手で移植すればいいが、繰り返しはしんどい。
というわけで次のようなものを書いてもらった。これで9割はうまく変換してくれたし、後はざっと目検して行って変換にしくじっている部分を直して対応した。
なお、この作業を行ってから時期が過ぎており、当時のファイルやプロンプトが喪失した状態からVSCodeの作業履歴をもとに復元しているため、実際の条件と合致しないため、あくまでイメージとして残しておく。要はLLMに変換させるのではなく、LLMに変換ツールを作らせると間違いが少ないという話だ。
#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
binmode(STDIN, ':utf8');
binmode(STDOUT, ':utf8');
my $input = do { local $/; <STDIN> };
# 複数のプロジェクトを分割(日付パターンで分割)
my @projects = split(/(?=\d{4}\/\d{2}\s*-\s*\d{4}\/\d{2})/, $input);
print "projects:\n";
for my $project (@projects) {
next if $project =~ /^\s*$/;
parse_project($project);
}
sub parse_project {
my ($text) = @_;
# 時期を抽出
my ($period) = $text =~ /^(\d{4}\/\d{2}\s*-\s*\d{4}\/\d{2})/m;
return unless $period;
# タイトル(時期の後の行)
my ($title) = $text =~ /^\d{4}\/\d{2}\s*-\s*\d{4}\/\d{2}\s*\n(.+?)(?:\t|規模|$)/m;
$title //= '';
$title =~ s/^\s+|\s+$//g;
# 要員数
my ($members) = $text =~ /要員数\s*\t?\s*(\d+名)/;
$members //= '';
# 役割
my ($role) = $text =~ /役割\s*\t?\s*([^\t\n]+)/;
$role //= '';
$role =~ s/^\s+|\s+$//g;
# プロジェクト概要
my ($overview) = $text =~ /【プロジェクト概要】\s*\n(.*?)(?=\n\s*\n|\n【|$)/s;
$overview //= '';
$overview =~ s/^\s+|\s+$//g;
$overview =~ s/\n\s*/\n/g;
# 主な業務
my ($tasks) = $text =~ /【主な業務】\s*\n(.*?)(?=\n\s*\n|\n【|$)/s;
$tasks //= '';
$tasks =~ s/^\s+|\s+$//g;
# 実績・取り組み
my ($achievements_text) = $text =~ /【実績・取り組み】\s*\n(.*?)(?=\t要員数|\n開発環境|$)/s;
$achievements_text //= '';
# 開発環境セクションを抽出
my ($dev_env_text) = $text =~ /開発環境\s*\n(.*?)$/s;
$dev_env_text //= '';
# 出力
print " - 時期: $period\n";
print " 内容: $title\n";
print " 要員数: $members\n";
print " 役割: $role\n";
# プロジェクト概要
print " プロジェクト概要: |-\n";
for my $line (split /\n/, $overview) {
print " $line\n" if $line =~ /\S/;
}
# 主な業務
print " 主な業務:\n";
print " - $tasks\n";
# 実績・取り組み
print " 実績・取り組み:\n";
parse_achievements($achievements_text);
# 開発環境
print " 開発環境:\n";
parse_dev_env($dev_env_text);
}
sub parse_achievements {
my ($text) = @_;
# 実績項目をパース(タイトル + 説明の形式)
my @items;
my $current_title = '';
my $current_desc = '';
for my $line (split /\n/, $text) {
$line =~ s/^\s+|\s+$//g;
next if $line eq '';
# 新しい項目タイトル(短い行で次の行に説明がある)
if ($line =~ /^(.+?)$/ && length($line) < 30 && $line !~ /。$/) {
if ($current_title) {
push @items, { title => $current_title, desc => $current_desc };
}
$current_title = $line;
$current_desc = '';
} else {
$current_desc .= ($current_desc ? "\n" : '') . $line;
}
}
if ($current_title) {
push @items, { title => $current_title, desc => $current_desc };
}
for my $item (@items) {
print " - $item->{title}: |-\n";
for my $desc_line (split /\n/, $item->{desc}) {
print " $desc_line\n" if $desc_line =~ /\S/;
}
}
}
sub parse_dev_env {
my ($text) = @_;
my %sections;
my $current_section = '';
for my $line (split /\n/, $text) {
$line =~ s/^\s+|\s+$//g;
next if $line eq '';
if ($line =~ /^【(.+?)】$/) {
$current_section = $1;
$sections{$current_section} = [];
} elsif ($current_section) {
# カンマやスペースで分割し、バージョン番号を除去
my @items = split /,\s*/, $line;
for my $item (@items) {
$item =~ s/^\s+|\s+$//g;
# バージョン番号を除去(数字とドットのパターン)
$item =~ s/\s*[\d.]+\s*$//;
$item =~ s/\s+\d+(\.\d+)*$//;
push @{$sections{$current_section}}, $item if $item =~ /\S/;
}
}
}
# 順序を維持して出力
my @section_order = ('言語・FW', 'インフラ', 'ドキュメント', 'CI/CD', 'VCS');
for my $section (@section_order) {
next unless exists $sections{$section};
print " $section:\n";
for my $item (@{$sections{$section}}) {
print " - $item\n";
}
}
# 定義順以外のセクションも出力
for my $section (keys %sections) {
next if grep { $_ eq $section } @section_order;
print " $section:\n";
for my $item (@{$sections{$section}}) {
print " - $item\n";
}
}
}
入力に使ったワードファイルのフォーマットは以下のような内容だ。
[企業情報A]
[案件情報1]
[案件情報2]
...
[企業情報B]
[案件情報1]
[案件情報2]
...
ツールの想定漏れにより一部手修正したり、ツールを作らせるほうが面倒な部位などは手作業で直しているが、低いレビューコストと最低限の手作業で冠水でき、LLMに読ませて分析させられる経歴書を作れたので、結果としてこれはよかった。
事例2:adiaryのMarkdownパーサーへの脚注記法の追加
これは重い腰を上げてadiaryのMarkdownパーサーを脚注記法に対応させたでも書いた内容だ。
端的に言うと既存コードを読ませて部品程度の機能を書いてもらった。
そのまま愚直に書いてもうまく動かなったので、結合する部位などは適当に手直ししているが、これもLLMを使ったコーディングが役に立った一例だ。
まとめ
現状そこまでバチバチに使えているかといわれると、そこまで使えていないのが正直なところだとは思う。
例えばサブエージェントやスキルといったものや、MCP、RAG、ファインチューニング、Loraといったものは活用できていない。
まぁ徐々に使えるようになっていければいいのかなぁというところで、程々にやっていきたい。
投稿日:
なんか昔のやり方が使えなくなってたので調べたメモ。
確認環境
| Env | Ver |
|---|---|
| OS | Ubuntu 24.04.3 LTS |
| Docker | version 28.1.1, build 4eba377 |
| Docker Compose | v2.35.1 |
| Mastodonのバージョン | v4.5.0-alpha.2 |
| Mastodonのコミットハッシュ | 06803422da3794538cd9cd5c7ccd61a0694ef921 |
手順
DEVELOPMENT.md#dockerの手順通りにやると行ける。
docker compose -f .devcontainer/compose.yaml up -d
docker compose -f .devcontainer/compose.yaml exec app bin/setup
docker compose -f .devcontainer/compose.yaml exec app bin/dev
コンテナへのアタッチ方法
# 方法1
docker compose -f .devcontainer/compose.yaml exec app zsh
# 方法2
cd .devcontainer
docker compose exec app zsh
VSCodeがあるならDockerやRemoteほげほげ系の拡張を使い、devcontainer-appにAttach Shellしてもよい。
検証ユーザーの作成
- 次のコマンドでユーザーを作る
./bin/tootctl accounts create hoge --email hoge@example.com --confirmed - ユーザーの承認を行い、必要に応じてパスワードを使いやすいものに変更する
rails console user = Account.find_by(username: 'hoge').user user.approve! # PW変更ここから user.password = 'password' user.skip_confirmation! # PW変更ここまで user.save! quit
フロントエンドのビルドとサーバー起動
bin/rails assets:precompile
bin/dev
トラブルシューティング
localhostではアクセスできるが、マシンのホスト名やhoge.testのようなローカルドメインでアクセスできない
.env.developmentにALTERNATE_DOMAINS=hoge.testの行を追加することで他のドメインでもアクセスできる。
この設定があるとMastodonを別環境で動かしててリモートからドメインアクセスする場合に便利。
ActiveRecord::PendingMigrationErrorなど、ActiveRecord関係のエラーが出る
コンテナの中でrails db:setupを叩くことで解決する。
Unable to load application: NameError: uninitialized constant LetterOpenerWebというエラーが出る
リポジトリルートにあるdocker-compose.ymlを使うと発生するので、.devcontainer/compose.yamlを使う事で解決する。
あとがき
検証用にバニラなMastodon環境が欲しくて作ってみたが昔と変わっていて地味にハマった…。
今までLLMを使う場合、文書校正や整理、ERP辺りが多かったが、そろそろコード作成にも必要だなと感じたので取り組んでみた結果の初回の雑感。
Claude Opus 4との直接対話
LLMエージェントを使わない、チャットインターフェースでの直接対話で行ってみたこと。これはClaude Opus 4で行っている。
簡単なボイラープレートやプログラムが関の山
正直、LLMとの単純な対話で作れるのは3カラムのハンバーガーメニュー付きのような画面のボイラープレートや、WebでJSを使った画像判定スクリプトあたりが関の山だと感じている。
それ以上のものも作れる可能性はあるが、要件定義とコードレビューが大変なので厳しい気がしている。
プログラムの変換は苦手
まず私はTampermonkeyで5分ごとにAPIをポーリングし、結果をパースして条件に応じてOSに通知トーストを出す、400行ほどのスクリプトを作っている。
そこで、このソースコードを丸っと渡して、C#.NETに変換してほしいと頼んでみたが、これは失敗した。根本的にビルドが通らないコードが出てきて多少の修正でどうにかなるレベルでもなく、全くダメだった。
ファイル構成もよくなく、ModelやControllerレベルではファイル分割されているものの、1ファイルの中に複数クラスが納められていたり、何ともな結果だった。
TSDocを書いているため、上手く推論できればInterfaceやClassも作れると思ったが、これは難しいようだった。
特定の設定方法を書くのは得意
OpenWrtの特定の設定を書かせることは得意だった。これはそのまま適用できた。やはりスコープが限定されているのが得意だと感じた。
Claude Codeを少しつついてみた感想
ファイル保存などの手間がいらなくなる
当たり前だがローカルマシン上に結果を出力するため、チャットインターフェースのように頑張ってファイルを保存したり、ディレクトリを切る必要は全くなくなる。
ボイラープレートの作成は得意
PHPを利用したMVC構成で簡単なブログをフルスクラッチで作ってほしいといえば、それらしい形のものは出してくれた。
動くかどうかは全く試していないが、大まかなスケルトンを作って貰って、そっからいじっていくベースとしては使えるような気がした。
やっぱりプログラムの変換は苦手
adiaryのテンプレートエンジン部分をPHPに書き換えてほしいと依頼してみたが、やはり動かないものが出てきた。adiaryの設計が極めて複雑でコンテキストが読み取りづらいのはあると思うが、やはりこの手の作業は苦手なようだ。
現状で見えてきたこと
そこまで大して使ったわけではないが、とりあえず所感として。
恐らく小規模でコンテキストの薄いコードを書かせるのが筋がよさそう。これは複雑な要件をLLMに伝えるのは難しいし、考えるのも大変なのと、コード変換も400行レベルでも厳しいと感じたからだ。
つまり、既存システムの移行は苦手なのではないかと思っている。なのでWordPressをGoで作り直すみたいなことは相当難しいと思う。逆にSOLID原則やClean Architectureのような、スコープが狭く責務が明確なものは作りやすいのではないかと感じた。
また仮にLLMが全て書いてくれるとしても、人がレビューしないとバグがあった時に当たりをつけるのが大変とか、知らない仕様が紛れ込んだりとかもあるため、LLMに書かせすぎるべきではなく、あくまで補助ツール程度に留めておくのが良いと考えている。
LLMの制約を味方にする開発術という記事を見た感じ、複雑なタスクを段階的に分解し、LLMの処理可能な単位に分解することが重要だと感じている。つまりこれは疎結合のほうが向いているということだ。また標準化されていて、属人性がないコードのほうが制約が少なくなるので、LLMもやりやすくなるだろう。これは標準化されておらず、属人性が高いコードは往々にしてカオスで、判断軸がなく、LLMの思考がぶれるからだと思われる。
結局どうしていくか
正直まだどう実用化していくかの展望は見えていない。
何はともあれ使い続けていくことが大切な気はしているので、個人的にはClaude Codeを使い続けていきたい。少なくとも面倒なボイラープレートを書く部分については非常に優秀なので、大まかに作らせて微調整するみたいな用途では間違いなく活路がある。こういうのは引き出しが多ければ多いほど活用できるだろうから、基礎を忘れないように自学していくことも引き続き重要で、LLMに教えてもらうのもいいだろう。適切に使えばLLMからは多くの学びを得られる。
普段でWeb系のコードを書いているときに、こういうのがよいのではないか?と考えていることを雑に書いてゆく。いわゆるクラスを使わず関数を主体にして実装するなんちゃって関数型モデルみたいなやつの話。雑に書いているのでところどころ変なかもしれない。コードはNext.jsを題材にして書いている。
端的に言うとSOLIDを軸に、適切にDRY、KISS、YAGNIといった一般的な設計原則を織り込み、MVCのように役割分担のあるアーキテクチャを利用することや、実装方式を統一することを意識している。
実装方式の統一
実装方式、コーディングスタイルを統一することで、微妙な書き方の差異に起因する不具合や、レビュー時のコスト、新人が来たときの迷いを減らせる。恐らくこれこそが最も重要な設計原則だと思う。
例えば以下のコードは書き方は違うがやりたいことは似ている処理群だ。もしチームに新人が来た場合、新人はどちらを書けばよいだろうか?レビューの時に複数の実装パターンが混ざったときにレビュアーはどう対応すればいいだろうか?
// undefinedの判定方法の違い。よほど特殊なことをしているのでなければ、基本的に後者でいい
if (typeof hoge === 'undefined') { }
if (hoge === undefined) { }
// 関数の定義方法の違い。どちらかに統一するほうが望ましい
function func1(param: T) { }
const func1 = (param: T) => {}
// 仮引数の定義方法の違い。これは分割代入とそれ以外で致命的に振る舞いが変わることがあるので統一することが望ましい
const func2 = ({ hoge, piyo }: { hoge: string, piyo: number }) => {}
const func2 = (param: { hoge: string, piyo: number }) => {}
type Func2Param = {
hoge: string;
piyo: number;
}
const func2 = (param: Func2Param) => {}
何よりこれらの処理は同じような処理に見えて、微妙に振る舞いが変わるものがある。それを意識せずコードスタイルとして表現していた場合に不具合を引き起こすトリガーになりやすい。特に分割代入はオブジェクトの参照が切れるので予期せぬ動作になりやすいし、関数のスコープが長くなってきたときに由来が解りづらくなるケースがある。逆に分割代入をしないことによって変数名が冗長になることもある。
こういった問題を解決するために、コードの書き方をあらかじめチームで決めておくのがよい。特にチーム開発でコードの書き方に秩序がないプロジェクトは品質が低い傾向があると感じる。例えば車のタイヤを全て別のメーカーにしていても車は走行できるが、基本的にメーカーが違う場合、品質などの差異があるので、追々予期せぬ問題が発生する可能性がある。それと同じ問題がコードにもあると私は考えている。
他にも同じことがしたいのに複数の実装方法があるとgrepしづらかったり、直感的ではないなど、コードを読んだ人間の認知負荷の増大に繋がるため、基本的には統一されていたほうがよいだろう。
MVCのように役割分担のあるアーキテクチャ
私が普段考えているレイヤリングはMVCとClean Architectureを足して二で割ったようなものだ。DDDや厳密なClean Arctectureだと複雑で重くなりがちなので、このくらいが丁度いいのではないかと考えている。これでも十分複雑なのは承知している。
| コンポーネント | 役割 |
|---|---|
| Adaptor | APIコールなど、外部と接続するためのアダプタ関数。呼び出し処理以外を一切含まない。 ここでデータの操作や例外処理は行わず、必要な場合は呼び出しの前後で行う |
| Util | 全体的な共通処理や、画面単位の細かいロジック類 |
| Controller | 画面で利用するイベントコールバック処理。useEffect()みたいなのもここ |
| State | 画面で利用する状態 |
| View | 画面のビュー、ほぼ純粋なJSX/TSXで、booleanによる表示分岐のうち、単純なもののみ扱う。ネストしたり、複合条件を使った分岐は扱わず、必要に応じてUI Components側に移譲する |
| UI Components | 画面のビューで利用するUI部品、状態はすべてprops経由で受け取り、自身では持たない |
| Usecase | Pageコンポーネントに埋め込む存在。画面のController, State, Viewの繋ぎ合わせと、Pageコンポーネントから来たgetServerSideProps()やgetStaticProps()の結果を適切なコンポーネントに注入する |
| Page | ページコンポーネント。Usecaseコンポーネントの埋め込みとgetServerSideProps()やgetStaticProps()の呼び出しのみを行う。 |
| getServerSideProps getStaticProps |
getServerSideProps()やgetStaticProps()で処理するコードを書く |
SOLIDを意識する
リスコフの置換原則と、インターフェース分離の原則については、今回のケースでは無視でいいと思っている。
| 原則 | 効能 |
|---|---|
| 単一責任の原則 | 関数の実装を単一責務として切り出すことで、関数の肥大化を防げ、再利用性を高めることができるほか、単体テストが書きやすくなる |
| 開放閉鎖の原則 | 関数のインターフェースを抽象化し、不変のものとすることで仕様変更などの変化に対応しやすくなる |
| 依存性逆転の原則 | その関数に必要な依存性をインターフェース経由で注入することで、関数内の処理が関数本体に依存しなくなり、疎結合になるほか、注入する依存性をモックなどに変えることでテストが容易になる |
単一責任の原則
一つの関数には一つの責務だけを持たせようという原則だ。
一例としてReactでよくみられるreducerは以下のように書ける。これはreducerはaction.typeに応じた関数を呼び出すことを責務として、実際の処理は関数に移譲するといった内容だ。
const reducer = (state, action) => {
switch (action.type) {
case 'update_account':
updateAccountProc(state, action.payload);
break;
...
}
}
この実装であればreducer関数のテストは呼び出す関数をすべてモックしておき、action.typeに応じた関数が、想定通りの引数で呼ばれているかどうかを確認するだけでいい。各関数は関数単体でテストが書けるのでシンプルだ。
もしこれが関数呼び出しではなく、処理がベタで書いてあると単体の規模が大きくなり、コードの見通しが悪くなり、比例してテストコードも長くなり、個人的にはあまりよくないと思っている。コードの見通しは良いほうがよいし、責務ごとに関数を切ることでコードマージ時のコンフリクトの規模を抑えられることや、Feature flagを導入しやすくなるなど、利点は多いと思う。
欠点としては細切れになることによって、パッと見何をしているかわかりづらくなることがあると思うが、適切に抽象化し、関数名をちゃんとつけていれば基本的にそこは考慮しなくてよいと考えている。
開放閉鎖の原則
この原則は拡張に対しては開かれており、修正に対しては閉じられていないといけないということだ。
例えば以下のコンポーネントではUsernameという単語がAccountNameに代わったときにPropsを書き換える必要があるが、もしもuserNameというワードを使わずにonChangeやvalueといった抽象的な用語にしておけば、変更を不要にできる。
type Props = {
onUserNameChanged: (username: string) => void;
userName: string;
}
export const UserNameInputField = (props: Props) => {
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
props.onUserNameChanged(event.target.value);
};
return <input type="text" onChange={onChange} value={props.userName} />;
}
多くのライブラリやAPIではメソッド名やプロパティ名は汎用的な名称になっていることが多く、アップデートで名前が破壊されることはそこまで多くない。こういった風に実装しておくことで内部実装への影響なく処理ができるという寸法だ。
依存性逆転の原則
これは下位コンポーネントに実装を持たせず、上位コンポーネントから中小を介して実装を注入する原則だ。
例えば以下のような実装がある場合、「この辺りに長い色んな処理があるものとする」の部分を仕様変更などで書き換えないといけなくなることがある。
type Props = {
onChange: (value: string) => void;
value: string;
}
export const UserNameInputField = (props: Props) => {
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
/**
* この辺りに長い色んな処理があるものとする
*/
props.onChange(event.target.value);
/**
* この辺りに長い色んな処理があるものとする
*/
};
return <input type="text" onChange={onChange} value={props.value} />;
}
しかし、props.onChangeの中に前後の処理を挟んでいれば、これを回避することができるし、onChangeの中に処理がうじゃうじゃあるのも責務として過剰だと思うので、親からonChangeに対し何かしらの処理をすることが自明な関数を注入することで、こういった問題を回避できる。
DRYについて
この原則については、基本的に過度に意識する必要はなくシステム全体の共通部品以外は二重実装を許容して構わないと考えている。
これは余りにもDRYにしすぎると、仕様変更などで影響箇所が広がりすぎて修正が困難になるからというのと、共通部品が多すぎると探すのが大変で、最悪自力で実装されるケースもあるためだ。
KISSについて
程度問題ではあるが、DDDのような冗長で難解な設計原則は避け、ある程度単純なものにしたほうがいいというのは思っている。
べた書きをすることでコードジャンプが減って見通しがいいという意見は行き過ぎだと考えている。
YAGNIについて
これは技術選定フェーズで主に使うもので、何かの技術を入れるとき、その技術である必要がなければ使わないのがよいと考えている。
例えばWeb画面からAPIを叩くときにGraphQLを採用するケースがよくあると思うが、RESTやWebAPIで済むならそちらのほうがよい可能性がある。
GraphQLはWeb標準ではなく、ライブラリによって実装もまちまちで、複雑な仕組みが絡み合っており、全容を理解するのはなかなか難しいうえに、HTTPの上にあるため、使いこなすためには基礎の理解が必要だ。スキーマ設計も難しく、単純にやるとWebAPIと特に変わらないものができてしまう。そう考えたときに、絶対にGraphQLでないといけないというケースがないのであれば、より簡単な技術を使ったほうがよいと思っている。
複雑なものを作っている時間で休憩でもしたほうが頭がすっきりするだろう。
処理の境界を挟んだ時に影響が波及しないようにする
これはAPI通信など、外部から貰ってきたデータがあったときに、内部向けにデータ変換を行うということだ。
例えばAPIからisHoge, isPiyo, isFugaというデータが来るパターンがあり、この三つのフィールドはいずれか一つしかtrueにならないケースがあるとして、画面ではこれに対応するラジオボタンを持っている場合、trueだった項目を文字列として持っておくと便利だ。こういった場合に、使う場所で毎回変換処理を挟むと将来的に修正漏れが起きるリスクが増えたり、個々の場所で微妙に違う実装になって実装方式に統一がなくなったり。そもそも出現箇所が増えて認知負荷が増えるなど厄介だ。
これを防ぐために、一番最初にレスポンスを受けた時点で変換処理を挟み、以後それを引き回すというのがよいと考えている。APIに戻すときはisHoge, isPiyo, isFugaのフォーマットに戻す必要があるのが、その場合はAPIを叩く前に逆変換の処理を挟むと、データ変換処理が入り口と出口だけに存在することになり、データフローがシンプルになる。
他にもAPIと画面の境界でENUMの内容を変換する処理を入れておくとAPI側がしれっとリファクタなどで名称変更をしたときに、異常値を検出しやすくなるし、画面の開発者は基本的にAPIのことを考えなくてもよくなるので楽になると思っている(境界部分を設計する人間だけが考えればよくなる)
あとがき
雑に書こうとしたものの、結局まとまりきらず、まとめようとしても永遠に終わらなかったので一旦吐き出してみた。多分全部話そうとすると発散しすぎて収拾がつかなくなるので、個々について深ぼったことを書いて、最後にそれをまとめたほうがいいのかもしれない。
といっても何を書くかが問題になってくるので、日々思ったことを雑にアウトプットしつつ、あとで振り返ってまとめていくのがいいのかもしれない。
投稿日:
adiaryをローカルで開発するための動かす環境構築のログ。
前提条件
- サーバー構成は前段にnginx、後段にapache2を配置し、adiaryはapache2側で動作させる
- 動作URLは
http://adiary.example.test/のようなサブドメイン付きURLとする- adiary.cgiなしでアクセス可能にする
- 開発を楽にするため、ホームディレクトリからシンボリックリンクを飛ばし、VSCodeで編集できるようにする
- nginxやapache2、perlは既にインストールされているものとする
- apache2はポート8080でListenしているものとする
確認環境
| Env | Ver |
|---|---|
| OS | Ubuntu 24.04.3 LTS |
| nginx | 1.26.1 |
| apache2 | 2.4.58 (Ubuntu) |
| adiary | 3.51a |
手順
この手順では手描きした内容をteeに書き直しており、正しくファイルが生やせるか見てないので注意。
/etc/nginx/nginx.confのhttpセクションのincludeの手前にapacheのupstream情報を追記するupstream apache { server [::1]:8080; }nginx→apache2のリバプロ設定の作成
cat <<'EOF' | sudo tee /etc/nginx/conf.d/adiary.conf server { listen [::]:80; client_max_body_size 100M; server_name adiary.example.local; access_log /var/log/nginx/adiary.example.access.log; error_log /var/log/nginx/adiary.example.error.log; location / { proxy_pass http://apache/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } } EOF sudo service nginx restartApache2でadiaryをホスティングするバーチャルホスト設定の作成
cat <<'EOF' | sudo tee /etc/apache2/sites-enabled/001-adiary.conf <VirtualHost *:8080> # internal use only domain ServerName adiary.example.local ServerAdmin webmaster@localhost DocumentRoot /var/www/html/sites/adiary # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, # error, crit, alert, emerg. # It is also possible to configure the loglevel for particular # modules, e.g. #LogLevel info ssl:warn ErrorLog ${APACHE_LOG_DIR}/adiary-error.log CustomLog ${APACHE_LOG_DIR}/adiary-access.log combined # For most configuration files from conf-available/, which are # enabled or disabled at a global level, it is possible to # include a line for only one particular virtual host. For example the # following line enables the CGI configuration for this host only # after it has been globally disabled with "a2disconf". #Include conf-available/serve-cgi-bin.conf </VirtualHost> EOF/etc/apache2/apache2.confを開き<Directory /var/www/>のセクションを次のように書き換えるOptions Indexes FollowSymLinks AllowOverride All Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch AddHandler cgi-script .cgi Require all grantedOptions +ExecCGI -MultiViews +SymLinksIfOwnerMatchの行は標準のserve-cgi-bin.confからパクってきたのでOptions +ExecCGIだけでも動く可能性があるAllowOverride Allはindex.htmlを残したまま表示させないために.htaccessを書くのに使う
mod_rewriteとmod_cgiを有効化し、apache2を再起動
sudo a2enmod rewrite sudo a2enmod cgi sudo service apache2 restart/var/www/html/sites/adiaryを作成し、adiaryの動作環境一式を配置- 開発し易いように権限を緩和
chmod 777 -R /var/www/html/sites/adiary sudo chown -R <your-user-name>:<your-user-name> /var/www/html/sites/adiary/ /var/www/html/sites/adiary/.htaccessにadiary用の.htaccessを書くDirectoryIndex RewriteEngine On # favicon RewriteRule ^favicon\.ico /path/to/icon.png [L] # 正規URLは通す RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*) adiary.cgi/$1 [L]
Apache2でadiaryを動かす理由
結論から書くとnginx + fcgiwrapだとtry_filesの仕様的にadiaryが行うパス解析が上手く行かずに正常に動作しないためだ。
元々ModRewrite前提のCGIとして設計されており、個人的なユースケースがCGIとしての動作であることから、Apacheで動かすことにした。
adiaryの作者がcgiラッパーを作っているため、これを使えばnginxでも動かせそうだが、adiary専用のデーモンを起動するのもなんか嫌というのも理由の一つとしてある。
何より現在このブログが動作している環境が、さくらのレンタルサーバーであり、構成的にnginx + Apacheであるため、この構成を作ることに興味があったというのもあるし、現状のローカル環境ではnginxをフロントに立たせて後ろで様々なものを動かしている都合で、フロントはnginxで捌いた方が管理がしやすいのもある。
