ファイルアップロードの理想像を求めて 3. cdk-serverless-clamscan編
Agaroot IT Partners(AITP)のtomoです。
ファイルアップロードシステムに関するシリーズの最終回になります。
今までの記事は下記2本になりますのでよければご覧ください。
本記事ではS3バケットにアップロードされたファイルに対して、ウイルスチェックを行ってから公開するまでの方法を紹介できればと思います。
この記事はこのような方におすすめ
- S3バケット上のファイルのウイルスチェックを簡単に行いたい方
- ウイルスチェックを行ってからファイルを公開する方法を探している方
要約
- cdk-serverless-clamscanを用いればウイルスチェックが簡単に行える
- ウイルスチェック後のファイル公開はS3×CloudFront×Lambda関数で
前回までのおさらい
第2弾記事でできたこと
第2弾記事において、S3バケットにファイルを直接アップロードすることができました。これでファイルサイズ制限などの問題も解消です。
残された課題
残された、今回取り組む課題は次の2つです。
- ウイルスチェックを行う
アップロードされたファイルに対してウイルスチェックを行い、安全性を高めます。 - ファイルの公開
ウイルスチェックの結果安全だとわかったファイルを公開します。今回は高負荷にも耐えられるようにCDNも導入します。
cdk-serverless-clamscan
上記で述べた課題(特にウイルスチェック実施)のために用いるのが、cdk-serverless-clamscanです。これはS3バケット上のファイルを、ClamAVを用いてウイルスチェックを行うライブラリ(厳密にはコンストラクトと呼ぶようですが、ここでは分かりやすくライブラリとします)です。
これを用いれば、S3バケットにファイルがアップロードされると自動でウイルスチェックを行い、その結果を様々なリソース(LambdaやEventBridgeなど)に渡すことができます。

https://github.com/awslabs/cdk-serverless-clamscan/blob/main/serverless-clamscan.png
前述の通り、S3バケットにファイルをアップロードする仕組みはできています。
あとはcdk-serverless-clamscanを用いてウイルスチェックを行い、Lambda関数を呼んでファイル公開処理を行えば万事解決です。
いざ実装
早速実装方法を説明していきます。
コードの全体は第1弾記事にて紹介していますので、本記事ではその中から重要な点を抜粋していきます。
cdk-serverless-clamscanでのリソース作成
コード記述の流れとは多少異なるのですが、処理の流れに沿って説明していったほうがわかりやすいかと思うので、そのようにいたします。
まずはcdk-serverless-clamscanでのリソース作成を行なっていきましょう。
下記コードの通り、ServerlessClamscanのインスタンスを新規作成することでリソースが立ち上がります。
new ServerlessClamscan(this, "S3ClamAV", {
buckets: [inputBucket],
onResult: new LambdaDestination(fileMoveLambda),
});なお、一部すでに定義されている変数を用いているのでその点について説明します。
inputBucket: ファイルがアップロードされるインプット用バケットのインスタンスfileMoveLambda: ファイル公開処理を行うLambda関数のインスタンス(詳細は後述します)
3つ目の変数内のbucketsでウイルスチェックを行うS3バケットを複数指定できます。
onResultではウイルスチェックが正常終了したら行われる処理を指定します。今回はLambda関数を指定しています。
このコードにより、以下の流れで処理が行われるようになります。
inputBucketにファイルがアップロードされる- 1. をトリガーにウイルスチェックが行われる
fileMoveLambdaで定義された関数が実行される(2. の結果が渡される)
公開処理用Lambda関数
次に、先ほど登場したfileMoveLambdaで行われる処理について触れていきます。
結論から言うと、このLambda関数ではファイルを公開する処理を行なっています。より厳密には、インプット用バケットから公開用バケットにファイルを複製する処理を行なっています。
const inputBucket = this.createBucket("cdk-test-app-input-bucket");
const outputBucket = this.createBucket("cdk-test-app-output-bucket");
// 中略
const fileMoveLambda = this.createLambda(
"fileMove",
join(__dirname, "workers", "file", "Move.ts"),
{ OUTPUT_BUCKET_NAME: outputBucket.bucketName }
);
inputBucket.grantRead(fileMoveLambda);
outputBucket.grantPut(fileMoveLambda);バケットとLambda関数のインスタンス(BucketとNodejsFunctionのインスタンス)を作成しています(そのために用いているメソッドについては第2弾記事において説明していますので、よければそちらを参照ください)。
その後、Lambda関数(fileMoveLambda)に対してインプット用バケット(inputBucket)から読み取り権限を、公開用バケット(outputBucket)から書き込み権限を付与します。
これによりインプット用バケット内のファイルを読み取り、その内容を公開用バケットに書き込む処理が可能となります。
そしてこれがLambda関数のハンドラーです。大して行数もないので、全文掲載します。
import { Handler } from "aws-lambda";
import { S3Client, CopyObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const client = new S3Client({});
const OUTPUT_BUCKET_NAME = process.env.OUTPUT_BUCKET_NAME!;
export const handler: Handler = async (event: any): Promise<any> => {
const result = event.responsePayload.status === "CLEAN";
console.log(event);
console.log(`ウイルスチェック結果: 検知${result ? "なし" : "あり"}`);
if (result) {
const command = new CopyObjectCommand({
Bucket: OUTPUT_BUCKET_NAME,
Key: event.responsePayload.input_key,
CopySource: `${event.responsePayload.input_bucket}/${event.responsePayload.input_key}`,
});
await client.send(command);
}
return result;
};cdk-serverless-clamscanによってLambda関数が呼び出された際、event.responsePayloadに結果が格納されています。
上記コードにて参照している項目について説明しましょう。
status: ウイルスチェック結果。ウイルスが検知された場合は"INFECTED"、検知されなかった場合は"CLEAN"となる- input_bucket: ウイルスチェックを行ったS3バケット名
- input_key: ウイルスチェックを行ったファイルのキー
上記コードにより、ウイルスが検知されなかったらCopyObjectCommandが実行され、インプット用バケットのファイルが公開用バケットに複製されるようになります。
CloudFrontの立ち上げ
公開用バケットにファイルが複製されるようになったので、今度はそのファイルを閲覧できるようにしましょう。
今回は公開用バケットに直接アクセスされず、あくまでもCDN、つまりCloudFront経由でアクセスが行われるようにしたいです。
その方法は、Origin Access Identity(OAI)の設定を追加することで可能です。OAIの詳細や実装方法なども下記ページにて紹介されています。
https://dev.classmethod.jp/articles/cloudfront-origin-access-identity-to-restrict-access-to-s3-bucket-in-aws-cdk/
今回は上記ページなども参考にさせていただきつつ、以下のように実装いたしました。
const cloudFrontIdentity = new cloudfront.OriginAccessIdentity(
this,
"CdkTestAppIdentity"
);
const outputBucketPolicyStatement = new iam.PolicyStatement({
actions: ["s3:GetObject"],
effect: iam.Effect.ALLOW,
principals: [
new iam.CanonicalUserPrincipal(
cloudFrontIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId
),
],
resources: [`${outputBucket.bucketArn}/*`],
});
outputBucket.addToResourcePolicy(outputBucketPolicyStatement);
new cloudfront.CloudFrontWebDistribution(this, "CdkTestAppDist", {
originConfigs: [
{
s3OriginSource: {
s3BucketSource: outputBucket,
originAccessIdentity: cloudFrontIdentity,
},
behaviors: [
{
isDefaultBehavior: true,
viewerProtocolPolicy:
cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
],
},
],
});作成したシステムを試してみよう
実装とデプロイが終わったら、早速動作確認してみましょう。以下の流れに従って動作確認を行ってみてください。
- ファイルの用意
正常なファイルとウイルスチェックに引っかかるファイルを用意してください。ウイルスチェックに引っかかるファイルは下記ページから入手可能です(当然ながら、安全なテストファイルです)。
https://www.eicar.org/download-anti-malware-testfile/ - ファイルのアップロード
インプット用バケットにファイルをアップロードします。第2弾記事で作成したAPIを用いていただいても構いませんし、マネジメントコンソールなどをご利用いただいても構いません。 - (オプション) Lambda関数の実行ログチェック
一定時間経つとLambda関数(fileMove)が実行されます。
CloudWatchから実行ログを見てみましょう。「ウイルスチェック結果: 検知あり/なし」などといったメッセージが表示されているはずです。 - 公開用バケットでの確認
正常なファイルの場合には、公開用バケットにアップロードされているものの、ブラウザで「{バケットURL}/{ファイルのキー}」を開こうとしてもアクセス拒否されると思います。
ウイルスチェックに引っかかるファイルの場合には、そもそもファイルが公開用バケットに存在しないはずです。 - ファイル閲覧
正常なファイルの場合、「{CloudFrontのURL}/{ファイルのキー}」を開くと、ファイルが表示できるかと思います。
まとめ
本記事を含む3本の記事を通してファイルアップロードシステムの理想像を探っていきました。
S3やcdk-serverless-clamscanなどを利用することで、大容量ファイルにも強くセキュアなシステムができたのではと思います。
BtoCサービスではSNSのアイコン登録、BtoBサービスではCSVファイルなどによる一括登録、ファイルを扱う要望は意外と多いのではと思います。
今回のシリーズで扱った内容が参考になれば幸いです。