【Terraform】特定タグのEC2インスタンスを特定の時間に停止する
はじめに
今回は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
}
② 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のサンプル
stop
メソッド
残った課題
-
aws_lambda_permission
が不要なのは何故か - CloudTrailからLambda関数のInvokeログを取得する
終わりに
特定タグのEC2停止はコストに直結するため、Terraformで楽に用意出来るのはとても便利だと感じました。
いくつか課題が残っているので調べないとですね。
参考
EventBridgeスケジューラのTerraformドキュメント
Discussion