この記事は Wanoグループアドベントカレンダー2018の 9日目の記事になります。
前回に続き、goformationを使ってCloudFormationのコードを生成していきます。
SQSの監視
前回はSQSのキューとデッドレターキューを作成するとこまでやりました。
今回は、
- デッドレターキューにメッセージが入ったらSNS経由でLambdaを起動する
- デッドレターキューの放置期間が一定期間を過ぎたらSNS経由でLambdaを起動する
というアラート項目の設定を記述してみます。
コード
package main
import (
"flag"
"fmt"
cfn "github.com/awslabs/goformation/cloudformation"
"github.com/labstack/gommon/log"
"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,
}
template.Resources[LOGICAL_ID_SNS_ALARM_TOPIC_COMMON.String()] = cfn.AWSSNSTopic{
TopicName: resources.AlarmSnsTopicCommon,
Subscription: []cfn.AWSSNSTopic_Subscription{
cfn.AWSSNSTopic_Subscription{
Protocol:`lambda`,
Endpoint: cfn.GetAtt(LOGICAL_ID_LAMBDA_SNS_ALARM_TO_SLACK.String() , `Arn`) ,
},
},
}
template.Resources[LOGICAL_ID_PERMISSION_LAMBDA_SNS_EVENT.String()] = cfn.AWSLambdaPermission{
Action: `lambda:InvokeFunction`,
FunctionName:cfn.GetAtt(LOGICAL_ID_LAMBDA_SNS_ALARM_TO_SLACK.String() , `Arn`),
Principal:`sns.amazonaws.com`,
SourceArn:cfn.Ref(LOGICAL_ID_SNS_ALARM_TOPIC_COMMON.String()),
}
template.Resources[LOGICAL_ID_LAMBDA_SNS_ALARM_TO_SLACK.String()] = cfn.AWSLambdaFunction{
Handler: `index.handler`,
Runtime: `nodejs8.10`,
MemorySize: 256,
Timeout: 30,
Code:&cfn.AWSLambdaFunction_Code{
ZipFile: func() string{
lambdaFunctionJs , err := readJs(pwd + `/app/example1/lambda/send_to_slack.js`)
if err != nil {
panic(err)
}
return lambdaFunctionJs
}(),
},
Role: GetExitedResources().ServerlessRole,
}
template.Resources[LOGICAL_ID_CLOUDWATCH_ALARM_DEAD_LETTER_DETECTED.String()] = cfn.AWSCloudWatchAlarm{
ActionsEnabled: true,
AlarmName: resources.DeadLetterDetected,
MetricName:"ApproximateNumberOfMessagesVisible",
Namespace:`AWS/SQS`,
Threshold: 0.001,
Dimensions:[]cfn.AWSCloudWatchAlarm_Dimension{
cfn.AWSCloudWatchAlarm_Dimension{
Name : `QueueName`,
Value: resources.DeadLetterQueueName,
},
},
TreatMissingData: `ignore`,
EvaluationPeriods :1,
Period:60,
ComparisonOperator: "GreaterThanOrEqualToThreshold",
AlarmActions:[]string{
cfn.Ref(LOGICAL_ID_SNS_ALARM_TOPIC_COMMON.String()),
},
OKActions: []string{
cfn.Ref(LOGICAL_ID_SNS_ALARM_TOPIC_COMMON.String()),
},
Statistic:`Minimum`,
}
OLDEST_10_DAYS_SEC := 1 * 60 * 60 * 24 * 10
template.Resources[LOGICAL_ID_CLOUDWATCH_ALARM_DEAD_LETTER_OLDEST_10DAYS.String()] = cfn.AWSCloudWatchAlarm{
ActionsEnabled:true,
AlarmName: resources.DeadLetterOldest10Days,
MetricName:"ApproximateAgeOfOldestMessage",
Namespace:`AWS/SQS`,
Threshold: float64(OLDEST_10_DAYS_SEC),
Dimensions:[]cfn.AWSCloudWatchAlarm_Dimension{
cfn.AWSCloudWatchAlarm_Dimension{
Name : `QueueName`,
Value: resources.DeadLetterQueueName,
},
},
TreatMissingData: `ignore`,
EvaluationPeriods:1,
Period:600,
ComparisonOperator: "GreaterThanOrEqualToThreshold",
AlarmActions:[]string{
cfn.Ref(LOGICAL_ID_SNS_ALARM_TOPIC_COMMON.String()),
},
OKActions:[]string{
cfn.Ref(LOGICAL_ID_SNS_ALARM_TOPIC_COMMON.String()),
},
Statistic:`Average`,
}
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/example1/export/template-`+ENV+`.yml`, y, 0777)
if err != nil {
panic(err)
}
}
定数系
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`
LOGICAL_ID_CLOUDWATCH_ALARM_DEAD_LETTER_DETECTED LOGICAL_ID = `CloudWatchAlarmDeadLetterDetected`
LOGICAL_ID_CLOUDWATCH_ALARM_DEAD_LETTER_OLDEST_10DAYS LOGICAL_ID = `CloudWatchAlarmDeadLetterOldest10Days`
LOGICAL_ID_SNS_ALARM_TOPIC_COMMON LOGICAL_ID = `SnsAlarmTopicCommon`
LOGICAL_ID_PERMISSION_LAMBDA_SNS_EVENT LOGICAL_ID = `PermissionLambdaSnsEvent`
LOGICAL_ID_LAMBDA_SNS_ALARM_TO_SLACK LOGICAL_ID = `LambdaSnsAlarmToSlack`
)
type ResourceNames struct {
DeadLetterQueueName string
FifoQueueName string
DeadLetterDetected string
DeadLetterOldest10Days string
AlarmSnsTopicCommon string
AlarmSnsSubscriptionCommon string
SnsAlarmToSlackLambdaTopic string
}
func NewResourceNames(env string) ResourceNames {
return ResourceNames{
DeadLetterQueueName: fmt.Sprintf(`%s-example1-dead-letter-queue.fifo` , env),
FifoQueueName: fmt.Sprintf(`%s-example1-fifo-queue.fifo` , env),
DeadLetterDetected: fmt.Sprintf(`%s-example1-deadletter-detected` , env),
DeadLetterOldest10Days: fmt.Sprintf(`%s-example1-deadletter-oldest-10days` , env),
AlarmSnsTopicCommon : fmt.Sprintf(`%s-example1-alarm-sns-topic-common` , env),
AlarmSnsSubscriptionCommon : fmt.Sprintf(`%s-example1-sns-subscription-common` , env),
SnsAlarmToSlackLambdaTopic : fmt.Sprintf(`%s-example1-sns-alarm-totify-slack-topic` , env),
}
}
func readJs(path string) (string, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
// エラー処理
log.Error(err)
return "", err
}
return string(data), nil
}
type ExistedResources struct {
ServerlessRole string
}
func GetExitedResources() ExistedResources {
return ExistedResources{
ServerlessRole: `arn:aws:iam::xxxxx:role/xxxxxx-serverless-role`,
}
}
インラインで記載するlambdaのコード
exports.handler = (event , context)=> {
console.log(JSON.stringify(event));
// Todo
// slackになんか送るコードを書く。
// Lambda Layerにaxiosとかいれとくと楽???
return {};
};
前回よりリソース定義がグッと増えて、CloudwatchやらSnsやらLambdaの実行Permission設定までも必要となっています。
Role自体は今回はIAMまでは自動生成せず、既存のものを使う前提で書きました。
また、実行するlambda(js)のコードは別ファイルとしてアップロードせず、インラインで記載する手法をやってみました。
生成されたtempalte
AWSTemplateFormatVersion: "2010-09-09"
Description: example1アプリの dev 環境です
Resources:
CloudWatchAlarmDeadLetterDetected:
Properties:
ActionsEnabled: true
AlarmActions:
- Ref: SnsAlarmTopicCommon
AlarmName: dev-example1-deadletter-detected
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: QueueName
Value: dev-example1-dead-letter-queue.fifo
EvaluationPeriods: 1
MetricName: ApproximateNumberOfMessagesVisible
Namespace: AWS/SQS
OKActions:
- Ref: SnsAlarmTopicCommon
Period: 60
Statistic: Minimum
Threshold: 0.001
TreatMissingData: ignore
Type: AWS::CloudWatch::Alarm
CloudWatchAlarmDeadLetterOldest10Days:
Properties:
ActionsEnabled: true
AlarmActions:
- Ref: SnsAlarmTopicCommon
AlarmName: dev-example1-deadletter-oldest-10days
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: QueueName
Value: dev-example1-dead-letter-queue.fifo
EvaluationPeriods: 1
MetricName: ApproximateAgeOfOldestMessage
Namespace: AWS/SQS
OKActions:
- Ref: SnsAlarmTopicCommon
Period: 600
Statistic: Average
Threshold: 864000
TreatMissingData: ignore
Type: AWS::CloudWatch::Alarm
LambdaSnsAlarmToSlack:
Properties:
Code:
ZipFile: "exports.handler = (event , context)=> {\n console.log(JSON.stringify(event));\n
\ // Todo\n // slackになんか送るコードを書く。\n // Lambda Layerにaxiosとかいれとくと楽???\n
\ \n return {};\n};\n"
Handler: index.handler
MemorySize: 256
Role: arn:aws:iam::xxxxx:role/xxxxxx-serverless-role
Runtime: nodejs8.10
Timeout: 30
Type: AWS::Lambda::Function
PermissionLambdaSnsEvent:
Properties:
Action: lambda:InvokeFunction
FunctionName:
Fn::GetAtt:
- LambdaSnsAlarmToSlack
- Arn
Principal: sns.amazonaws.com
SourceArn:
Ref: SnsAlarmTopicCommon
Type: AWS::Lambda::Permission
SnsAlarmTopicCommon:
Properties:
Subscription:
- Endpoint:
Fn::GetAtt:
- LambdaSnsAlarmToSlack
- Arn
Protocol: lambda
TopicName: dev-example1-alarm-sns-topic-common
Type: AWS::SNS::Topic
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
デプロイ && 実行
aws cloudformation deploy --template-file <テンプレ名> --stack-name <スタック名> --s3-bucket <アップロード先> --s3-prefix <任意>
すればデプロイ完了です。
デッドレターキューにメッセージを登録し、アラートが上がり、Lambdaの実行ログも出てたらとりあえずは完成でいいでしょう。
ハマりどころ & 課題
- 論理ID部分の
PermissionLambdaSnsEvent
がなかなか曲者で、これを設定しないとSNS側の設定だけではLambdaが実行されません。ハマった。
LambdaのイベントソースとしてSNSが設定されてるかとかは確認するといいかと思います。
- CloudwatchAlarmのThresholdを0にするとフィールドがomitemptyされてしまってvalidateでエラーが出るのが曲者。
これどうしたら良いんだ。。> 0
で設定したいのだが。
template.Resources[LOGICAL_ID_CLOUDWATCH_ALARM_DEAD_LETTER_DETECTED.String()] = cfn.AWSCloudWatchAlarm{
ActionsEnabled: true,
AlarmName: resources.DeadLetterDetected,
MetricName:"ApproximateNumberOfMessagesVisible",
Namespace:`AWS/SQS`,
Threshold: 0.001,
Dimensions:[]cfn.AWSCloudWatchAlarm_Dimension{
cfn.AWSCloudWatchAlarm_Dimension{
Name : `QueueName`,
Value: resources.DeadLetterQueueName,
},
},
...
まとめ
アラート周りはいつもプロジェクトの最初にちゃちゃっと設定したり、GUI側から登録すると勝手にpermissionを作ってくれる面がありましたので、今回定義コードを作ってみて、その部分は改めて何が行われているかを知るいい機会になりました。
また、goformationのCloudwatch周りはまだまだ単純に map[string]interface{}
で定義されているだけの項目も多かったので、そのへんは拡充を期待したいところです。(コミットしろ)