🦔

TerraformでECSタスクを自動停止する

2022/12/17に公開

やること

時間を指定してECSタスクを自動停止します。
おなじやり方で自動起動もできます。

全体像

EventBridgeでLambdaを起動してECSサービスのdesired countを0にします。
Lambdaはコンテナイメージを使用します。

対応ディレクトリ

.
├── ecr.tf
├── event_bridge.tf
└── iam_roles.tf
└── modules
│   └── iam_role
│       ├── main.tf
│       └── variable.tf
├── lambda
│   ├── Dockerfile
│   ├── package.json
│   └── app.js

事前準備

Lambdaイメージ管理用のECR作成

./ecr.tf
resource "aws_ecr_repository" "lambda" {
  name = "stop-ecr-lambda"
}
Lambdaイメージの作成

Lambdaコード

今回はNode.jsを使います。

./lambda/app.js
'use strict';
const AWS = require('aws-sdk');

const desiredCount = 0;
const cluster = 'your-ecs-cluster-name';
const service = 'your-ecs-service-name';

exports.handler = async function (event, context) {
  const ecs = new AWS.ECS();
  return await ecs
    .updateService({
      desiredCount,
      cluster,
      service
    })
    .promise();
};

ビルドしてECRにpush

こちらご参照ください => Lambda コンテナイメージの作成

./lambda/Dockerfile
FROM public.ecr.aws/lambda/nodejs:14

# Assumes your function is named "app.js", and there is a package.json file in the app directory 
COPY app.js package.json  ${LAMBDA_TASK_ROOT}

# Install NPM dependencies for function
RUN npm install

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "app.handler" ] 
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 012345678910.dkr.ecr.ap-northeast-1.amazonaws.com

$ docker build -t stop-ecr-lambda .

$ docker tag stop-ecr-lambda:latest 012345678910.dkr.ecr.ap-northeast-1.amazonaws.com/stop-ecr-lambda:latest

$ docker push 012345678910.dkr.ecr.ap-northeast-1.amazonaws.com/stop-ecr-lambda:latest

LambdaにアタッチするIAMロールの定義

LambdaにECSタスク数を制御できる権限を与えます。

./iam_roles.tf
data "aws_iam_policy_document" "ecs_startstop" {
  statement {
    effect    = "Allow"
    resources = ["*"]
    actions = [
      "ecs:DescribeServices",
      "ecs:UpdateService"
    ]
  }
  statement {
    effect    = "Allow"
    resources = ["arn:aws:logs:*:*:*"]
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
  }

}
module "ecs_startstop_role" {
  source     = "./modules/iam_role"
  name       = "ecs_startstop"
  identifier = "lambda.amazonaws.com"
  policy     = data.aws_iam_policy_document.ecs_startstop.json
}
./modules/iam_role/main.tf
resource "aws_iam_role" "default" {
  name               = var.name
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = [var.identifier]
    }
  }
}

resource "aws_iam_policy" "default" {
  name   = var.name
  policy = var.policy
}

resource "aws_iam_role_policy_attachment" "default" {
  role       = aws_iam_role.default.name
  policy_arn = aws_iam_policy.default.arn
}

output "iam_role_arn" {
  value = aws_iam_role.default.arn
}

output "iam_role_name" {
  value = aws_iam_role.default.name
}

EventBridgeの作成

今回は平日の20:30(JST)にECSサービスを停止します。
cronの書き方はコンソールで操作してみるとわかりやすいです。
resource "aws_lambda_permission"を使ってEventBridgeにLambdaの起動を許可する必要があるのが注意点です。参照

event_bridge.tf
locals {
  stop_function_name  = "lambda-ecs-stop-container"
}

resource "aws_lambda_function" "ecs_stop" {
  function_name = local.stop_function_name
  role          = module.ecs_startstop_role.iam_role_arn
  package_type  = "Image"
  image_uri     = "${aws_ecr_repository.lambda.repository_url}:latest"
  timeout       = 60

  lifecycle {
    ignore_changes = [image_uri]
  }
  depends_on = [aws_cloudwatch_log_group.ecs_stop_lambda]
}

resource "aws_cloudwatch_log_group" "ecs_stop_lambda" {
  name              = "/aws/lambda/${local.stop_function_name}"
  retention_in_days = 30
}

resource "aws_cloudwatch_event_rule" "ecs_stop" {
  name                = "ecs-stop"
  description         = "stop ecs task"
  is_enabled          = "true"
  schedule_expression = "cron(30 11 ? * MON-FRI *)" # 月〜金 20:30(JST)に停止
}

resource "aws_cloudwatch_event_target" "ecs_stop" {
  arn       = aws_lambda_function.ecs_stop.arn
  rule      = aws_cloudwatch_event_rule.ecs_stop.name
  target_id = "ecs-stop"
}

resource "aws_lambda_permission" "allow_cloudwatch_to_call_lambda_stop" {
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.ecs_stop.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.ecs_stop.arn
}

Discussion