🏷️

特定タグがついたEC2を起動停止させるTerraformの話

2024/07/01に公開

JINSでクラウドエンジニアとかをやっている@tskomaです。

はじめに

EC2インスタンス、利用されていないタイミングは停止しておきたいですよね!
開発環境や日中帯しか使わないEC2などは課金がもったいないので、使ってない時間は停止しておく運用を以前から実施しており、仕組み自体はよくある管理サーバのEC2を立て、そこからaws ec2 start-instancesをcronで実施するものでした。
この仕組みでもやりたいこととしては問題ないのですが

  • リストアなどでインスタンスIDが変わってしまうと再設定必要なのが面倒
  • そもそもcron動いていない状態でもEC2が起動しっぱなしなのがもったいない
  • せっかくならもうちょっとモダンな構成にしたいよね

ということで、もうちょっといい感じの仕組みに進化させてみました。

構成

構成図は比較的シンプルで以下の通りです。

流れはざっくり以下のような感じで、

  1. EC2に以下のようなタグをつける(SSM Agent入っている前提です)
    • 起動させたいインスタンス
      • Key : StartTime
      • Value : 起動したい時間(例えば19:00(UTC)なら19)
    • 停止させたいインスタンス
      • Key : StopTime
      • Value : 停止したい時間(例えば09:00(UTC)なら9)
  2. 特定のタグ条件で動くSSMドキュメントを作成する
    EC2を起動・停止するSSMドキュメントに以下のような定義を追加し、1.で設定したタグがついているEC2だけに起動・停止を実施するドキュメントを作成する。
Targets:
    - Key: 'tag:Start(Stop)Time'
      Values:
        - '{{ Start(Stop)Time }}'
  1. 00:00 - 23:00に対応する24個のEventBridge Ruleを作成し、ターゲットを2.で作成したSSMドキュメントとして自動化パラメータで以下のような入力を渡す
{
  "Start(Stop)Time": ["11"]
}

こうしてあげることで、11時(UTC)に実行されたEventBridgeが、StartTime:11というパラメータをSSM Automationに渡し、SSM AutomationはStartTime:11というタグが追加インスタンスにだけStartAutomationExecutionを実行することが可能になります。
4. あとは作ったEventBridgeを1時間おきにぶん回す

Terraform

という構成を、Terraformで実装すると以下のような感じになります。
実はそこそこ前に実装していたものなので、バージョン古めでごめんなさい。

対象 バージョン
Terraform 1.2.4
AWS 4.18.0

また、起動させるケースのみピックアップしますが、停止する場合はStartStopに読み替えてください。
叩くAPIがStopAutomationExecution以外やっていることはだいたい同じです。

IAMロール

resource "aws_iam_role" "ssm" {
  name = "ssm-role-pro"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { Service = ["events.amazonaws.com","ssm.amazonaws.com"]}
        Action    = "sts:AssumeRole"
      }
    ]
  })
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole"
  ]
}

resource "aws_iam_role_policy" "ssm_policy" {
  role = aws_iam_role.ssm.name
  policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Resource": ["*"],
      "Action": "tag:GetResources"
    }
  ]
}
POLICY
}

EventBridge Rule

resource "aws_cloudwatch_event_rule" "start" {
  count = 24
  name  = "start-${count.index}-hour-pro"
  schedule_expression = "cron(00 ${count.index} * * ? *)"
}

count = 24にしてEventBridge Ruleを量産させた上、schedule_expressionnameの中に${count.index}を入れることでリソース名と実行時刻を一致させてます。

EventBridge Target

locals{
  start_arn = "${aws_ssm_document.start_instance.arn}"
}

resource "aws_cloudwatch_event_target" "start" {
  count = 24
  arn = "${replace(local.start_arn, "document/", "automation-definition/")}"
  rule = aws_cloudwatch_event_rule.start[count.index].name
  role_arn = aws_iam_role.ssm.arn
  input = <<DOC
{
  "StartTime": ["${count.index}"]
}
DOC
}

ここに結構手こずったのですが、aws_cloudwatch_event_target内でtargetのarnを指定する際にautomation-definition/にしてあげないと「ここにinput入れるなー」と怒られてしまったため、arnを置換するやり方とっています。
その実装のために、local変数作ってreplaceやったりしていますが、もっとスマートな実装はありそう。
そもそも、どこかのバージョンでいい感じに改善されているかも?しれないので、もしご存じの方いれば教えてください...

SSM Document

resource "aws_ssm_document" "start_instance" {
  name          = "StartEC2Instances_UsingTags"
  document_type = "Automation"
  document_format = "YAML"
  content = <<DOC
description: 'StartEC2Instances Using Tags:StartTime'
schemaVersion: '0.3'
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
  StartTime:
    type: String
    default: null
    description: (Required) hour
  AutomationAssumeRole:
    type: String
    description: (Optional) The ARN of the role that allows Automation to perform the actions on your behalf.
    default: ''
mainSteps:
  - name: StartEC2Instances
    action: 'aws:executeAwsApi'
    inputs:
      Service: ssm
      Api: StartAutomationExecution
      DocumentName: AWS-StartEC2Instance
      TargetParameterName: InstanceId
      Targets:
        - Key: 'tag:StartTime'
          Values:
            - '{{ StartTime }}'

DOC
}

ここは割とそのまんまで、Targets:部分を追加しているくらいです。

やってみて

実装してみた結果として、以下あたり挙げられます。

  1. インスタンスにタグをつけるだけで起動・停止してくれるのは素直に便利
  2. 管理サーバが潰せるからEC2 x 1台分のコスト削減
    絶賛、複数アカウントでEC2起動・停止をこの仕組みに切り替え、管理サーバを停止しようと頑張ってます。
  3. 今まではサーバにSSHでログインして、cronの設定見て、cronを変更して...だったのが、起動・停止したいインスタンスにタグをつけるだけになったことでマネコン触れる人ならば設定できるという点で、サーバの起動時間制限してねのハードルを下げることができた
  4. 当たり前ですが、Terraform化しておいたことで、マルチアカウント環境でも色々なAWSアカウントにこの仕組みをばら撒くのが簡単
  5. タグで制御することによってこんな感じでマネコンからどのインスタンスが何時に起動・停止されるかが見やすくなった

    今まで、担当者から「このサーバって何時に起動してるんでしたっけー?」と聞かれるたびにサーバのcronを確認しにいっていた状況から、「マネコン見といて」で済むようになったのは非常に便利です。

Terraformの実装で地味に手こずる部分ありましたが、実装してしまえばメリットしかないなーです!

まとめ

タグベースでEC2インスタンスの起動・停止をサーバレスでできるいい感じの仕組みをTerraformで実装できました。
ながーく使っている仕組みを多少は新しい仕組みに置き換える活動はシンプルに楽しいですし、円安が進みまくる昨今ではちりつも方式でもコスト最適化を進めていくことはとても大事と思っているので、こういった改善活動は続けていきたいなーと思っています!

JINSテックブログ

Discussion