アレについて記す

CloudFormationでWAF(WebACLs)のARNを取得するCustom Resourceを作成する

Posted on February 20, 2018 at 00:35 (JST)

AWSのCloudFormation(以下CFnという)にはカスタムリソースというリソースが用意されています。
これを使用すると、CFn単体では実現できないことを他のAWSリソースと組み合わて実現できます。
Lambda-backedカスタムリソースはLambdaの処理結果をCFnにて利用可能にします。

本記事ではWAF(Web ACLs)のARNをLambdaで取得し、CFnのテンプレートで利用する方法を紹介します。

作成したサンプルはGithubにて公開しています。[ cloudformation-sample ]

手順

下記の手順で進めます。

  1. 事前準備(WAF作成)
  2. Lambdaのソースを作成
  3. CloudFormationのテンプレートを作成
  4. S3へアップロード
  5. CloudFormation起動

事前準備

IP制限用のWAFを作っておきます。
本題ではないので割愛

Lambdaのソースを作成

公式のAMIを取得するサンプルをベースに作成しました。
aws-sdkを使用してWAFの情報にアクセスします。

findWafARN.js

/********************************************
 * WAFのARNを取得するFunction
 ********************************************/

const aws = require('aws-sdk')

exports.handler = (event, context) => {

  console.log('REQUEST RECEIVED:\n' + JSON.stringify(event))

  // CreateとDeleteのリクエストが発生する
  if (event.RequestType === 'Delete') {
    sendResponse(event, context, 'SUCCESS')
    return
  }

  // CloudFormationから受け取る引数
  const region = event.ResourceProperties.Region
  const name = event.ResourceProperties.Name

  // 対象サービス(WAF)用のインスタンス生成
  const waf = region
    ? new aws.WAFRegional({region})
    : new aws.WAF()
  
  // Web ACLsを取得し、リクエストに該当したもののARNを返却する
  waf.listWebACLs({}, (err, data) => {
    if (err) {
       console.log(err, err.stack)
       const responseData = { Error: 'waf#listWebACLs call failed' }
       sendResponse(event, context, 'FAILED', responseData)
       return
    } 

    console.log('RESULT waf#listWebACLs:\n' + JSON.stringify(data))
    const cert = findByName(data, name)
    if (!cert) {
      const responseData = { Error: 'acl not found' }
      sendResponse(event, context, 'FAILED', responseData)
      return
    }

    sendResponse(event, context, 'SUCCESS', cert)
  });
}

/**
 * 該当する証明書が見つかった場合は先頭のものを返却.
 * 見つからなかった場合はnullを返却する.
 */
function findByName(data, name) {
  const found = data.WebACLs.filter(acl => {
    return acl.Name === name
  })

  return found? found[0] : null
}


/**
 * CloudWatchへログを出力し、結果を返却する
 */
function sendResponse(event, context, responseStatus, responseData) {
  const responseBody = JSON.stringify({
    Status: responseStatus,
    Reason:
      'See the details in CloudWatch Log Stream: ' + context.logStreamName,
    PhysicalResourceId: context.logStreamName,
    StackId: event.StackId,
    RequestId: event.RequestId,
    LogicalResourceId: event.LogicalResourceId,
    Data: responseData
  })

  console.log('RESPONSE BODY:\n', responseBody)

  const https = require('https')
  const url = require('url')

  const parsedUrl = url.parse(event.ResponseURL)
  const options = {
    hostname: parsedUrl.hostname,
    port: 443,
    path: parsedUrl.path,
    method: 'PUT',
    headers: {
      'content-type': '',
      'content-length': responseBody.length
    }
  }

  console.log('SENDING RESPONSE...\n')

  const request = https.request(options, function(response) {
    console.log('STATUS: ' + response.statusCode)
    console.log('HEADERS: ' + JSON.stringify(response.headers))
    context.done()
  })

  request.on('error', function(error) {
    console.log('sendResponse Error:' + error)
    context.done()
  })

  // write data to request body
  request.write(responseBody)
  request.end()
}


上から順にポイントを説明していきます。

if (event.RequestType === 'Delete') {
  sendResponse(event, context, 'SUCCESS')
  return
}

CFnでテンプレートを作成するときに、CreateとDeleteのリクエストがきます。
Deleteの場合は何もする必要がないので即リターンしています。

const region = event.ResourceProperties.Region
const name = event.ResourceProperties.Name

CFnテンプレートにて設定した引数はevent.ResourcePropertiesに格納されています。

const waf = region
  ? new aws.WAFRegional({region})
  : new aws.WAF()

CloudFrontで利用するWAFはGlobalなのでregion指定が不要、
ELBで利用するWAFはregion指定が必要です。
そもそも別リソースとして扱われている点に注意が必要です。

Reason:
    'See the details in CloudWatch Log Stream: ' + context.logStreamName,

作成失敗時にCFnのコンソールに出力するメッセージです。
このLambdaのログ(console.log)はCloudWatch Logsに出力されるため、 そのパスが出力されます。

CloudFormationのテンプレートを作成

findWafARNFunc.yaml

---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Export the WebACL ARN found by Name'
Parameters:
  Name:
    Description: The name to find acl
    Type: String
    MinLength: '1'
    ConstraintDescription: required.
  Region:
    Description: The region to send requests
    Type: String
  ExportName:
    Description: The name of the result
    Type: String
    Default: WebACLArn
  S3Bucket:
    Description: The name of the bucket that contains your packaged source
    Type: String
  S3Key:
    Description: The name of the ZIP package
    Type: String
    Default: lambda/waf/findWafARN.zip
Resources:
  Arn:
    Type: Custom::Arn
    Properties:
      ServiceToken:
        Fn::GetAtt:
        - Function
        - Arn
      Region: !Ref Region
      Name: !Ref Name
  Function:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref S3Bucket
        S3Key: !Ref S3Key
      Handler:
        Fn::Join:
        - ''
        - - "findWafARN"
          - ".handler"
      Role:
        Fn::GetAtt:
        - LambdaExecutionRole
        - Arn
      Runtime: nodejs6.10
      Timeout: '10'
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: root
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: arn:aws:logs:*:*:*
          - Effect: Allow
            Action:
            - waf:Get*
            - waf:List*
            - waf-regional:Get*
            - waf-regional:List*
            Resource: "*"
Outputs:
  WebACLArn:
    Value:
      Fn::GetAtt:
      - Arn
      - WebACLId
    Export:
      Name: !Ref ExportName


こちらも順に説明します。

- Effect: Allow
  Action:
  - waf:Get*
  - waf:List*
  - waf-regional:Get*
  - waf-regional:List*
  Resource: "*"

Lambdaを実行するロールにWAFの参照権限を付与することをお忘れなく!!

Export:
  Name: !Ref ExportName

ParametersにExportNameを指定する事で、このテンプレートを使い回しやすくしています。
他のテンプレートからFn::ImportValue: ExportNameのように指定して取得結果を利用できます。

S3へアップロード

S3バケットはCFn実行リージョンに作成しました。

Lambdaで利用できるよう、jsファイルをzipにします。
今回は下記のようにローカルのディレクトリ構造のままアップロードしました。

lambda ━ waf ┳ findWafARN.js
             ┣ findWafARN.zip
             ┗ FindWafARNFunc.yaml

CloudFormation起動

S3へアップロードしたFindWafARNFunc.yamlを指定しても、 ローカル開発環境の同ファイルを指定しても大丈夫です。

作成完了後のCFnコンソールはこんな感じ。 確認画面

CloudWatch Logsはこんな感じです。 確認画面

おわりに

Lambdaが利用できることで夢が膨らみますね。
サンプルプロジェクトにACMで管理している証明書のARNを取得するサンプルもコミットしてあるので、興味がある方はご参照ください。

参考