はじめに
静的コンテンツを公開するパブリックバケットを作る機会がありましたので、勉強がてらAWS CDKで作ってみました。
概要
構成は以下になります。
- 公開バケット
- アクセスログ格納バケット
- 公開バケットのS3アクセスログの出力先
- アクセスログ検索サービス
- 図中ではAthenaですが、実際は以下
- Athena::WorkGroup
- Glue::Database
- Glue::Table
- 図中ではAthenaですが、実際は以下
- Athenaの結果の格納先
- ユーザ
- 公開バケットへコンテンツの追加・削除が可能
- Athenaに対して、問い合わせ可能
図示すると、以下の感じです。
作り方
環境準備など
以前に書いた、以下の記事を参考にしてください。
コードなど
バケット名は、cdk.jsonで定義して、それを読み込ませる形にしました。
"context": {
, "bucketName": "<ユニークになる文字列>"
CDKのコードは以下になります。
クリックで表示
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam'
import { Duration } from 'aws-cdk-lib';
import * as athena from 'aws-cdk-lib/aws-athena';
import * as glue from 'aws-cdk-lib/aws-glue';
export class CreatePublicbucketStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// read parameter
const bucketName = this.node.tryGetContext('bucketName');
// accountId
const accountId = cdk.Aws.ACCOUNT_ID;
// log bucket
const logBucket = new s3.Bucket(this, 'LogBucket',{
bucketName: bucketName + '-log',
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
encryption: s3.BucketEncryption.S3_MANAGED
});
// アクセスログ保存のため、S3からのPUTを許可
logBucket.addToResourcePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:PutObject'],
principals: [ new iam.ServicePrincipal('s3.amazonaws.com') ],
resources: [`${logBucket.bucketArn}/*`],
conditions:{
StringEquals: {
's3:x-amz-acl' : 'bucket-owner-full-control'
}
}
})
);
// S3 bucket with public read access
const publicBucket = new s3.Bucket(this, 'PublicBucket',{
bucketName: bucketName,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
publicReadAccess: true,
serverAccessLogsBucket: logBucket,
serverAccessLogsPrefix: 'access-logs/'
});
// 公開設定
publicBucket.addToResourcePolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:GetObject'],
principals: [new iam.AnyPrincipal() ],
resources: [`${publicBucket.bucketArn}/*`]
})
);
// Athena
//// result bucket
const athenaResultBucket = new s3.Bucket(this, 'AthenaQueryResultBucket',{
bucketName: 'aws-athena-query-results-' + bucketName,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
lifecycleRules:[{
expiration: Duration.days(7)
}]
});
//// WorkGroup
const workGroup = new athena.CfnWorkGroup(this,'AthenaWorkGroup',{
name: 'athena-work-group-' + bucketName,
recursiveDeleteOption: true,
state: 'ENABLED',
workGroupConfiguration:{
resultConfiguration:{
outputLocation: `s3://${athenaResultBucket.bucketName}/athena-results/`
}
}
});
//// Database
const database = new glue.CfnDatabase(this,'GlueDatabase',{
catalogId: accountId,
databaseInput: {
name: 'glue-database-' + bucketName
}
});
//// Athena table
const accessLogsTable = new glue.CfnTable(this,'AccessLogsTable',{
catalogId: accountId,
databaseName: database.ref,
tableInput: {
name: `glue-table-${bucketName}-log`,
storageDescriptor: {
columns:[
{name: 'bucketowner', type: 'string' },
{name: 'bucket_name', type: 'string' },
{name: 'requestdatetime', type: 'string' },
{name: 'remoteip', type: 'string' },
{name: 'requester', type: 'string' },
{name: 'requestid', type: 'string' },
{name: 'operation', type: 'string' },
{name: 'key', type: 'string' },
{name: 'request_uri', type: 'string' },
{name: 'httpstatus', type: 'string' },
{name: 'errorcode', type: 'string' },
{name: 'bytessent', type: 'bigint' },
{name: 'objectsize', type: 'bigint' },
{name: 'totaltime', type: 'string' },
{name: 'turnaroundtime', type: 'string' },
{name: 'referrer', type: 'string' },
{name: 'useragent', type: 'string' },
{name: 'versionid', type: 'string' },
{name: 'hostid', type: 'string' },
{name: 'sigv', type: 'string' },
{name: 'ciphersuite', type: 'string' },
{name: 'authtype', type: 'string' },
{name: 'endpoint', type: 'string' },
{name: 'tlsversion', type: 'string' },
{name: 'accesspointarn', type: 'string' },
{name: 'aclrequired', type: 'string' }
],
location: `s3://${logBucket.bucketName}/access-logs/`,
inputFormat: 'org.apache.hadoop.mapred.TextInputFormat',
outputFormat: 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat',
serdeInfo: {
serializationLibrary: 'org.apache.hadoop.hive.serde2.RegexSerDe',
parameters: {
'input.regex': '([^ ]*) ([^ ]*) \\[(.*?)\\] ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) (-|[0-9]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) ([^ ]*)(?: ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*))?.*$'
}
}
}
}
});
// create user
const uploadUser = new iam.User(this,'uploadUser');
//// allow S3 upload
uploadUser.addToPolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:ListAllMyBuckets'],
resources: ['*']
}));
uploadUser.addToPolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:ListBucket'],
resources: [publicBucket.bucketArn]
}));
uploadUser.addToPolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:*Object'],
resources: [publicBucket.bucketArn + '/*']
}));
//// Athena fullaccess
uploadUser.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonAthenaFullAccess')
);
uploadUser.addToPolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:GetObject', 's3:ListBucket'],
resources: [logBucket.bucketArn + '*']
}));
}
}
Athenaの、S3 サーバーのアクセスログの定義については、以下を参考にしました。
上記からS3アクセスログの内容が増えていましたので、以下を参考に追加しています。
またAthena結果の出力先バケット名の先頭にaws-athena-query-results-
を付けています。これはユーザに付与しているAWSのマネージドポリシーAmazonAthenaFullAccess
にてデフォルトで定義されている、「Athenaのクエリ結果格納先としての許可に対して、既に設定されているリソース」になります。
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:GetObject",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:ListMultipartUploadParts",
"s3:AbortMultipartUpload",
"s3:CreateBucket",
"s3:PutObject",
"s3:PutBucketPublicAccessBlock"
],
"Resource": [
"arn:aws:s3:::aws-athena-query-results-*"
]
}
出力先バケットを上記に当てはまらない名前にした場合は、このポリシーを自作して、ユーザに付与してください。
おわりに
ChatGPTが流行っていますので、いい機会と思い補助してもらいながらCDKを書いてみました。
元々CFnをやっていてある程度の目星がついた状況下でしたので、ChatGPTの間違いを修正しながら作ることができました。
なので、ChatGPTの誤りに気付ける程度の事前知識は必要と感じています。