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 ]
手順
下記の手順で進めます。
- 事前準備(WAF作成)
- Lambdaのソースを作成
- CloudFormationのテンプレートを作成
- S3へアップロード
- 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を取得するサンプルもコミットしてあるので、興味がある方はご参照ください。