背景
- 弊社では、「Infrastructure as Code」を進めている。
- AWSリソースは全てCloudFormationで構築・更新されている。
- ログ解析のためにGCPのBigQueryを使っている。
課題
- 環境構築時に、ログを流し込む先のDataSetを作る必要がある。
- そのたびに、手動でDataSetを作るのは辛い。
解決策
- CustomResourceを使う。
CustomResourceとは?
- CloudFormation経由でLambdaを実行できる機能。
- CloudFormationからの呼び出しに応じてDataSetを作ったり、削除したりするLambdaを書けば良い。
- 呼び出し時のリクエスト/レスポンスの仕様はココ
どんないいことがあるの?
- AWS以外のリソースもCloudFormationのStackとして管理できる。
- IaCのインタフェースとしてCloudFormationだけ考えていれば良い。
- ServiceCatalogから呼び出せる。
実践
今回はGoでLambdaを書きます。
まず、リクエスト/レスポンスをマーシャル/アンマーシャルするためのstructを書きます。
type Request struct {
RequestType string `json:"RequestType"`
ResponseURL string `json:"ResponseURL"`
StackId string `json:"StackId"`
RequestId string `json:"RequestId"`
ResourceType string `json:"ResourceType"`
LogicalResourceId string `json:"LogicalResourceId"`
PhysicalResourceId string `json:"PhysicalResourceId"`
ResourceProperties json.RawMessage `json:"ResourceProperties"`
OldResourceProperties json.RawMessage `json:"OldResourceProperties"`
ServiceToken string `json:"ServiceToken"`
}
type Response struct {
StackId string `json:"StackId"`
RequestId string `json:"RequestId"`
ResponseURL string `json:"ResponseURL"`
LogicalResourceId string `json:"LogicalResourceId"`
PhysicalResourceId string `json:"PhysicalResourceId"`
Status string `json:"Status"` // SUCCESS or FAILED
Reason string `json:"Reason"`
Data Data `json:"Data"`
}
type Data struct {
DataSetName string `json:"DataSetName"`
}
Dataは CloudFormationのテンプレート内で、!GetAtt
を使って取り出せます。
次に、実際にDataSetを作ったり削除したりするコードを書きます。
エラーハンドルやクレデンシャルの読み込みなどは省略します。
func handle(req Request) {
res := Response{
StackId: req.StackId,
RequestId: req.RequestId,
ResponseURL: req.ResponseURL,
LogicalResourceId: req.LogicalResourceId,
PhysicalResourceId: uuid.New().String(),
Status: "SUCCESS",
}
// リクエストのPropertiesを受け取る。
var prop map[string]string
json.Unmarshal(req.ResourceProperties, &prop)
res.Data.DataSetName = prop["DataSetName"]
// bigqueryクライアントの作成。実際にはcontextやtokenが必要。
bq, err := bigquery.NewClient(...)
// RequestTypeごとに処理を変える。
if req.RequestType == "Create" {
bq.Dataset(res.Data.DataSetName).Create(ctx, &bigquery.DatasetMetadata{})
}
if req.RequestType == "Delete" {
bq.Dataset(res.Data.DataSetName).DeleteWithContents(ctx)
}
if req.RequestType == "Update" {
// Updateには未対応。
}
// 処理結果をCloudFormationに返す。
client := &http.Client{}
bytea, _ := json.MarshalIndent(res, "", " ")
resreq, _ := http.NewRequest("PUT", res.ResponseURL, bytes.NewReader(bytea))
resreq.ContentLength = int64(len(string(bytea)))
client.Do(resreq)
}
レスポンスはLambdaの戻り値として返すのではなく、リクエストで指定されたURLにPUTする形でCloudFormationとコミュニケーションするようです。
最後にmain()
でハンドラをセットします。AWS公式のGo対応最高。
package main
import "github.com/aws/aws-lambda-go/lambda"
func main() {
lambda.Start(handle)
}
CustomResourceを生成するCloudFormationのStackを作ります。
AWSTemplateFormatVersion: "2010-09-09"
Outputs:
BigQueryDataSetServiceTokenArn:
Value: !GetAtt BigQueryDataSetFunction.Arn
Export:
Name: BigQueryDataSetServiceTokenArn
Resources:
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
Path: /
Policies:
- PolicyName: "root"
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Resource:
- arn:aws:logs:*:*:*
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
BigQueryDataSetFunction:
Type: AWS::Lambda::Function
Properties:
Handler: handle
Runtime: go1.x
Timeout: 30
Role: !GetAtt LambdaExecutionRole.Arn
Code:
S3Bucket: "example.com"
S3Key: "lambda/custom-resource-bigquery.zip"
あとは、DataSetを作るタイミングでResourceを定義すれば良いです。
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
DataSetName:
Description: ""
Type: String
Default: hogehoge_dataset_01
Outputs:
DataSetName:
Value: !GetAtt BigQueryDataSet.DataSetName
Resources:
BigQueryDataSet:
Type: Custom::BigQueryDataSet
Properties:
ServiceToken: !ImportValue BigQueryDataSetServiceTokenArn
Region: !Ref AWS::Region
DataSetName: !Ref DataSetName
同様に、パスワード用のランダム文字列生成といった手作業でやっていたことや社内システムとの連携なども、CloudFormationで完結することができます。
Lambdaを開発・保守するコストはかかりますが、使い所を見極めればCustomResourceはIaCをさらに進めてくれる存在だと思います。