LoginSignup
11
7

More than 3 years have passed since last update.

CDP:EC2を使わずLambdaで署名つきURLを作成する

Last updated at Posted at 2019-12-04

EC2は使わない

CDP(Cloud Design Pattern)で紹介されているPCD(Private Cache Distribution)はEC2を使った方法が記載されていますが、EC2はできることが多いので使いたくない、、

そんな訳でLambda上で署名つきURLを作成します。

構成

スクリーンショット 2019-12-04 21.22.47.png

流れとしては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」としておきます。

スクリーンショット 2019-12-04 22.35.32.png

準備は以上です!

CDKでリソースを作り上げる!

CDK(Cloud Development Kit)で簡単に作っちゃいます。

まずは一番重要なLambdaに置くコード

~/src/lambda/index.ts

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の順で作成します。

~/src/resrouce/Lambda.ts

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

~/src/resources/ApiGateway.ts

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

~/src/resources/S3.ts

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

~/src/resources/CloudFront.ts

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

スタック

~/src/stacks/PrivateDistributionStack.ts

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に追記しておきましょう!

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}"
  }
}

リクエストを投げると。。。
スクリーンショット 2019-12-04 23.47.15.png

署名つきURLが返却され、無事にS3のデータを取得することができました!

まとめ

時間がなく、駆け足で書いてしまったので内容薄めになってしまいました...。
最後までありがとうございました!

11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7