CustomResourceを使って、BigQueryのDataSetをCloudFormationのStackとして扱う。

# 背景

 - 弊社では、「Infrastructure as Code」を進めている。
 - AWSリソースは全てCloudFormationで構築・更新されている。
 - ログ解析のためにGCPのBigQueryを使っている。

# 課題

 - 環境構築時に、ログを流し込む先のDataSetを作る必要がある。
 - そのたびに、手動でDataSetを作るのは辛い。

# 解決策

 - CustomResourceを使う。

## CustomResourceとは?

 - CloudFormation経由でLambdaを実行できる機能。
 - CloudFormationからの呼び出しに応じてDataSetを作ったり、削除したりするLambdaを書けば良い。
 - 呼び出し時のリクエスト/レスポンスの仕様は[ココ](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html)

## どんないいことがあるの？

 - AWS以外のリソースもCloudFormationのStackとして管理できる。
   + IaCのインタフェースとしてCloudFormationだけ考えていれば良い。
   + ServiceCatalogから呼び出せる。

# 実践

今回はGoでLambdaを書きます。

まず、リクエスト/レスポンスをマーシャル/アンマーシャルするためのstructを書きます。


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

```golang
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を作ったり削除したりするコードを書きます。
エラーハンドルやクレデンシャルの読み込みなどは省略します。


```golang
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対応最高。

```golang
package main

import "github.com/aws/aws-lambda-go/lambda"

func main() {
	lambda.Start(handle)
}
```

CustomResourceを生成するCloudFormationのStackを作ります。

```yaml
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を定義すれば良いです。

```yaml
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をさらに進めてくれる存在だと思います。