はじめに
もう何番煎じかわかりませんが、EC2インスタンスの自動起動・停止を実現する環境が欲しくなったので構築します。
会社で検証に使っているAWS環境が1週間ごとに消えてしまう仕様なので、毎回手動で設定しなくてもいいようにTerraformを使って環境構築の自動化を行いました。
実現したかったこと
- AWS EC2インスタンスの自動起動・停止を行い、利用料を削減する
- 平日の9:00〜21:00の間だけ稼働させる。ただし休日に作業したい際や、夜通し動かしたいといった要件に備えて柔軟にスケジュール設定できるようにしておく
- 検証環境のため、対象サーバは基本全部。インスタンスが増えてもコードや設定をいじらずに対応できるようにする。また一応特定のインスタンスをターゲットにできるようにしておく
- できる限り最小限の稼働で環境構築を行う
成果物と使い方
以下のGitLab.comに今回作成したコードをあげています。
「とにかく使えればいい」場合はこちらからcloneなりforkしてREADMEを参照の上お使いください。
突貫工事のため、コードが汚いですがご了承ください。
- Gitリポジトリ:https://gitlab.com/skitamur/aws-terraform
- 使い方:https://gitlab.com/skitamur/aws-terraform/-/blob/main/autostartstop_function/README.md
参考手順
基本は以下の公式ページのプロセスを参考にTerraformを作成しています。
https://aws.amazon.com/jp/premiumsupport/knowledge-center/start-stop-lambda-eventbridge/
前提条件
- AWS IAMユーザー(AdministratorAccess)
- Terrafrom v1.1.2
- aws provider : v3.74.0
- archive : v2.2.0
概要
各tfファイルの簡単な概要と解説になります。
フォルダ・ファイル構成
$ tree autostartstop_function
autostartstop_function
.
├── README.md
├── cloudwatch_event.tf
├── data_lambda_src.tf
├── iam_lambda.tf
├── lambda_function.tf
├── lambda_permission.tf
├── local.tf
├── provider.tf
└── src
├── autostart
│ └── autostart.py
└── autostop
└── autostop.py
Provider
- Gitにあげる関係上、access_keyとsecret_keyは環境変数にて設定する記載にしています(設定方法はGitのREADMEを参照)
- Lambdaにコードをアップする際に該当のフォルダをzip圧縮する必要があるため、
archive provider
を使用します。
provider.tf
provider "aws" {
#Set the access key to the environment variable "AWS_ACCESS_KEY_ID".
#Set the secret key to the environment variable "AWS_SECRET_ACCESS_KEY".
region = local.region
}
provider "archive" {}
local.tf(抜粋)
locals {
region = "ap-northeast-1"
IAM
- Lambda用のIAMロール、IAMポリシーを作成して
aws_iam_policy_attachment
で関連づけます。 - 参考ページとほぼ同じポリシーですが、実環境からインスタンスIDを取得するため、ポリシーに
ec2:Describe*
actionを追記しています。
iam_lambda.tf
resource "aws_iam_role" "lambda_role" {
name = local.iam_role.name
assume_role_policy = local.iam_role.assume_role_policy
}
resource "aws_iam_policy" "lambda_policy" {
name = local.iam_policy.name
policy = local.iam_policy.policy
}
resource "aws_iam_policy_attachment" "lambda-iam-attach" {
name = "lambda-iam-attachment"
roles = [
aws_iam_role.lambda_role.name
]
policy_arn = aws_iam_policy.lambda_policy.arn
}
local.tf(抜粋)
iam_role = {
name = "AutoStartStopRoleForLambda"
assume_role_policy = <<EOT
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOT
}
iam_policy = {
name = "AutoStartStopPolicy"
policy = <<EOT
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"ec2:Start*",
"ec2:Stop*",
"ec2:Describe*"
],
"Resource": "*"
}
]
}
EOT
}
Lambda用Python
- 参考ページのコードをベースとしていますが、以下2点の修正を行なっています。
- ReginをLambdaの環境変数から取得する。もし環境変数
REGION
の指定がない場合はデフォルト値ap-northeast-1
を使う - 指定のRegion上にあるインスタンスIDを取得し、配列に格納。もし環境変数
INSTANCES
の指定がない場合は取得したインスタンスID全てを対象とする(詳細は後述)
- ReginをLambdaの環境変数から取得する。もし環境変数
autostart.py
import boto3
import os
region = os.environ.get('REGION','ap-northeast-1')
ec2 = boto3.client('ec2', region_name=region)
ec2_data = ec2.describe_instances()
ec2_instance_id = list()
for ec2_reservation in ec2_data['Reservations']:
for ec2_instance in ec2_reservation['Instances']:
ec2_instance_id.append(ec2_instance['InstanceId'])
instances = os.environ.get('INSTNACES',ec2_instance_id)
def lambda_handler(event, context):
ec2.start_instances(InstanceIds=instances)
print('started your instances: ' + str(instances))
autostop.py
import boto3
import os
region = os.environ.get('REGION','ap-northeast-1')
ec2 = boto3.client('ec2', region_name=region)
ec2_data = ec2.describe_instances()
ec2_instance_id = list()
for ec2_reservation in ec2_data['Reservations']:
for ec2_instance in ec2_reservation['Instances']:
ec2_instance_id.append(ec2_instance['InstanceId'])
instances = os.environ.get('INSTNACES',ec2_instance_id)
def lambda_handler(event, context):
ec2.stop_instances(InstanceIds=instances)
print('stopped your instances: ' + str(instances))
Pythonスクリプトを圧縮
- Lambdaに作成したPythonスクリプトをアップする際はzipに圧縮する必要があるため、
archive provider
のdata.archive_file
を使って圧縮します。
data_lambda_src.tf
# Lambda File archive to Zip
data "archive_file" "autostart_lambda_file" {
type = local.autostart_lambda_file.archive_file_type
source_dir = local.autostart_lambda_file.lambda_source_dir
output_path = local.autostart_lambda_file.deploy_upload_filename
}
# Lambda File archive to Zip
data "archive_file" "autostop_lambda_file" {
type = local.autostop_lambda_file.archive_file_type
source_dir = local.autostop_lambda_file.lambda_source_dir
output_path = local.autostop_lambda_file.deploy_upload_filename
}
local.tf(抜粋)
autostart_lambda_file = {
archive_file_type = "zip"
lambda_source_dir = "./src/autostart"
deploy_upload_filename = "./src/autostart_lambda_src.zip"
}
autostop_lambda_file = {
archive_file_type = "zip"
lambda_source_dir = "./src/autostop"
deploy_upload_filename = "./src/autostop_lambda_src.zip"
}
Lambda Function
- いくつかパラメータがあるので、特徴的なものを説明します。
-
filename
:Lambdaスクリプトを指定。今回はarchive_file
で作成したzipファイルを参照 -
role
:前述のIAMロールを指定 -
handler
:実行するLambdaのハンドラーを指定。今回は上記のスクリプトの通りautostart.lambda_handler
となる -
source_code_hash
:ソースコードのハッシュ値を参照して、もし異なる場合はソースコードの更新を実施する
-
-
local.tf
のvariables
でLambdaの環境変数を指定しています。ここでINSTANCES
をコメントアウトしているので、デフォルトでは全インスタンスを対象としています。 - もしインスタンスを指定する場合はコメントアウトを解除し、記載方法を参考に修正してください。
lambda_function.tf
resource "aws_lambda_function" "autostart_lambda" {
filename = data.archive_file.autostart_lambda_file.output_path
function_name = local.autostart_lambda_function.function_name
role = aws_iam_role.lambda_role.arn
handler = local.autostart_lambda_function.handler
source_code_hash = data.archive_file.autostart_lambda_file.output_base64sha256
runtime = local.autostart_lambda_function.runtime
timeout = local.autostart_lambda_function.timeout
environment {
variables = local.autostart_lambda_function.variables
}
}
resource "aws_lambda_function" "autostop_lambda" {
filename = data.archive_file.autostop_lambda_file.output_path
function_name = local.autostop_lambda_function.function_name
role = aws_iam_role.lambda_role.arn
handler = local.autostop_lambda_function.handler
source_code_hash = data.archive_file.autostop_lambda_file.output_base64sha256
runtime = local.autostop_lambda_function.runtime
timeout = local.autostop_lambda_function.timeout
environment {
variables = local.autostop_lambda_function.variables
}
}
local.tf(抜粋)
autostart_lambda_function = {
filename = "autostart.py"
function_name = "StartEC2Instances"
runtime = "python3.9"
timeout = 10
handler = "autostart.lambda_handler"
variables = {
REGION = local.region
# If INSTANCES is not set, then all instances will be targeted.
#INSTANCES = "[i-XXXXXXXXXXX,i-XXXXXXXXXXX]"
}
}
autostop_lambda_function = {
filename = "autostop.py"
function_name = "StopEC2Instances"
runtime = "python3.9"
timeout = 10
handler = "autostop.lambda_handler"
variables = {
REGION = local.region
# If INSTANCES is not set, then all instances will be targeted.
#INSTANCES = "[i-XXXXXXXXXXX,i-XXXXXXXXXXX]"
}
}
EventBridge
- CloudWatch EventはEventBridgeに包含されましたが、使うresourceは
aws_cloudwatch_event_rule
やaws_cloudwatch_event_target
になります。 -
schedule_expression
にcronを記述することで柔軟なスケジュール実行を実現できます。- cronはUTC(日本時間-9時間)なので、時間の記載には気をつけてください。
-
is_enabled
にtrue
やfalse
を設定することで、自動実行のON/OFFを切り替えられます。「停止は自動化したいけど起動は自分でやる」といった使い方にも対応可能です。
cloudwatch_event.tf
# Auto Start Rule
resource "aws_cloudwatch_event_rule" "autostart_rule" {
name = local.autostart_event_rule.name
description = local.autostart_event_rule.description
schedule_expression = local.autostart_event_rule.schedule_expression
is_enabled = local.autostart_event_rule.is_enabled
}
# Auto Start Target
resource "aws_cloudwatch_event_target" "autostart_target" {
arn = aws_lambda_function.autostart_lambda.arn
rule = aws_cloudwatch_event_rule.autostart_rule.id
}
# Auto Stop Rule
resource "aws_cloudwatch_event_rule" "autostop_rule" {
name = local.autostop_event_rule.name
description = local.autostop_event_rule.description
schedule_expression = local.autostop_event_rule.schedule_expression
is_enabled = local.autostop_event_rule.is_enabled
}
# Auto Stop Target
resource "aws_cloudwatch_event_target" "autostop_target" {
arn = aws_lambda_function.autostop_lambda.arn
rule = aws_cloudwatch_event_rule.autostop_rule.id
}
local.tf(抜粋)
autostart_event_rule = {
name = "StartEC2Instances"
description = "Start EC2 Instances"
# Run at 0:00 (UTC) every Monday through Friday
schedule_expression = "cron(0 0 ? * MON-FRI *)"
is_enabled = true
}
autostop_event_rule = {
name = "StopEC2Instances"
description = "Stop EC2 Instances"
# Run at 12:00 (UTC) every Monday through Friday
schedule_expression = "cron(0 12 ? * MON-FRI *)"
is_enabled = true
}
Lambdaのパーミッション設定
- 参考ページの手順では気にすることのない設定ですが、Terraformで実行する際は明示的に設定が必要な箇所になります。
- 詳細は以下のページに記載されています。
- ここは
local.tf
で設定する必要のない箇所ですので、「ふーんこんなのが必要なんだ」くらいの認識でOKです。
lambda_permission.tf
resource "aws_lambda_permission" "start_allow_cloudwatch" {
statement_id = "AllowExecutionFromCloudWatch"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.autostart_lambda.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.autostart_rule.arn
}
resource "aws_lambda_permission" "stop_allow_cloudwatch" {
statement_id = "AllowExecutionFromCloudWatch"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.autostop_lambda.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.autostop_rule.arn
}
local.tf(全体)
local.tf
locals {
region = "ap-northeast-1"
iam_role = {
name = "AutoStartStopRoleForLambda"
assume_role_policy = <<EOT
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOT
}
iam_policy = {
name = "AutoStartStopPolicy"
policy = <<EOT
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"ec2:Start*",
"ec2:Stop*",
"ec2:Describe*"
],
"Resource": "*"
}
]
}
EOT
}
autostart_lambda_function = {
filename = "autostart.py"
function_name = "StartEC2Instances"
runtime = "python3.9"
timeout = 10
handler = "autostart.lambda_handler"
variables = {
REGION = local.region
# If INSTANCES is not set, then all instances will be targeted.
#INSTANCES = "[i-XXXXXXXXXXX,i-XXXXXXXXXXX]"
}
}
autostop_lambda_function = {
filename = "autostop.py"
function_name = "StopEC2Instances"
runtime = "python3.9"
timeout = 10
handler = "autostop.lambda_handler"
variables = {
REGION = local.region
# If INSTANCES is not set, then all instances will be targeted.
#INSTANCES = "[i-XXXXXXXXXXX,i-XXXXXXXXXXX]"
}
}
autostart_lambda_file = {
archive_file_type = "zip"
lambda_source_dir = "./src/autostart"
deploy_upload_filename = "./src/autostart_lambda_src.zip"
}
autostop_lambda_file = {
archive_file_type = "zip"
lambda_source_dir = "./src/autostop"
deploy_upload_filename = "./src/autostop_lambda_src.zip"
}
autostart_event_rule = {
name = "StartEC2Instances"
description = "Start EC2 Instances"
# Run at 0:00 (UTC) every Monday through Friday
schedule_expression = "cron(0 0 ? * MON-FRI *)"
is_enabled = true
}
autostop_event_rule = {
name = "StopEC2Instances"
description = "Stop EC2 Instances"
# Run at 12:00 (UTC) every Monday through Friday
schedule_expression = "cron(0 12 ? * MON-FRI *)"
is_enabled = true
}
}
おわりに
当初実現したいことはこのコードで概ね実現できました。
Tagで判別して云々すればもっと柔軟な設定を実現できますが、あくまで検証用ですので、これくらいでいいかなと思います。