goformationで始めるCloudFormation (1) SQS/デッドレターキュー



この記事は [Wanoグループアドベントカレンダー2018](https://qiita.com/advent-calendar/2018/wano-group)の 8日目の記事になります。
8日目ったら8日目です。

## AWSインフラ自動構築の必要性
仕事やプライベートでAWSをそこそこ使ってきたんですが、[CloudFormation](https://aws.amazon.com/jp/cloudformation/)にはほとんどノータッチできてしまいました。
[aws-sam-cli](https://github.com/awslabs/aws-sam-cli)や[serverless framework](https://serverless.com/)で必要があればちょっと設定を足してきた、くらいの事はやっていたんですが、基本生CloudFormationを書いていくのは複雑そうだという印象があり、ちょっと足が遠のいていたわけです。
実際「基本アプリ一個立ててCRUD」くらいのうちはGUI側でポチポチ...でなんとかなってきたケースも多かったのですが、もう少し複雑になるとそうもいかなくなってきます。

* SQSの多段キュー構成
* 複数サービスをまたいだイベントベースのそこそこ複雑なピタゴラ
* 複数AWSアカウント/複数region。それぞれプロダクション環境/dev環境毎に同じ環境

これらをチェック項目作って毎回ポチポチ立ててくのは流石に治安が悪すぎ、チームメンバーにも何がどこに設定されてるのか伝わりづらい..ということで、そろそろ取り組んでみるか...という気になりました。

## goformation

cloudformationはjson/yamlの形式で定義できます。
しかし、独自の組み込み関数が多く、yaml1枚でまるでプログラミング言語のような様相を見せています。
まあ基本的に各プロパティの説明をドキュメント見ながら地道に書いていくしかないわけですが、もうちょっとなんとかならないかな、、ということで何かしらの定義ツールを使うことにしました。

そこで、awslabsの [goformation](https://github.com/awslabs/goformation) を使ってみました。
[aws-cdk](https://github.com/awslabs/aws-cdk) + TypeScriptで書くという選択肢も考えたのですが、

* aws-cdkはCloudFormationをラップ/再定義した独自のモデルになりすぎている  
* 現在のプロジェクトではサーバーサイドの実装にGo言語を使っているのでリポジトリに含めやすい  

ということで、今回は学習がてら、こちらを使用することにしました。

## SQS FIFO キュー/デッドレターキューを作ってみる

まず、よく作るパターンである 「SQSキューを作る」「可視性タイムアウトの時間を超えたらデッドレターキューに入る」という構成を作るとこまでやってみました。

yaml生成コード

```go
package main

import (
	"flag"
	"fmt"
	cfn "github.com/awslabs/goformation/cloudformation"
	"io/ioutil"
	"os"
)

var ENV string

func main() {

	flag.StringVar(&ENV, "env", "dev", "env指定")
	flag.Parse()

	template := cfn.NewTemplate()
	template.Description = fmt.Sprintf(`example1アプリの %s 環境です` , ENV)

	pwd  , _ := os.Getwd()

	resources := NewResourceNames(ENV)

	const MESSEGE_RETENSION_PERIOD_14DAYS = 1209600
	template.Resources[LOGICAL_ID_SQS_DEAD_LETTER_QUEUE.String()] = cfn.AWSSQSQueue{
		FifoQueue: true,
		QueueName: resources.DeadLetterQueueName,
		MessageRetentionPeriod: MESSEGE_RETENSION_PERIOD_14DAYS,
	}

	const TIMEOUT_SEC_5 = 1 * 5
	template.Resources[LOGICAL_ID_SQS_FIFO_QUEUE.String()] = cfn.AWSSQSQueue{
		FifoQueue: true,
		QueueName: resources.FifoQueueName,
		DelaySeconds: 3,
		RedrivePolicy: map[string]interface{}{
			"deadLetterTargetArn": cfn.GetAtt(LOGICAL_ID_SQS_DEAD_LETTER_QUEUE.String(), "Arn"),
			"maxReceiveCount":     1,
		},
		VisibilityTimeout:      TIMEOUT_SEC_5,
		MessageRetentionPeriod: MESSEGE_RETENSION_PERIOD_14DAYS,
	}

	y, err := template.YAML()
	if err != nil {
		fmt.Printf("Failed to generate YAML: %s\n", err)
	} else {
		//fmt.Printf("%s\n", string(y))
	}

	err = ioutil.WriteFile(pwd + `/app/example0/export/template-`+ENV+`.yml`, y, 0777)
	if err != nil {
		panic(err)
	}

}
```

定数系

```golang
type LOGICAL_ID string

func (self LOGICAL_ID) String() string {
	return (string)(self)
}

const (
	LOGICAL_ID_SQS_DEAD_LETTER_QUEUE LOGICAL_ID = `SqsDeadLetterQueue`
	LOGICAL_ID_SQS_FIFO_QUEUE LOGICAL_ID = `SqsFifoQueue`
)

type ResourceNames struct {
	DeadLetterQueueName string
	FifoQueueName string
	DeadLetterDetected string
	DeadLetterOldest10Days string
}

func NewResourceNames(env string) ResourceNames {

	return ResourceNames{
		DeadLetterQueueName: fmt.Sprintf(`%s-example0-dead-letter-queue.fifo` , env),
		FifoQueueName: fmt.Sprintf(`%s-example0-fifo-queue.fifo` , env),
		DeadLetterDetected: fmt.Sprintf(`%s-example0-deadletter-detected` , env),
		DeadLetterOldest10Days: fmt.Sprintf(`%s-example0-deadletter-oldest-10days` , env),
	}
}
```

Makefile

```
ENV = dev
init:
	dep ensure vendor-only=true
	npm install -g  cfn-lint
build-example-0:
	go run ./app/example0/cfn.go -env ${ENV}
	aws cloudformation validate-template --template-body  file://app/example0/export/template-${ENV}.yml
	cfn-lint validate ./app/example0/export/template-${ENV}.yml
deploy-example-0-dev:
	aws cloudformation deploy --template-file app/example0/export/template-dev.yml --stack-name dev-example-0 --s3-bucket cfn-example-xxx --s3-prefix dev-example-0
```

aws-cliのバリデーションだけだと若干心許なかったので、yaml生成時に[cfn-lint](https://github.com/aws-cloudformation/cfn-python-lint)を同時にかけるようにしてあります。

## 生成されたコード

```
ENV=dev make build-example-0
```
します。

``` yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: example1アプリの dev 環境です
Resources:
  SqsDeadLetterQueue:
    Properties:
      FifoQueue: true
      MessageRetentionPeriod: 1209600
      QueueName: dev-example1-dead-letter-queue.fifo
    Type: AWS::SQS::Queue
  SqsFifoQueue:
    Properties:
      DelaySeconds: 3
      FifoQueue: true
      MessageRetentionPeriod: 1209600
      QueueName: dev-example1-fifo-queue.fifo
      RedrivePolicy:
        deadLetterTargetArn:
          Fn::GetAtt:
          - SqsDeadLetterQueue
          - Arn
        maxReceiveCount: 1
      VisibilityTimeout: 5
    Type: AWS::SQS::Queue

```
以上のものが生成されました。

## デプロイ
```
make deploy-example-0-dev
```

して先ほど生成されたtemplateファイルをデプロイします。

GUI上で確認すると、FIFOキューとデッドレターキューがそれぞれできてます。
<img width="1340" alt="できてる.png" src="https://qiita-image-store.s3.amazonaws.com/0/85375/86e45813-2a5a-d938-2f12-8fe8128f7052.png">


キューにメッセージを登録し
<img width="620" alt="できてる2.png" src="https://qiita-image-store.s3.amazonaws.com/0/85375/320d2ccd-f1a0-7155-f4f7-ee3aa11efe62.png">

観測します。
<img width="1323" alt="できてる3.png" src="https://qiita-image-store.s3.amazonaws.com/0/85375/ebf6c24a-05f0-c778-98a2-95462efe215e.png">

デッドレターキューに移った。。

<img width="1000" alt="できてる5.png" src="https://qiita-image-store.s3.amazonaws.com/0/85375/4ec2938d-d6db-0a83-d12e-ed741dd5625f.png">


## 感想

CloudFormationを立てる上で大事な論理IDなどを定数化できるのはなかなか良いところで、デプロイ環境の違いなども無理にテンプレートの組み込み関数を使わず、コードで定義できるのでそこはとても気に入りました。
ただ、goで書いたSQSの RedrivePolicy の箇所に見られるように、身も蓋もない ``` string ``` や``` map[string]interface{} ``` での定義などが細部で出てくるので、そこは辛いところではありました。
この辺は徐々にアップデートされるでしょうし、元のコードの定義にjumpすれば詳細な説明やドキュメントへのリンクが必ず記載してありますので、AWSのインフラ自体を学習してく上でも理解が深まりそうです。