LoginSignup
4
0

More than 3 years have passed since last update.

goformationで始めるCloudFormation (2) Cloudwatch Alarm/SNS/Lambda

Last updated at Posted at 2018-12-09

この記事は 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の実行ログも出てたらとりあえずは完成でいいでしょう。

1.png
3.png

ハマりどころ & 課題

  • 論理ID部分の PermissionLambdaSnsEvent がなかなか曲者で、これを設定しないとSNS側の設定だけではLambdaが実行されません。ハマった。 LambdaのイベントソースとしてSNSが設定されてるかとかは確認するといいかと思います。

2.png

  • 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{}で定義されているだけの項目も多かったので、そのへんは拡充を期待したいところです。(コミットしろ)

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0