👋

【Terraform】特定タグのEC2インスタンスを特定の時間に停止する

2023/07/22に公開

はじめに

今回はEC2を特定の時間で停止できるような環境を用意しました。
EC2の指定はLambda関数がタグを検索することで行います。
Lambdaの指定はEventBridge Schedulerに任せています。

環境のプロビジョニングにはTerraformを使用しています。

イメージ図

実行結果

例えばスケジュール起動のCronをこのように設定した場合

resource "aws_scheduler_schedule" "cron" {
  flexible_time_window {
    mode = "OFF"
  }
  // 略
  schedule_expression          = "cron(33 23 ? * * *)"
  schedule_expression_timezone = "Asia/Tokyo"
}

23:33分にスケジューラによってLambda関数が起動され、EC2を停止します。

前提

  • runningのEC2を用意済
  • プロビジョニングにTerraformを使用する

コード

今回使用するコード

lambda.tf(Lambda関数周りのtfファイル)

data "archive_file" "example_zip" {
  type        = "zip"
  source_dir  = "${path.module}/lambda_function"
  output_path = "${path.module}/example_lambda.zip"
}

resource "aws_lambda_function" "example_lambda" {
  function_name    = "example-lambda"
  handler          = "main.lambda_handler"
  runtime          = "python3.10"
  filename         = data.archive_file.example_zip.output_path
  source_code_hash = filebase64sha256(data.archive_file.example_zip.output_path)
  role             = aws_iam_role.lambda_role.arn
  timeout          = 10
}

resource "aws_iam_role" "lambda_role" {
  name = "example-lambda-role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

resource "aws_iam_policy" "log_policy" {
  name        = "example-lambda-policy"
  description = "IAM policy for the example Lambda function"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = [
          aws_cloudwatch_log_group.example_log_group.arn,
          "${aws_cloudwatch_log_group.example_log_group.arn}:*"
        ]
      }
    ]
  })
}


resource "aws_iam_role_policy_attachment" "log_policy_attachment" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = aws_iam_policy.log_policy.arn
}

resource "aws_iam_policy" "ec2_stop_policy" {
  name        = "ec2_stop_policy"
  path        = "/"
  description = "IAM policy for stopping EC2 instances"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:StopInstances"
      ],
      "Resource": "*"
    }
  ]
}
EOF
}

resource "aws_iam_role_policy_attachment" "ec2_stop_policy_attachment" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = aws_iam_policy.ec2_stop_policy.arn
}

resource "aws_cloudwatch_log_group" "example_log_group" {
  name              = "/aws/lambda/${aws_lambda_function.example_lambda.function_name}"
  retention_in_days = 30
}

eventbridge-scheduler.tf(スケジューラー周りのtfファイル)

resource "aws_scheduler_schedule" "cron" {
  name = "ec2-stop-schedule"

  flexible_time_window {
    mode = "OFF"
  }

  target {
    arn      = aws_lambda_function.example_lambda.arn
    role_arn = aws_iam_role.schedule_role.arn
  }

  schedule_expression          = "cron(xx xx ? * * *)"
  schedule_expression_timezone = "Asia/Tokyo"
}

resource "aws_iam_role" "schedule_role" {
  name = "schedule-role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "scheduler.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}


resource "aws_iam_role_policy_attachment" "schedule_policy_attachment" {
  role       = aws_iam_role.schedule_role.name
  policy_arn = aws_iam_policy.schedule_policy.arn
}

resource "aws_iam_policy" "schedule_policy" {
  name        = "schedule_policy"
  path        = "/"
  description = "IAM policy for invoking Lambda Function"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "lambda:InvokeFunction"
      ],
      "Resource": "${aws_lambda_function.example_lambda.arn}"
    }
  ]
}
EOF
}

main.py(EC2停止用Lambda関数)

import boto3

def lambda_handler(event, context):
    ec2 = boto3.resource('ec2')

    filters = [{
            'Name': 'tag:AutoStop',
            'Values': ['True']
        },
        {
            'Name': 'instance-state-name', 
            'Values': ['running']
        }
    ]

    instances = ec2.instances.filter(Filters=filters)

    RunningInstances = [instance.id for instance in instances]

    if len(RunningInstances) > 0:
        # perform the shutdown
        stoppingInstances = ec2.instances.filter(InstanceIds=RunningInstances).stop()
        print("Stopped instances: " + str(RunningInstances))
    else:
        print("No instances to stop")

ポイント

① Lambda関数のタイムアウト設定

Lambdaのタイムアウトはデフォルトで3秒です。
このようなタイムアウトのログが出る場合は設定しましょう。

{"errorMessage":"2023-07-22T11:53:57.896Z 86702c62-5722-4403-9038-c8f433cc13d2 Task timed out after 3.02 seconds"}~

本環境では`10秒に設定したところ、2台のEC2停止が成功しています。

resource "aws_lambda_function" "example_lambda" {
  // 略
  timeout          = 10
}

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-function-common.html#configuration-timeout-console

② EC2停止用のIAMポリシ適用

Lambda関数がEC2を検索、停止できるように適切なポリシーを与える必要があります。

resource "aws_iam_policy" "ec2_stop_policy" {
  name        = "ec2_stop_policy"
  path        = "/"
  description = "IAM policy for stopping EC2 instances"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:StopInstances"
      ],
      "Resource": "*"
    }
  ]
}
EOF
}

aws_lambda_permissionが不要

てっきり必要なものだと思っていたのですが、使ってところなくても動作するようです。
理由はわからないので後日調べてみます。

resource "aws_lambda_permission" "allow_eventbridge-scheduler" {
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.example_lambda.function_name
  principal     = "scheduler.amazonaws.com"
}

④ ログ確認

Lambda関数をSchedulerがInvokeした記録を見れればいいのだが、
コンソールからは見つけられませんでした。
次回の課題とします。

とりあえずLambda関数のロググループから起動時間を確認し、
スケジューラの設定時間+20秒前後で実行されていればよしとします。

⑤ 特定のタグを持ったEC2を停止させるLambda関数

Lambda関数のポイントは以下です。

  • タグキーが「AutoStop」、タグバリューが「True」であり、状態が「running」であるEC2のインスタンスIDを取得
    • filterの内容はこちらを見れば良さげです
  • 取得したインスタンスIDのインスタンスをstopメソッドで停止させます。

filterのサンプル
https://boto3.amazonaws.com/v1/documentation/api/latest/guide/migrationec2.html#checking-what-instances-are-running

stopメソッド
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2/instance/stop.html

残った課題

  • aws_lambda_permissionが不要なのは何故か
  • CloudTrailからLambda関数のInvokeログを取得する

終わりに

特定タグのEC2停止はコストに直結するため、Terraformで楽に用意出来るのはとても便利だと感じました。
いくつか課題が残っているので調べないとですね。

参考

https://qiita.com/neruneruo/items/34d5092af44d79914849
https://medium.com/@igorkachmaryk/using-terraform-to-setup-aws-eventbridge-scheduler-and-a-scheduled-ecs-task-1208ae077360

EventBridgeスケジューラのTerraformドキュメント
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/scheduler_schedule

Discussion