EC2は使わない
CDP(Cloud Design Pattern)で紹介されているPCD(Private Cache Distribution)はEC2を使った方法が記載されていますが、EC2はできることが多いので使いたくない、、
そんな訳でLambda上で署名つきURLを作成します。
構成
流れとしてはAPIを投げ、API GatewayはLambdaをキックします。
LambdaはSecertManagerから各パラメータを受け取り、署名つきURLを作成し、値を返却します。
クライアント側は受け取った署名つきURLでデータを取得する。という流れになります。
準備
まず、CloudFrontのキーペアを作成します。※ルートのアカウントしか作成できません!
作り方は以下を参照してください。
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html#private-content-creating-cloudfront-key-pairs-procedure
キーペアIDとプライベートキーはSecretManagerに保存します。
キーは「KEY_PAIR_ID」、「PRIVATE_KEY」としておきます。
準備は以上です!
CDKでリソースを作り上げる!
CDK(Cloud Development Kit)で簡単に作っちゃいます。
まずは一番重要なLambdaに置くコード
import { CloudFront, SecretsManager } from 'aws-sdk'
import { Signer } from 'aws-sdk/lib/cloudfront/signer'
import { APIGatewayProxyEvent } from 'aws-lambda'
function getSignedUrlAsync(
keyPairId: string,
privateKey: string,
options: Signer.SignerOptionsWithPolicy | Signer.SignerOptionsWithoutPolicy
) {
return new Promise<string>((resolve, reject) => {
const signer = new CloudFront.Signer(keyPairId, privateKey)
signer.getSignedUrl(options, (err, url) => {
if (err) {
reject(err)
}
resolve(url)
})
})
}
const formatPrivateKey = (privateKey: string) => {
return privateKey.replace(/(-----) /, '$1\n').replace(/ (-----)/, '\n$1') // --BEGIN と --END の間には改行がないと[no start line]エラーになる為
}
const parseStringifyToJson = (data: any) => {
return JSON.parse(data)
}
export const handler = async (event: APIGatewayProxyEvent) => {
const param = parseStringifyToJson(event.body)
const url = param.bucket.url
const secretId = param.secretsManager.id
const client = new SecretsManager({ region: 'ap-northeast-1' })
const data = await client.getSecretValue({ SecretId: secretId }).promise()
const secretJson = JSON.parse(data.SecretString!)
const keyPairId = secretJson.KEY_PAIR_ID
const privateKey = formatPrivateKey(secretJson.PRIVATE_KEY)
const expires = new Date().getTime() + 30 * 60 * 1000
const preURL = await getSignedUrlAsync(keyPairId, privateKey, {
url,
expires
})
const responseParam = {
statusCode: 200,
body: JSON.stringify({
message: 'success',
preURL
})
}
return responseParam
}
これができたらLambda、API Gateway、S3、CloudFrontの順で作成します。
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'
import * as iam from '@aws-cdk/aws-iam'
const createLambda = (scope: cdk.Construct) => {
const handler = new lambda.Function(scope, 'Function', {
code: new lambda.AssetCode('src/lambda'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_10_X
})
handler.addToRolePolicy(
new iam.PolicyStatement({
resources: ['*'],
actions: ['secretsmanager:GetSecretValue']
})
)
return handler
}
export default createLambda
import * as cdk from '@aws-cdk/core'
import * as apiGateway from '@aws-cdk/aws-apigateway'
import * as lambda from '@aws-cdk/aws-lambda'
const createApiGateway = (scope: cdk.Construct, handler: lambda.IFunction) => {
const apiKey = new apiGateway.ApiKey(scope, 'ApiKey')
const privateDistributionApiGateway = new apiGateway.LambdaRestApi(
scope,
'RestApi',
{
handler,
deploy: true,
deployOptions: {
stageName: 'Stage'
},
proxy: false
}
)
new apiGateway.Method(scope, 'Method', {
httpMethod: 'POST',
resource: privateDistributionApiGateway.root,
options: { apiKeyRequired: true }
})
new apiGateway.UsagePlan(scope, 'UsePlan', {
apiKey,
apiStages: [
{ api: privateDistributionApiGateway, stage: privateDistributionApiGateway.deploymentStage }
]
})
return privateDistributionApiGateway
}
export default createApiGateway
import * as cdk from '@aws-cdk/core'
import * as s3 from '@aws-cdk/aws-s3'
import * as cloudFront from '@aws-cdk/aws-cloudfront'
import * as iam from '@aws-cdk/aws-iam'
const createS3 = (scope: cdk.Construct) => {
const privateDistributionBucket = new s3.Bucket(scope, 'Bucket', {
blockPublicAccess: new s3.BlockPublicAccess({
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true
})
})
const privateDistirbutionCloudFrontOriginAccessIdentity = new cloudFront.CfnCloudFrontOriginAccessIdentity(
scope,
'CloudFrontOriginAccessIdentity',
{
cloudFrontOriginAccessIdentityConfig: {
comment: `access-identity-${privateDistributionBucket.bucketName}`
}
}
)
privateDistributionBucket.addToResourcePolicy(
new iam.PolicyStatement({
actions: ['s3:GetObject'],
effect: iam.Effect.ALLOW,
principals: [
new iam.ArnPrincipal(
`arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${privateDistirbutionCloudFrontOriginAccessIdentity.ref}`
)
],
resources: [`arn:aws:s3:::${privateDistributionBucket.bucketName}/*`]
})
)
return { privateDistributionBucket, privateDistirbutionCloudFrontOriginAccessIdentity }
}
export default createS3
import * as cdk from '@aws-cdk/core'
import * as CloudFront from '@aws-cdk/aws-cloudfront'
import * as s3 from '@aws-cdk/aws-s3'
import * as route53 from '@aws-cdk/aws-route53'
import * as route53Targets from '@aws-cdk/aws-route53-targets'
const createCloudFront = (
scope: cdk.Construct,
bucket: s3.Bucket,
originAccessIdentityId: string,
) => {
new CloudFront.CloudFrontWebDistribution(
scope,
'Distribution',
{
originConfigs: [
{
s3OriginSource: {
s3BucketSource: bucket,
originAccessIdentityId
},
behaviors: [
{ compress: false, trustedSigners: ['{your iam account}'], isDefaultBehavior: true }
]
}
]
}
)
}
export default createCloudFront
スタック
import * as cdk from '@aws-cdk/core'
import createApiGateway from '../resources/ApiGateway'
import createLambda from '../resources/Lambda'
import createCloudFront from '../resources/CloudFront'
import createS3 from '../resources/S3'
class PrivateDistributionStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const handler = createLambda(this)
createApiGateway(this, handler)
const {
privateDistributionBucket,
privateDistirbutionCloudFrontOriginAccessIdentity
} = createS3(this)
createCloudFront(
this,
privateDistributionBucket,
privateDistirbutionCloudFrontOriginAccessIdentity.ref
)
}
}
export default PrivateDistributionStack
Synth & Deploy
LambdaはTypeScriptに対応してないので、index.tsはトランスパイルします。
synth時に勝手にトランスパイルしてくれるようにpackage.jsonに追記しておきましょう!
{
"scripts": {
"build": "npx tsc src/lambda/index.ts && npx cdk synth",
"deploy": "npx cdk deploy --profile {profile_name}",
"destroy": "npx cdk destroy PrivateDistributionStack --profile {profile_name}"
}
}
下記コマンドでsynth・deploy
$ npm run build
$ npm run deploy
これでリソースは完成しました!
リクエストを投げてみる
Postmanを起動してメソッドはPOSTにし、Headerにx-api-keyを指定して値には先ほど作ったAPIKeyを入れます。
Bodyには下記のように指定します。
{
"Bucket": {
"url": "https://{CloudFrontのドメイン}/{バケットのデータの場所}"
},
"secretsManager": {
"id": "{最初に作ったSecretManagerのID}"
}
}
署名つきURLが返却され、無事にS3のデータを取得することができました!
まとめ
時間がなく、駆け足で書いてしまったので内容薄めになってしまいました...。
最後までありがとうございました!