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

# EC2は使わない
CDP（Cloud Design Pattern）で紹介されているPCD（Private Cache Distribution）は[EC2を使った方法](http://aws.clouddesignpattern.org/index.php/CDP:Private_Cache_Distribution%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)が記載されていますが、EC2はできることが多いので使いたくない、、

そんな訳でLambda上で署名つきURLを作成します。
# 構成
<img width="1072" alt="スクリーンショット 2019-12-04 21.22.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333018/0db5a647-0678-0022-5d75-06d496cae68b.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」としておきます。

<img width="808" alt="スクリーンショット 2019-12-04 22.35.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333018/632d839d-b529-2045-6486-f2cab0959ecd.png">

準備は以上です！

# CDKでリソースを作り上げる！
CDK（Cloud Development Kit）で簡単に作っちゃいます。

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


```typescript:~/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の順で作成します。

```typescript:~/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

```
```typescript:~/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

```

```typescript:~/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

```

```typescript:~/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
```
スタック

```typescript:~/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に追記しておきましょう！

```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

```terminal
$ npm run build
$ npm run deploy
```

これでリソースは完成しました！
# リクエストを投げてみる

Postmanを起動してメソッドはPOSTにし、Headerにx-api-keyを指定して値には先ほど作ったAPIKeyを入れます。
Bodyには下記のように指定します。

```
{
  "Bucket": {
    "url": "https://{CloudFrontのドメイン}/{バケットのデータの場所}"
  },
  "secretsManager": {
    "id": "{最初に作ったSecretManagerのID}"
  }
}
```

リクエストを投げると。。。
<img width="1024" alt="スクリーンショット 2019-12-04 23.47.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333018/eedcb33e-0fd5-0cd1-2e79-a5dd27f5c225.png">

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

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