AWSでCloudWatchを活用していると、「ログの保持コストをできるだけ抑えたい」という要望はよくあります。
その対策として、よりコスト効率の良いS3にログを出力し、CloudWatch側のログは削除するという方法が1つの選択肢となります※1。
また、CloudWatchではログ内に個人情報が含まれないよう、マスキング機能※2を利用することがあります。
しかし、コスト削減のためにS3へログを出力する際に、マスキングが外れてしまっては本末転倒です。
そのため、S3出力時にマスキングの状態がどうなるのか、しっかり確認していきましょう。
※1:Cloudwatchの保存料金はUSD 0.033/GB。S3(標準)の保存料金は0.025USD/GB。 (2024/6時点の東京リージョンでの料金)
※2:https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/mask-sensitive-log-data.html
検証準備
Cloudwatchでマスキング設定
マスキング対象のログ
今回マスキング対象のログとして下記のサンプルを使用します。
本サンプルでマスキング対象となるのは、「password」と「mailAddress」になります。
{
"id": "199",
"password": "cX3T*FMZ",
"mailAddress": "hogehoge@gmail.com",
"createdAt": "2020-02-20T11:00:28.107Z"
}
マスキングの設定
マスキングの設定は「Cloudwatch->設定->ログ」から行い
「メールアドレス」、「パスワード」のマスキングはそれぞれ下記の機能を利用します。
- メールアドレスのマスキング:データ保護アカウントポリシー
- パスワードのマスキング :カスタムデータ識別子
メールアドレスのマスキング
メールアドレスは、[1]にあるようにAWS側でマスキングの機能が用意されているためこちらの機能を使用します。
[1]保護できるデータの種類
https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/protect-sensitive-log-data-types.html
【設定手順】
①:データ保護アカウントポリシー設定を選択
②:アカウントポリシーからマスキングの対象としてEmailAddressを選択し、データ保護をアクティブ化する
パスワードのマスキング
メールアドレスとは違い、パスワードはマスキングできる対象に含まれていないようなので
カスタムデータ識別子を利用して、マスキングを行います。
【設定手順】
①:データ保護アカウントポリシーの設定を選択 (メールアドレスのマスキングの手順①と同様)
②:アカウントポリシーからカスタムデータ識別子を選択し、パスワードを検出する正規表現を設定し、データ保護をアクティブ化する
今回使用した正規表現("password"[^,]*")は
「"password"から始まり、シングルクオーテーションまで」をマスキング対象としています。
マスキングの結果を確認
マスキング対象のログ を実際にCloudWatchに出力した結果が下記になります。
→ 「メールアドレス」と「パスワード」がマスキングされています。
CloudwatchのログをS3に出力する
Lambda作成
CloudwatchからS3へ出力するLambdaを作成します。(Node.js 20.x)
CloudwatchからS3への出力は、「CreateExportTaskCommand」を使用します。
import { CloudWatchLogsClient, CreateExportTaskCommand, DescribeExportTasksCommand } from "@aws-sdk/client-cloudwatch-logs";
const client = new CloudWatchLogsClient({ region: "your-region" });
export const handler = async (event) => {
// パラメータの設定
const logGroupName = 'your-log-group-name';
const bucketName = 'your-bucket-name';
// 現在の日時を取得
const now = new Date();
const year = now.getUTCFullYear();
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
const day = String(now.getUTCDate()).padStart(2, '0');
// プレフィックスの設定 (ロググループ名/yyyy/mm/dd)
const destinationPrefix = `${logGroupName}/${year}/${month}/${day}`;
// エクスポートタスクの時間範囲を設定
const startTime = now.setHours(now.getHours() - 1); // 1時間前
const endTime = Date.now(); // 現在
// エクスポートタスクの作成
try {
const createCommand = new CreateExportTaskCommand({
taskName: 'ExportLogsToS3',
logGroupName: logGroupName,
from: startTime,
to: endTime,
destination: bucketName,
destinationPrefix: destinationPrefix
});
const createResponse = await client.send(createCommand);
const taskId = createResponse.taskId;
console.log(`Export task created with ID: ${taskId}`);
// エクスポートタスクのステータスを確認
const describeCommand = new DescribeExportTasksCommand({
taskId: taskId
});
let taskStatus = 'PENDING';
let taskMessage = '';
while (taskStatus === 'PENDING' || taskStatus === 'RUNNING') {
const describeResponse = await client.send(describeCommand);
const exportTasks = describeResponse.exportTasks;
if (exportTasks && exportTasks.length > 0) {
const task = exportTasks[0];
taskStatus = task.status.code;
taskMessage = task.status.message || '';
console.log(`Task Status: ${taskStatus}, Message: ${taskMessage}`);
}
if (taskStatus === 'PENDING' || taskStatus === 'RUNNING') {
await new Promise(resolve => setTimeout(resolve, 10000)); // 10秒待機
}
}
if (taskStatus === 'COMPLETED') {
console.log('Export task completed successfully.');
} else {
console.log(`Export task failed with status: ${taskStatus}, message: ${taskMessage}`);
}
return {
statusCode: 200,
body: `Export task completed with status: ${taskStatus}, message: ${taskMessage}`
};
} catch (error) {
console.error(`Error creating or describing export task: ${error}`);
return {
statusCode: 500,
body: `Error creating or describing export task: ${error}`
};
}
};
Lambdaが使用するロールに適切な権限を付与
S3へ出力する権限をLambdaに持たせるため、下記の内容をLambdaにアタッチするIAMロールに含めます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateExportTask",
"logs:DescribeExportTasks",
"logs:CancelExportTask"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::your-bucket-name/*"
}
]
}
S3バケットポリシーの修正
Cloudwatchからのログ出力を受け止められるようにS3のバケットポリシーに下記を設定します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "logs.ap-northeast-1.amazonaws.com"
},
"Action": "s3:GetBucketAcl",
"Resource": "arn:aws:s3:::your-bucket-name",
"Condition": {
"StringLike": {
"aws:SourceArn": "your-log-group-arn"
}
}
},
{
"Effect": "Allow",
"Principal": {
"Service": "logs.ap-northeast-1.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::from-cloudwatch-to-s3-yy/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control"
},
"StringLike": {
"aws:SourceArn": "your-log-group-arn"
}
}
}
]
}
検証結果
マスキング対象のログ をCloudwatchに出力後、Lambda作成 で用意したLambdaを実行し、S3に出力されたログファイルを確認すると、マスキングされたまま出力されました。
本記事の内容を実施することで、ログ保持コストを25%削減することができ、コスト負担を軽減しながらセキュリティレベルを維持することが可能ですので、ぜひやってみてください!
◆S3に出力されたログ
2024-06-15T04:39:21.385Z test
2024-06-15T04:35:41.269Z { "id": "199", **********************, "mailAddress": "******************", "createdAt": "2020-02-20T11:00:28.107Z" }