3
4

More than 1 year has passed since last update.

Terraformを使ってEC2インスタンスの自動起動/自動停止環境を構築する

Last updated at Posted at 2022-02-01

はじめに

もう何番煎じかわかりませんが、EC2インスタンスの自動起動・停止を実現する環境が欲しくなったので構築します。
会社で検証に使っているAWS環境が1週間ごとに消えてしまう仕様なので、毎回手動で設定しなくてもいいようにTerraformを使って環境構築の自動化を行いました。

実現したかったこと

  • AWS EC2インスタンスの自動起動・停止を行い、利用料を削減する
  • 平日の9:00〜21:00の間だけ稼働させる。ただし休日に作業したい際や、夜通し動かしたいといった要件に備えて柔軟にスケジュール設定できるようにしておく
  • 検証環境のため、対象サーバは基本全部。インスタンスが増えてもコードや設定をいじらずに対応できるようにする。また一応特定のインスタンスをターゲットにできるようにしておく
  • できる限り最小限の稼働で環境構築を行う

成果物と使い方

以下のGitLab.comに今回作成したコードをあげています。
「とにかく使えればいい」場合はこちらからcloneなりforkしてREADMEを参照の上お使いください。
突貫工事のため、コードが汚いですがご了承ください。

参考手順

基本は以下の公式ページのプロセスを参考に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全てを対象とする(詳細は後述)
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 providerdata.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.tfvariablesで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_ruleaws_cloudwatch_event_targetになります。
  • schedule_expressionにcronを記述することで柔軟なスケジュール実行を実現できます。
    • cronはUTC(日本時間-9時間)なので、時間の記載には気をつけてください。
  • is_enabledtruefalseを設定することで、自動実行の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で判別して云々すればもっと柔軟な設定を実現できますが、あくまで検証用ですので、これくらいでいいかなと思います。

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4