LinearとAWS Lambdaで構築する、攻めのコンプライアンス:リアルタイム監査ログシステムの全貌
こんにちは。開発チームの生産性を極限まで高めつつ、ガバナンスとセキュリティを担保し続けることに命をかけているテックリードです。
モダンな開発チームにとって、Linearはその圧倒的なUI/UXと爆速のキーボード操作により、ベロシティを底上げする最強の課題管理ツールです。しかし、組織がスケールし、SOC2やISMSなどのコンプライアンス要件が厳しくなると、ある深刻な課題に直面します。
「誰が、いつ、どのイシューを削除したのか?」
「機密性の高いステータス変更の履歴を、Linearの保持期間に依存せず、永続的に改ざん不可能として保存するにはどうすればいいのか?」
Linearは優れたアクティビティログを持っていますが、エンタープライズ要件を満たすためには、自社管理のストレージへリアルタイムにイベントを退避させる「独自監査ログシステム」が不可欠です。
今回は、LinearのWebhookとAWS Lambda、そしてAmazon S3/DynamoDBを組み合わせ、開発の手を止めることなく、要件を120%満たすサーバーレス監査ログ基盤の構築ハンズオンをお届けします。
—
0. 前提:プロが実践するLinear環境のチューニング
コードを書く前に、チーム全体の生産性をブーストするためのLinearの「隠し味」を共有しておきます。ここを整えておかないと、どれだけログ基盤を作っても開発体験は向上しません。
開発スピードを劇的に高める神ショートカット
マウスに手を伸ばした瞬間、エンジニアのフロー状態は途切れます。以下を体に叩き込んでください。
- `C`:どこにいても新規イシュー作成 (Create)
- `G` → `I`:マイイシュー一覧へジャンプ
- `Cmd + K` (Mac) / `Ctrl + K` (Win):何でも呼び出せるコマンドメニュー(ここからイシュー検索やラベル変更が秒速で行えます)
- `O`:選択中のイシューをプレビューしつつ、`P`で担当者変更、`S`でステータス変更
チームのサイロ化を防ぐ設定共有ルール
- Workflowの厳格化: ステータス(Backlog, Todo, In Progress, In Review, Done, Canceled)は全チームで統一。特に `Canceled` と `Done` の扱いは監査ログに直結するため、カスタムステータスを乱立させないこと。
- Labelsの命名規則: `security`, `compliance`, `breaking-change` などのタグ付けルールを徹底し、Webhook側でフィルタリングできるようにする。
—
1. システムアーキテクチャの全体像
今回構築するアーキテクチャは以下の通りです。完全サーバーレスで運用コストはほぼゼロ(数セント)です。
[ Linear Webhook ]
│ (HTTPS POST + HMAC Signature Validation)
▼
[ API Gateway (HTTP API) ]
│
▼
[ AWS Lambda (Node.js / TypeScript) ]
├──► [ Amazon DynamoDB ] (高速検索・直近の変更履歴トラッキング)
└──► [ Amazon S3 ] (改ざん検知・長期保管用イミュータブルストレージ)
1. Linearでイシューの作成・更新・削除が発生。
2. LinearのWebhook機能がAWS API GatewayへHTTPS POSTを送信。
3. AWS Lambdaがリクエストを受け取り、HMAC署名(X-Linear-Signature)を検証してリクエストの正当性を担保。
4. イベントデータをパースし、検索用の DynamoDB と、コンプライアンス保管用の S3 に並列書き込み。
—
2. 実装:AWS Lambdaファンクション(TypeScript)
セキュリティと堅牢性を担保するため、TypeScriptを用いてLambda関数を実装します。LinearからのWebhookは不正リクエストを防ぐため、必ず署名検証を行います。
`package.json` & 依存関係
{
“name”: “linear-audit-logger”,
“version”: “1.0.0”,
“description”: “Linear Webhook to S3/DynamoDB Audit Logger”,
“main”: “dist/index.js”,
“dependencies”: {
“@aws-sdk/client-dynamodb”: “^3.400.0”,
“@aws-sdk/client-s3”: “^3.400.0”
},
“devDependencies”: {
“@types/aws-lambda”: “^8.10.119”,
“@types/node”: “^20.4.0”,
“typescript”: “^5.1.6”
}
}
`index.ts` (メインの監査ログ処理ロジック)
import { APIGatewayProxyEvent, APIGatewayProxyResult } from ‘aws-lambda’;
import as crypto from ‘crypto’;
import { S3Client, PutObjectCommand } from ‘@aws-sdk/client-s3’;
import { DynamoDBClient } from ‘@aws-sdk/client-dynamodb’;
import { DynamoDBDocumentClient, PutCommand } from ‘@aws-sdk/lib-dynamodb’;
// AWSクライアントの初期化(リージョンは環境変数から取得)
const s3Client = new S3Client({});
const dynamoClient = new DynamoDBClient({});
const ddbDocClient = DynamoDBDocumentClient.from(dynamoClient);
const S3_BUCKET_NAME = process.env.AUDIT_S3_BUCKET || ”;
const DYNAMODB_TABLE_NAME = process.env.AUDIT_DYNAMO_TABLE || ”;
const LINEAR_WEBHOOK_SECRET = process.env.LINEAR_WEBHOOK_SECRET || ”;
/
- LinearからのWebhook署名を検証する
/
function verifyLinearSignature(signature: string, rawBody: string, secret: string): boolean {
if (!signature || !secret) return false;
const hmac = crypto.createHmac(‘sha256’, secret);
const digest = hmac.update(rawBody).digest(‘hex’);
// タイミング攻撃を防ぐ安全な比較
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
export const handler = async (event: APIGatewayProxyEvent): Promise
const signature = event.headers[‘X-Linear-Signature’] || event.headers[‘x-linear-signature’];
const rawBody = event.body || ”;
// 1. セキュリティチェック:署名検証
if (!verifyLinearSignature(signature, rawBody, LINEAR_WEBHOOK_SECRET)) {
console.error(‘Invalid Webhook Signature’);
return {
statusCode: 401,
body: JSON.stringify({ message: ‘Unauthorized: Invalid signature’ }),
};
}
try {
const payload = JSON.parse(rawBody);
const { action, type, data, createdAt, organizationId } = payload;
const eventId = event.headers[‘X-Linear-Delivery’] || `${Date.now()}-${crypto.randomUUID()}`;
console.log(`Received Linear Event: type=${type}, action=${action}, id=${data?.id}`);
// 2. 長期保管用としてAmazon S3へJSONをイミュータブル保存
// パーティション戦略: year=YYYY/month=MM/day=DD/timestamp-action-id.json
const dateObj = new Date(createdAt || Date.now());
const year = dateObj.getUTCFullYear();
const month = String(dateObj.getUTCMonth() + 1).padStart(2, ‘0’);
const day = String(dateObj.getUTCDate()).padStart(2, ‘0’);
const s3Key = `audit-logs/org=${organizationId}/year=${year}/month=${month}/day=${day}/${eventId}-${type}-${action}.json`;
await s3Client.send(new PutObjectCommand({
Bucket: S3_BUCKET_NAME,
Key: s3Key,
Body: rawBody,
ContentType: ‘application/json’,
ServerSideEncryption: ‘AES256’,
}));
// 3. 高速検索用としてAmazon DynamoDBへインデックス保存
// イシューが削除された場合でも、最後の状態と削除イベントを記録
await ddbDocClient.send(new PutCommand({
TableName: DYNAMODB_TABLE_NAME,
Item: {
id: data?.id || eventId, // プライマリキー (イシューIDまたはイベントID)
timestamp: dateObj.getTime(), // ソートキー等に活用
eventType: type, // 例: “Issue”, “Comment”
action: action, // “create”, “update”, “remove”
actorId: data?.updatedBy?.id || ‘system’, // 操作を行ったユーザーID
organizationId: organizationId,
fullPayload: payload, // 全ペイロードのバックアップ
ttl: Math.floor(Date.now() / 1000) + (60 60 24 365 2), // 2年後に自動削除(必要に応じて変更)
},
}));
return {
statusCode: 200,
body: JSON.stringify({ message: ‘Audit log recorded successfully’ }),
};
} catch (error) {
console.error(‘Error processing Linear webhook:’, error);
return {
statusCode: 500,
body: JSON.stringify({ message: ‘Internal Server Error’ }),
};
}
};
—
3. インフラ構成のベストプラクティス (Terraform / CloudFormation)
この監査ログ基盤をコード(IaC)として管理するための、Terraformのベストプラクティス構成例を提示します。コンプライアンス要件を満たすため、S3バケットのバージョニングとパブリックアクセスブロックは必須です。
`main.tf`
provider “aws” {
region = “ap-northeast-1”
}
1. 長期監査ログ保存用 S3 バケット
resource “aws_s3_bucket” “audit_logs” {
bucket = “my-company-linear-audit-logs-bucket”
}
resource “aws_s3_bucket_versioning” “audit_logs_versioning” {
bucket = aws_s3_bucket.audit_logs.id
versioning_configuration {
status = “Enabled”
}
}
resource “aws_s3_bucket_public_access_block” “audit_logs_block” {
bucket = aws_s3_bucket.audit_logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
2. 高速検索用 DynamoDB テーブル
resource “aws_dynamodb_table” “audit_table” {
name = “LinearAuditLogs”
billing_mode = “PAY_PER_REQUEST”
hash_key = “id”
range_key = “timestamp”
attribute {
name = “id”
type = “S”
}
attribute {
name = “timestamp”
type = “N”
}
ttl {
attribute_name = “ttl”
enabled = true
}
server_side_encryption {
enabled = true
}
}
—
4. Linear側の設定手順とテスト方法
基盤の準備ができたら、Linear側でWebhookを有効化します。
1. Linearのワークスペース設定(Settings)を開く。
2. API または Webhook メニューに移動。
3. 「Create webhook」をクリック。
4. URL: AWS API GatewayのエンドポイントURLを入力。
5. Subscriptions: 監査対象とするイベント(`Issue`, `Comment`, `Project` など。すべて選択するか、セキュリティ上重要な `Issue` の `create`, `update`, `remove` を選択)にチェックを入れる。
6. Secret: 生成された(または任意の)シークレット文字列をコピーし、AWS Lambdaの環境変数 `LINEAR_WEBHOOK_SECRET` に設定する。
動作確認(ローカルデバッグの極意)
LinearのWebhookは直接ローカル環境に向けるのが難しいため、開発時は ngrok や AWS Lambdaのコンソールにある「テストイベント機能」 を活用します。
Linearが送信する実際のペイロード構造(サンプル)をLambdaのテストイベントに仕込んで実行します。
{
“headers”: {
“X-Linear-Signature”: “ここに正しいHMAC署名文字列を入れる”,
“X-Linear-Delivery”: “test-delivery-id-001”
},
“body”: “{\”action\”:\”remove\”,\”type\”:\”Issue\”,\”createdAt\”:\”2023-10-25T10:00:00.000Z\”,\”organizationId\”:\”org-uuid-1234\”,\”data\”:{\”id\”:\”issue-uuid-5678\”,\”title\”:\”Sensitive Bug Fix\”}}”
}
これでS3にオブジェクトが生成され、DynamoDBにアイテムが書き込まれていれば構築完了です。
—
5. テックリードからの総括
今回構築したシステムにより、以下のメリットがチームにもたらされます。
1. コンプライアンス監査の完全パス: Linear側のデータ保持ポリシーに縛られず、社内規定に応じた期間(例: 5年間)セキュアにログを保持できます。
2. インシデント発生時のフォレンジック強化: 「誰がいつ重要イシューを消したか・書き換えたか」をタイムスタンプ付きで即座に追跡可能です。
3. 開発体験への影響ゼロ: エンジニアはいつものようにLinearを爆速で操作するだけで、背後でサイレントにコンプライアンスが担保されます。
ツールはただ使うだけでなく、その裏側のイベントをハックして組織の盾とすることで、真の価値を発揮します。ぜひ、あなたのチームのLinear環境にもこの仕組みを導入し、セキュアで高速な開発ライフサイクルを実現してください。