AWS Lambda+EventBridgeを利用した時間起動の自動処理

## はじめに
稼働時間外にシステムへアクセスがあったユーザに対して、稼働時間外であることを伝えるページを表示させるため、AWS Lambda+EventBridgeにて自動化してみました。

今回のケースに限らず、他の処理自動化にも流用することができると思います。

## 環境
- macOS Sonoma 14.1
- terraform 1.5.2

## 目次
- イメージ図
- ELBリスナールールの作成
- Lambdaの設定
    - Lambda関数の作成
    - iamロールの設定
- EventBridgeの設定
    - EventBridgeの作成
    - targetの設定
    - lambda permissionの設定
- 補足

## イメージ図
対象システムにはELBを配置しているため、ELBリスナールールにて稼働時間外メッセージ表示、管理を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3314945/d48cbfad-5746-5e75-f3ee-60bc80e5afb5.png)


## ELBリスナールールの作成
まずは、稼働時間外メッセージ表示のリスナールールを作成します。

```terraform:aws_lb_listener_rule.tf
# aws_lb_listener_rule.change_lb_listener_rule_priority_foo_close:
resource "aws_lb_listener_rule" "change_lb_listener_rule_priority_foo_close" {
    listener_arn = aws_lb_listener.foo-https.arn
    priority     = 1000
    tags         = {}
    action {
        order            = 1
        type             = "fixed-response"
        fixed_response {
            content_type = "text/html"
            message_body = <<-EOT
                <!doctype html>
                <head>
                <meta charset="utf-8">
                <title>システム稼働時間外</title>
                </head>
                <body>
                <header>
                <div id="content">
                <section class="row">
                    <div class="col-full">
                    <p>
                        当サイトはシステム稼働時間外のためアクセスできません。<br>
                        稼働時間まで今しばらくお待ちください。<br>
                    </p>
                    <p>
                        This site is currently out of operation hours<br>
                        Please wait a while until the operation time.<br>
                    </p>
                    <h1>稼働時間<br>08:00 〜 19:00</h1>
                    </div>
                </section>
                <section class="row">
                <div class="col-full">
                    <p>
                    <span style="font-weight:bold;">株式会社foo</span><br /> <span style="font-size:70%;">© foo Inc.</span>
                    </p>
                </div>
                </section>
                </div>
                </body>
                </html>
                EOT
            status_code  = "503"
        }
    }
    condition {
        host_header {
            values = [
                "foo.com",
                "bar.com",
            ]
        }
    }
    lifecycle {
        ignore_changes = [
            priority,
            action[0].fixed_response[0].message_body
        ]
    }
}
```

## Lamdbaの設定
### Lambda関数の作成
続いて、Lambda関数を作成します。
作成したELBリスナールールの`priority`順位を変更することにより、稼働時間外メッセージの表示・非表示を切り替えるための関数になります。
```python:/../../lambda_functions/change_lb_listener_rule_priority/lambda_function.py
import boto3
import json

def lambda_handler(event, context):
    print(json.dumps(event))

    client = boto3.client('elbv2')

    try:
        # event値取得
        listener_rule_arn = event['resources'][0]
        priority_value = int(event['detail']['priority'][0])

        # リスナールールのpriority変更
        response = client.set_rule_priorities(
            RulePriorities=[
                {
                    'RuleArn': listener_rule_arn,
                    'Priority': priority_value
                },
            ]
        )
        return {
            'statusCode': 200,
            'body': f'Successfully updated rule priority for {listener_rule_arn} to {priority_value}'
        }
    except Exception as e:
        raise e
```

`lambda_function.py`はterraformとは別ディレクトリにて管理していたため、`data.tf`内の`path.module`にてパスを指定し、参照するにようにしています。
```terraform:aws_lambda_function.tf
# aws_lambda_function.change_lb_listener_rule_priority:
resource "aws_lambda_function" "change_lb_listener_rule_priority" {
    architectures                  = [
        "x86_64",
    ]
    function_name                  = "change_lb_listener_rule_priority"
    filename                       = data.archive_file.change_lb_listener_rule_priority.output_path
    handler                        = "lambda_function.lambda_handler"
    layers                         = []
    memory_size                    = 128
    package_type                   = "Zip"
    publish                        = false
    source_code_hash               = data.archive_file.change_lb_listener_rule_priority.output_base64sha256
    reserved_concurrent_executions = -1
    role                           = aws_iam_role.LambdaRole_change_lb_listener_rule_priority.arn
    runtime                        = "python3.9"
    tags                           = {}
    timeout                        = 120
    ephemeral_storage {
        size = 512
    }
    tracing_config {
        mode = "PassThrough"
    }
    vpc_config {
        security_group_ids = [
            aws_security_group.lambda.id,
        ]
        subnet_ids         = [
            aws_subnet.protect-1a.id,
            aws_subnet.protect-1c.id,
            aws_subnet.protect-1d.id,
        ]
    }
}
```

```terraform:data.tf
data "archive_file" "change_lb_listener_rule_priority" {
    type        = "zip"
    output_path = "${path.module}/../../lambda_functions/change_lb_listener_rule_priority/change_lb_listener_rule_priority.zip"
    source_file = "${path.module}/../../lambda_functions/change_lb_listener_rule_priority/lambda_function.py"
  }
```
### iamロールの設定
Lambda関数に対してELBリスナールールの`priority`変更を実施できる権限を付与します。
```terraform:aws_iam_role.tf
# aws_iam_role.LambdaRole_change_lb_listener_rule_priority:
resource "aws_iam_role" "LambdaRole_change_lb_listener_rule_priority" {
    assume_role_policy    = jsonencode(
        {
            Statement = [
                {
                    Action    = "sts:AssumeRole"
                    Effect    = "Allow"
                    Principal = {
                        Service = "lambda.amazonaws.com"
                    }
                },
            ]
            Version   = "2012-10-17"
        }
    )
    force_detach_policies = false
    managed_policy_arns   = [
        "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
        "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole",
    ]
    max_session_duration  = 3600
    name                  = "LambdaRole_change_lb_listener_rule_priority"
    path                  = "/service-role/"
    tags                  = {}
    inline_policy {
        name   = "change_lb_rule_priority_policy"
        policy = jsonencode(
            {
                Statement = [
                    {
                        Action   = [
                            "elasticloadbalancing:SetRulePriorities"
                        ]
                        Effect   = "Allow"
                        Resource = [
                            "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*/*",
                            "arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*/*"
                        ]
                    },
                ]
                Version   = "2012-10-17"
            }
        )
    }
}
```


## EventBridgeの設定
### EventBridgeの作成
続いて、EventBridgeを作成します。
特定時刻にLambda関数を自動実行するようにします。
下記では7：55と19：00に実行するように設定しています。

```terraform:aws_cloudwatch_event_rule.tf
# aws_cloudwatch_event_rule.change_lb_listener_rule_priority_open_0755JST:
resource "aws_cloudwatch_event_rule" "change_lb_listener_rule_priority_open_0755JST" {
    description         = "システム稼働時間外メッセージを非表示する（LBリスナールールプライオリティ変更）"
    event_bus_name      = "default"
    is_enabled          = true
    name                = "change_lb_listener_rule_priority_open_0755JST"
    schedule_expression = "cron(55 22 ? * SUN-THU *)"
    tags                = {}
}

# aws_cloudwatch_event_rule.change_lb_listener_rule_priority_close_1900JST:
resource "aws_cloudwatch_event_rule" "change_lb_listener_rule_priority_close_1900JST" {
    description         = "システム稼働時間外メッセージを表示する(LBリスナールールプライオリティ変更）"
    event_bus_name      = "default"
    is_enabled          = true
    name                = "change_lb_listener_rule_priority_close_1900JST"
    schedule_expression = "cron(00 10 ? * MON-FRI *)"
    tags                = {}
}
```
### targetの設定
EventBridgeにtarget設定をすることにより、対象のLambda関数を実行します。
この時にEventBridge側からはevent値として`priority`をLambda関数に渡しています。

7:55では`priority`の優先度を下げて時間外メッセージを非表示に、19：00では`priority`の優先度を上げて時間外メッセージを表示するようにします。
```terraform:aws_cloudwatch_event_target.tf
# aws_cloudwatch_event_target.change_lb_listener_rule_priority_open_0755JST:
resource "aws_cloudwatch_event_target" "change_lb_listener_rule_priority_open_0755JST" {
    arn            = aws_lambda_function.change_lb_listener_rule_priority.arn
    rule           = aws_cloudwatch_event_rule.change_lb_listener_rule_priority_open_0755JST.name
    event_bus_name = "default"
    input          = jsonencode({
        "detail" : {
            "priority" : [
                "1000"
            ]
        },
        "resources" : [
            aws_lb_listener_rule.change_lb_listener_rule_priority_close.arn
        ]
    })
}
# aws_cloudwatch_event_target.change_lb_listener_rule_priority_close_1900JST:
resource "aws_cloudwatch_event_target" "change_lb_listener_rule_priority_close_1900JST" {
    arn            = aws_lambda_function.change_lb_listener_rule_priority.arn
    rule           = aws_cloudwatch_event_rule.change_lb_listener_rule_priority_close_1900JST.name
    event_bus_name = "default"
    input          = jsonencode({
        "detail": {
            "priority": [
                "1"
            ]
        },
        "resources": [
            aws_lb_listener_rule.change_lb_listener_rule_priority_close.arn
        ]
    })
}

```

### lambda permissionの設定
作成したEventBridgeがLambda関数を実行できるようlambda permissionにて実行権限を付与します。
`locals.tf`にてリスト化し、`for_each`でまとめて実行しています。
```terraform:aws_lambda_permission.tf
# aws_lambda_permission.events:
resource "aws_lambda_permission" "events" {
    for_each      = { for i in local.events_lambda_permission_list : "${i[0].name}-${i[1]}" => i }
    statement_id  = "AllowExecutionFromEventBridge-${each.value[0].name}"
    action        = "lambda:InvokeFunction"
    function_name = each.value[1]
    principal     = "events.amazonaws.com"
    source_arn    = each.value[0].arn
}
```
```terraform:locals.tf
# locals:
locals {
  events_lambda_permission_list = [
    [aws_cloudwatch_event_rule.change_lb_listener_rule_priority_open_0755JST,  aws_lambda_function.change_lb_listener_rule_priority.function_name],
    [aws_cloudwatch_event_rule.change_lb_listener_rule_priority_close_1900JST, aws_lambda_function.change_lb_listener_rule_priority.function_name],
]
}
```

## 補足
※ディレクトリ構成やterraformリソース管理の関係で一部コードは`data.tf`等による追加定義や修正が必要かもしれませんが、ご容赦ください。