AWS日記19 (CloudWatch Events)

# はじめに
今回は AWS CloudWatch Events を試します。
スケジュールをcron式で設定し、Lambda関数を実行するページを作成します。
[Lambda関数・SAMテンプレート]
(https://github.com/tanaka-takurou/serverless-application-cron-page-go)


# 準備
[AWS SAM の準備をします](https://qiita.com/tanaka_takurou/items/cae7c19fc3aaf0031abb)

[AWS CloudWatch Eventsの資料]
[AWS CloudWatch Events](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/WhatIsCloudWatchEvents.html)


# AWS SAM テンプレート作成

AWS SAM テンプレートで API-Gateway , Lambda, CloudWatch Eventsの設定をします。

[参考資料]
[AWS SAM テンプレートを作成する](https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/tutorial-lambda-sam-template.html)

<details><summary>template.yml</summary><div>

```yaml:template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Serverless Application Cron

Parameters:
  ApplicationName:
    Type: String
    Default: 'ServerlessApplicationCron'
  CronEventRuleName:
    Type: String
    Default: 'CronEventRule'
  CronMainFunctionName:
    Type: String
    Default: 'CronMainFunction'
  FrontPageApiStageName:
    Type: String
    Default: 'ProdStage'

Metadata:
  AWS::ServerlessRepo::Application:
    Name: Serverless-Application-Cron
    Description: 'This application is test for CloudWatch Events.'
    Author: tanaka-takurou
    SpdxLicenseId: MIT
    LicenseUrl: LICENSE.txt
    ReadmeUrl: README.md
    Labels: ['ServerlessRepo']
    HomePageUrl: https://github.com/tanaka-takurou/serverless-application-cron-page-go
    SemanticVersion: 0.0.1
    SourceCodeUrl: https://github.com/tanaka-takurou/serverless-application-cron-page-go

Resources:
  FrontPageApi:
    Type: AWS::Serverless::Api
    Properties:
      EndpointConfiguration: REGIONAL
      StageName: !Ref FrontPageApiStageName
  FrontPageFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Cron Front Function'
      Events:
        FrontPageApi:
          Type: Api
          Properties:
            Path: '/'
            Method: get
            RestApiId: !Ref FrontPageApi
      Environment:
        Variables:
          REGION: !Ref 'AWS::Region'
          API_PATH: !Join [ '', [ '/', !Ref FrontPageApiStageName, '/api'] ]
  MainFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Ref CronMainFunctionName
      CodeUri: api/bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Cron API Function'
      Policies:
      - Statement:
        - Effect: 'Allow'
          Action:
            - 'logs:CreateLogGroup'
            - 'logs:CreateLogStream'
            - 'logs:PutLogEvents'
          Resource: '*'
        - Effect: 'Allow'
          Action:
            - 'lambda:*'
            - 'events:DescribeRule'
            - 'events:PutRule'
          Resource: '*'
      Environment:
        Variables:
          LAST_EVENT: ""
          EVENT_NAME: !Ref CronEventRuleName
          FUNCTION_NAME: !Ref CronMainFunctionName
          REGION: !Ref 'AWS::Region'
      Events:
        FrontPageApi:
          Type: Api
          Properties:
            Path: '/api'
            Method: post
            RestApiId: !Ref FrontPageApi
  CronApiPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref MainFunction
      Principal: apigateway.amazonaws.com
  CronEventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Ref CronEventRuleName
      Description: CronRule
      ScheduleExpression: 'cron(0 12 * * ? *)'
      State: 'ENABLED'
      Targets:
        - Arn: !GetAtt MainFunction.Arn
          Id: TargetCronFunction
  PermissionForEventsToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref MainFunction
      Action: 'lambda:InvokeFunction'
      Principal: 'events.amazonaws.com'
      SourceArn: !GetAtt CronEventRule.Arn

Outputs:
  APIURI:
    Value: !Join [ '', [ 'https://', !Ref FrontPageApi, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/',!Ref FrontPageApiStageName,'/'] ]
```
</div></details>

### CloudWatch Eventsの設定は以下の部分
```yaml
  CronEventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Ref CronEventRuleName
      Description: CronRule
      ScheduleExpression: 'cron(0 12 * * ? *)'
      State: 'ENABLED'
      Targets:
        - Arn: !GetAtt MainFunction.Arn
          Id: TargetCronFunction
  PermissionForEventsToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref MainFunction
      Action: 'lambda:InvokeFunction'
      Principal: 'events.amazonaws.com'
      SourceArn: !GetAtt CronEventRule.Arn
```


# Lambda関数作成

※ Lambda関数は aws-lambda-go を利用し、CloudWatch Events周りの処理は aws-sdk-go-v2 を利用しました。
[AWS SDK for Go API Reference V2](https://docs.aws.amazon.com/sdk-for-go/v2/api/service/cloudwatchevents/)


### ルールを追加・更新するには PutRule を使う
```golang
func putRule(ctx context.Context, minute string, hour string, day string, month string, year string) error {
	var m_ int
	var h_ int
	var d_ int
	var o_ int
	var y_ int
	m_, _ = strconv.Atoi(minute)
	h_, _ = strconv.Atoi(hour)
	d_, _ = strconv.Atoi(day)
	o_, _ = strconv.Atoi(month)
	y_, _ = strconv.Atoi(year)
	if m_ < 0 {
		m_ = 0
	}
	sm := strconv.Itoa(m_)
	if h_ < 0 {
		h_ = 0
	}
	sh := strconv.Itoa(h_)
	sd := "*"
	if d_ > 0 {
		sd = strconv.Itoa(d_)
	}
	so := "*"
	if o_ > 0 {
		so = strconv.Itoa(o_)
	}
	sy := "*"
	if y_ >= 1970 {
		sy = strconv.Itoa(y_)
	}
	if cloudwatcheventsClient == nil {
		cloudwatcheventsClient = getCloudwatcheventsClient()
	}
	params := &cloudwatchevents.PutRuleInput{
		Name: aws.String(os.Getenv("EVENT_NAME")),
		ScheduleExpression: aws.String("cron(" + sm + " " + sh + " " + sd + " " + so + " ? " + sy + ")"),
	}
	res, err := cloudwatcheventsClient.PutRule(ctx, params)
	if err != nil {
		log.Print(err)
		return err
	}
	log.Printf("%+v\n", res)
	return nil
}
```


### ルールの内容を取得するには DescribeRule を使う
```golang
func describeRule(ctx context.Context)(string, error) {
	if cloudwatcheventsClient == nil {
		cloudwatcheventsClient = getCloudwatcheventsClient()
	}
	params := &cloudwatchevents.DescribeRuleInput{
		Name: aws.String(os.Getenv("EVENT_NAME")),
	}
	res, err := cloudwatcheventsClient.DescribeRule(ctx, params)
	if err != nil {
		log.Print(err)
		return "", err
	}
	return stringValue(res.ScheduleExpression), nil
}
```

# 終わりに
cron式で指定するタイムゾーンはUTCのため、時差を考慮する必要があります。
Lambda関数以外も CloudWatch Eventから実行できるため、今後試していこうと思います。

<details><summary>参考資料</summary><div>
[Amazon CloudWatch Events で cron 式を使う場合は時差に気をつける](https://qiita.com/niwasawa/items/6e0b191d6aa21213c7ec)
[CloudWatch Eventについての整理・備忘録](https://qiita.com/RyoMa_0923/items/61b67e12fb8aebd08241)
[CloudFormationでCloudWatch EventsとLambdaを構築](https://qiita.com/tomoki_s/items/d23cd5eca26f7d778b73)
[ルールのスケジュール式](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/ScheduledEvents.html)