前回でアーキテクチャは完成しましたが、「動く」ことと「運用できる」ことは別物です。
本番環境で動かすボットには、予期せぬエラーや悪意ある操作に対する堅牢性が求められます。
今回は、Mediatorパターンだからこそ実現できる、一元化されたエラーハンドリングとセキュリティ対策について解説します。

コマンドがクラッシュしたら?
今の実装では、もし execute メソッドの中で die が発生すると、ボット全体のプロセスが死んでしまったり、ユーザーに何も返事が返ってこなかったりします。これを防ぎましょう。
Mediatorの dispatch メソッドを修正し、eval (または Try::Tiny) でコマンド実行を包みます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| use Try::Tiny;
sub dispatch {
my ($self, $text, $user_role, $user_name) = @_;
for my $cmd (@{$self->commands}) {
if (my $args = $cmd->match($text)) {
# ... 権限チェック ...
my $result_msg;
try {
# 実行をトライ
$result_msg = $cmd->execute($args);
# 成功イベント
$self->notify_observers({
type => 'success',
# ...
});
}
catch {
# エラー捕捉
my $error = $_;
$result_msg = "💥 コマンド実行中にエラーが発生しました: $error";
# エラーイベント
$self->notify_observers({
type => 'error',
command => ref($cmd),
error => $error,
user => $user_name,
});
};
return $result_msg;
}
}
# ...
}
|
このように一箇所修正するだけで、すべてのコマンドに対してエラーハンドリングが適用されます。これがMediatorの強みです。
エラー通知用Observer
エラーが発生したときだけ、開発者チームのチャンネル(#dev-alerts)にメンションを飛ばしたいとします。
これも新しいObserverを作るだけで実現できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| package Bot::Observer::ErrorHandler;
use Moo;
with 'Bot::Observer::Role';
sub update {
my ($self, $event) = @_;
# エラーイベント以外は無視
return unless $event->{type} eq 'error';
my $alert_msg = sprintf(
"🚨 *Panic Alert*\nUser: %s\nCommand: %s\nError: ```%s```",
$event->{user},
$event->{command},
$event->{error}
);
# 開発者チャンネルへ通知
send_slack_alert($alert_msg);
}
|
タイムアウト処理
デプロイコマンドなどが応答しなくなり、無限ループに入ったら?
Perlの Sys::SigAction や alarm を使って、コマンド実行に制限時間を設けることも、Mediatorなら一箇所で済みます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| use Sys::SigAction qw( set_sig_handler );
# ... dispatch内で ...
try {
my $timeout = 30; # 秒
eval {
local $SIG{ALRM} = sub { die "Timeout\n" };
alarm $timeout;
$result_msg = $cmd->execute($args);
alarm 0;
};
if ($@) { die $@ }; # エラー再送出
}
catch {
if (/Timeout/) {
$result_msg = "⏱️ 処理がタイムアウトしました (${timeout}秒)";
}
# ...
};
|
セキュリティ:Slack User IDマッピング
第5回でさらっと user_role を引数で渡していましたが、実際にはSlackのUser ID(例: U012ABCDEF)から権限を解決する必要があります。
これは、Webhookを受け取る層(Controller)で行うのが適切です。
1
2
3
4
5
6
7
8
9
10
| my %USER_ROLES = (
'U01234567' => 'admin', # nobu
'U99999999' => 'guest', # other
);
# Webhookハンドラ内
my $slack_user_id = $payload->{event}->{user};
my $role = $USER_ROLES{$slack_user_id} // 'guest';
my $response = $mediator->dispatch($text, $role, $slack_user_id);
|
このように、「認証(誰か)」は入り口で、「認可(何ができるか)」はMediatorで行うという役割分担を意識しましょう。
次回はいよいよ最終回。これまでのすべてを統合し、実際に動く「指令センター」としての完成形をお見せします。
そして、この設計が私たちの開発チームに何をもたらしたのか、ChatOpsの真の価値について語ります。
今回の完成コード
エラーハンドリングとセキュリティ対策を追加した Bot::CommandMediator です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| package Bot::CommandMediator;
use Moo;
use Try::Tiny;
use Sys::SigAction qw( set_sig_handler );
# ... プロパティ定義 ...
sub dispatch {
my ($self, $text, $user_role, $user_name) = @_;
for my $cmd (@{$self->commands}) {
if (my $args = $cmd->match($text)) {
if ($cmd->can('required_role') && $cmd->required_role ne $user_role) {
return "⛔ 権限が不足しています(必要権限: " . $cmd->required_role . ")";
}
my $result_msg;
try {
my $timeout = 30;
eval {
local $SIG{ALRM} = sub { die "Timeout\n" };
alarm $timeout;
$result_msg = $cmd->execute($args);
alarm 0;
};
if ($@) { die $@ };
$self->notify_observers({
type => 'success',
command_name => ref($cmd),
user => $user_name,
message => $result_msg,
});
}
catch {
my $error = $_;
if ($error =~ /Timeout/) {
$result_msg = "⏱️ 処理がタイムアウトしました";
$error = "Timeout";
} else {
$result_msg = "💥 エラーが発生しました: $error";
}
$self->notify_observers({
type => 'error',
command => ref($cmd),
error => $error,
user => $user_name,
});
};
return $result_msg;
}
}
return "不明なコマンドです。";
}
1;
|