Fusic Tech Blog

Fusion of Society, IT and Culture

TerraformでDLMとCloudWatchEventを作成する
2019/10/16

TerraformでDLMとCloudWatchEventを作成する

こんばんは、嶋生(しまお)です。先日作成した掲題のTerraformを共有させていただきたいと思います。

やったこと

  • TerraformでAmazon Data Lifecycle ManagerとAmazon CloudWatch Eventsを作成しました。
  • DLMでEBSのデイリーバックアップ、CloudWatchEventでEC2の定時起動/停止をやっています。

    • 決まった時間しか使用しない業務システムやテスト環境等で使えるかもしれません。

コード

以下、コードを掲載し簡単に解説させていただきます。

VPC, EC2等

※DLMと関連するため記載しますが、こちらについてはよくある内容だと思いますのでさらっと読む程度で良いかと思います。

# VPC
resource "aws_vpc" "test_vpc" {
  cidr_block           = "10.1.0.0/16"
  instance_tenancy     = "default"
  enable_dns_support   = "true"
  enable_dns_hostnames = "true"

  tags = {
    Name = "test_vpc"
  }
}

# Internet Gateway
resource "aws_internet_gateway" "test_igw" {
  vpc_id = "${aws_vpc.test_vpc.id}"

  tags = {
    Name = "test_igw"
  }
}

# Subnet
resource "aws_subnet" "test_subnet_public" {
  vpc_id                  = "${aws_vpc.test_vpc.id}"
  cidr_block              = "10.1.0.0/24"
  availability_zone       = "${var.availability_zone}"
  map_public_ip_on_launch = "true"

  tags = {
    Name = "test_subnet_public"
  }
}

# Route Table
resource "aws_route_table" "test_route_public" {
  vpc_id = "${aws_vpc.test_vpc.id}"

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = "${aws_internet_gateway.test_igw.id}"
  }

  tags = {
    Name = "testroute-table-public"
  }
}

resource "aws_route_table_association" "test_assoc" {
  subnet_id      = "${aws_subnet.test_subnet_public.id}"
  route_table_id = "${aws_route_table.test_route_public.id}"
}

# Security Group
### Web
resource "aws_security_group" "test_web_sg" {
  name        = "test_web_sg"
  description = "Allow SSH inbound traffic"
  vpc_id      = "${aws_vpc.test_vpc.id}"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = "${var.my_ips}"
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = "${concat(var.my_ips, var.customer_ips)}"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "test_web_sg"
  }
}

# Key Pair
resource "aws_key_pair" "test_key" {
  key_name   = "test_key"
  public_key = "${var.aws_public_key}"
}

# EC2 Instance
resource "aws_instance" "test_instance" {
  ami           = "${var.instance_ami}"
  instance_type = "${var.instance_type}"

  volume_tags = {
    Name     = "test_ebs"
    Snapshot = "true" # for dlm
  }

  vpc_security_group_ids = [
    "${aws_security_group.test_web_sg.id}",
  ]

  subnet_id                   = "${aws_subnet.test_subnet_public.id}"
  associate_public_ip_address = true

  root_block_device {
    volume_type = "gp2"
    volume_size = "${var.gp2_volume_size}"
  }

  tags = {
    Name = "test_instance"
  }

  key_name   = "${aws_key_pair.test_key.key_name}"
  monitoring = true
}

# EIP
resource "aws_eip" "test_eip" {
  instance = "${aws_instance.test_instance.id}"
  vpc      = true

  tags = {
    Name = "test_eip"
  }
}
  • ポイントはEC2の所でvolume_tagsに Snapshot = "true" を指定している点です。これを後述するDLMの対象EBSの条件にしています。

DLM

data "aws_iam_policy_document" "test_dlm_lifecycle_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["dlm.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "test_dlm_lifecycle_role" {
  name               = "TestDlmLifecycleRole"
  assume_role_policy = "${data.aws_iam_policy_document.test_dlm_lifecycle_role.json}"
}

data "aws_iam_policy_document" "test_dlm_lifecycle" {
  statement {
    effect    = "Allow"
    actions   = ["ec2:CreateSnapshot", "ec2:DeleteSnapshot", "ec2:DescribeVolumes", "ec2:DescribeSnapshots"]
    resources = ["*"]
  }

  statement {
    effect    = "Allow"
    actions   = ["ec2:CreateTags"]
    resources = ["arn:aws:ec2:*::snapshot/*"]
  }
}

resource "aws_iam_role_policy" "test_dlm_lifecycle" {
  name   = "test-dlm-lifecycle-policy"
  role   = "${aws_iam_role.test_dlm_lifecycle_role.id}"
  policy = "${data.aws_iam_policy_document.test_dlm_lifecycle.json}"
}

resource "aws_dlm_lifecycle_policy" "test_dlm_lifecycle_policy" {
  description        = "DLM lifecycle policy"
  execution_role_arn = "${aws_iam_role.test_dlm_lifecycle_role.arn}"
  state              = "ENABLED"

  policy_details {
    resource_types = ["VOLUME"]

    schedule {
      name = "daily snapshots in last week"

      create_rule {
        interval      = 24
        interval_unit = "HOURS"
        times         = ["18:00"] # UTC(JSTの3-4時を想定)
      }

      retain_rule {
        count = 7 # 保持するスナップショットの最大数
      }

      tags_to_add = {
        SnapshotCreator = "DLM"
      }

      copy_tags = true
    }

    target_tags = {
      Snapshot = "true"
    }
  }
}
  • 24時間間隔で Snapshot = "true" に該当するEBSのスナップショットを取るようにしています。

    • 一応、スナップショットを作成時刻はJSTでAM3時にしています。
    • ちなみにAWSコンソールにて作成すると スナップショットは、指定された開始時刻から 1 時間以内に作成が開始されます。 という注意書きがあります。実際、timesの指定時刻から30分程度経って作成されることが多い印象です。
  • retain_ruleにて保持するスナップショットを最大7つにしています。これにより直近一週間のバックアップとしています。

参考

Cloud Watch Event

# SSM Automation用のIAM Role
data "aws_iam_policy_document" "test_ssm_automation_trust" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ssm.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "test_ssm_automation" {
  name               = "TestSSMautomation"
  assume_role_policy = "${data.aws_iam_policy_document.test_ssm_automation_trust.json}"
}

# SSM Automation用のIAM RoleにPolicy付与
resource "aws_iam_role_policy_attachment" "ssm-automation-atach-policy" {
  role       = "${aws_iam_role.test_ssm_automation.id}"
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonSSMAutomationRole"
}

# CloudWatchイベント用のIAM Role
data "aws_iam_policy_document" "event_invoke_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["events.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "event_invoke_assume_role" {
  name               = "testCloudWatchEventRole"
  assume_role_policy = "${data.aws_iam_policy_document.event_invoke_assume_role.json}"
}


# CloudWatchイベント用のIAM RoleにPolicy付与
data "aws_caller_identity" "self" {}

data "aws_iam_policy_document" "event_invoke_policy" {
  statement {
    effect  = "Allow"
    actions = ["ssm:StartAutomationExecution"]
    resources = [
      "arn:aws:ssm:${var.region}:${data.aws_caller_identity.self.account_id}:automation-definition/AWS-StartEC2Instance:*",
      "arn:aws:ssm:${var.region}:${data.aws_caller_identity.self.account_id}:automation-definition/AWS-StopEC2Instance:*",
    ]
  }
  statement {
    effect    = "Allow"
    actions   = ["iam:PassRole"]
    resources = ["${aws_iam_role.test_ssm_automation.arn}"]

    condition {
      test     = "StringLikeIfExists"
      variable = "iam:PassedToService"
      values   = ["ssm.amazonaws.com"]
    }
  }
}

resource "aws_iam_role_policy" "event_invoke_policy" {
  name   = "testCloudWatchEventPolicy"
  role   = "${aws_iam_role.event_invoke_assume_role.id}"
  policy = "${data.aws_iam_policy_document.event_invoke_policy.json}"
}

# CloudWatchイベント - EC2の定時起動
resource "aws_cloudwatch_event_rule" "start_test_ec2_rule" {
  name                = "StartInstanceRule"
  description         = "Start instances after batch execution."
  schedule_expression = "cron(0 22 * * ? *)"
}

resource "aws_cloudwatch_event_target" "start_test_instance" {
  target_id = "StartInstanceTarget"
  arn       = "arn:aws:ssm:${var.region}:${data.aws_caller_identity.self.account_id}:automation-definition/AWS-StartEC2Instance"
  rule      = "${aws_cloudwatch_event_rule.start_test_ec2_rule.name}"
  role_arn  = "${aws_iam_role.event_invoke_assume_role.arn}"

  input = <<DOC
{
  "InstanceId": ["${aws_instance.test_instance.id}"],
  "AutomationAssumeRole": ["${aws_iam_role.test_ssm_automation.arn}"]
}
DOC
}

# CloudWatchイベント - EC2の定時停止
resource "aws_cloudwatch_event_rule" "stop_test_ec2_rule" {
  name                = "StopInstanceRule"
  description         = "Stop instances after batch execution."
  schedule_expression = "cron(0 10 * * ? *)"
}

resource "aws_cloudwatch_event_target" "stop-test-instance" {
  target_id = "StopInstanceTarget"
  arn       = "arn:aws:ssm:${var.region}:${data.aws_caller_identity.self.account_id}:automation-definition/AWS-StopEC2Instance"
  rule      = "${aws_cloudwatch_event_rule.stop_test_ec2_rule.name}"
  role_arn  = "${aws_iam_role.event_invoke_assume_role.arn}"

  input = <<DOC
{
  "InstanceId": ["${aws_instance.test_instance.id}"],
  "AutomationAssumeRole": ["${aws_iam_role.test_ssm_automation.arn}"]
}
DOC
}
  • AM7時にEC2を起動し、PM7時に停止するにしています。

    • cron式で記述しています。crontabの場合と異なり、年も指定できるため、6フィールドで指定します。
    • AWS公式に日付と曜日フィールドの両方で * を指定できないと記述があり、曜日の方のワイルドカードを ? としています。(理由はよくわからない…)
  • IAMロール周りが少し複雑ですが、Cloud Watch Eventが起動 -> Systems Manager AutomationがEC2を起動/停止 という処理の流れを反映してこのような権限の継承の形になっていると理解しています。

参考

一応、main.tf

provider "aws" {
  access_key = "${var.aws_access_key}"
  secret_key = "${var.aws_secret_key}"
  region     = "${var.region}"
}

一応、variables.tfも

variable "aws_access_key" {}
variable "aws_secret_key" {}
variable "aws_public_key" {}

variable "my_ips" {
  default = [
  ]
}

variable "customer_ips" {
  default = [
  ]
}

variable "region" {
  default = "us-west-2"
}

variable "availability_zone" {
  default = "us-west-2b"
}

variable "instance_ami" {
  default = ""
}

variable "instance_type" {
  default = ""
}

variable "gp2_volume_size" {
  default = ""
}

その他補足

  • (当然ではありますが)terraform destroyしても作成済のスナップショットは削除されませんので、そちらは必要に応じて削除する必要があります。
  • Terraformのバージョンが 0.12.8だとdestroyしたときに、CloudWatchEventの削除ができませんでした(おそらくこちらのissueかと思います)。バージョンを0.12.10に上げることで解消しました。

感想など

  • これまではLambdaやcronでやっていたようなことがマネージドサービスで出来るので楽だなと思います。
  • Terraformはコマンドがシンプルで大好きなのですが、一方で公式Docsを見ながらゼロから書く作業はあまり好きではないです(特に誰が書いても同じようなものの場合。生産的じゃない感を感じてしまいます)。なので、ニッチなもので需要はないかもしれないなぁと思いつつも、できるだけ共有しようと思っています。

以上です。最後まで読んでくださり、ありがとうございます。

shimao

shimao

福岡在住です。Perl→PHP→Python、ここ1年ほど機械学習をやっています。福岡でKaggleもくもく会を主催しているので、興味がある方は是非ご参加ください。