Step Functionsを使ったECSコンテナの制御

# はじめに
この記事では、AWS Step Functionsを使用してECSコンテナを開始し、10秒後に自動的に停止させるプロセスを解説します。このプロジェクトには、IAM、ECR、CloudTrail、EventBridge、Step Functions、Lambdaなどの主要なAWSサービスを利用します。

作成したレポジトリーはこちらです。

https://github.com/sugiyama404/practice_step_function

こちらの記事を参考にしました。

https://dev.classmethod.jp/articles/step-functions-workflow-studio-create-workflow-to-stop-ec2/

# 目的
このプロジェクトの目的は、AWS Step Functionsを用いて、ECSコンテナのライフサイクルを効率的に管理し、自動化することです。特に、コンテナを起動してから10秒後に自動で停止させることで、リソースの効率的な使用と運用の簡素化を図ります。


# 機能一覧

+ **ECSコンテナの開始と停止**: Step Functionsを使用してECSコンテナを開始し、10秒後に自動的に停止させます。
+ **インフラ管理**: IAM、ECR、CloudTrail、EventBridge、Step Functions、Lambdaを利用したインフラ構成。
+ **ログとトラッキング**: CloudTrailを使用して操作の監査ログを取得し、EventBridgeでイベントをトリガーします。

# インフラ構成

![aws.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/f64ebd2a-2c8b-4e48-03ae-47a1e1a8cc22.png)

+ **IAM**: IAMポリシーとロールを設定し、Step FunctionsとLambdaが必要な権限を持つようにします。
+ **ECR**: ECSで使用するコンテナイメージを格納するためのリポジトリを設定します。
+ **ECS**: コンテナの実行環境としてECSクラスターを設定します。
+ **CloudTrail**: すべてのAPIコールを監査し、セキュリティとコンプライアンスを強化します。
+ **EventBridge**: 特定のイベントをトリガーし、Step Functionsを起動します。
+ **Step Function**s: ECSコンテナの開始と停止を管理するステートマシンを設定します。
+ **Lambda**: Step Functionsのタスクとして使用され、コンテナの停止を実行します。

# 実装方法

この記事では、AWS Step Functionsを使用してECSコンテナを開始し、10秒後に停止させる自動化プロセスの実装方法について詳しく説明します。

# iam

以下に、必要なIAMロールおよびポリシーのTerraformコードを示します。

```terraform:infra/modules/iam/aws_iam_role.tf
resource "aws_iam_role" "ecs_role" {
  name = "ecs_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid : ""
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
      },
    ]
  })

  tags = {
    Name = "${var.app_name}-ecs-iam-role"
  }
}

resource "aws_iam_role" "lambda_role" {
  name = "lambda_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action = "sts:AssumeRole",
        Effect = "Allow",
        Principal = {
          Service = "lambda.amazonaws.com",
        },
      },
    ],
  })

  tags = {
    Name = "${var.app_name}-lambda-iam-role"
  }
}

resource "aws_iam_role" "cloudtrail_role" {
  name = "CloudTrail_CloudWatchLogs_Role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "cloudtrail.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

# IAM Role for Step Functions
resource "aws_iam_role" "step_functions_role" {
  name = "stepFunctionsRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "states.amazonaws.com"
        }
      },
    ]
  })
}

# IAM Role for EventBridge to Step Functions
resource "aws_iam_role" "eventbridge_role" {
  name = "EventBridgeToStepFunctionsRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "events.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}
```


```terraform:infra/modules/iam/aws_iam_policy.tf
resource "aws_iam_policy" "ecr_policy" {
  name = "${var.app_name}-ecr-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "ecr:*"
        ]
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}

resource "aws_iam_policy" "cloudwatch_policy" {
  name = "${var.app_name}-cloudwatch-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "logs:*"
        ]
        Effect   = "Allow"
        Resource = "arn:aws:logs:*:*:*"
      },
    ]
  })
}

resource "aws_iam_policy" "cloudtrail_policy" {
  name = "CloudTrail_CloudWatchLogs_Policy"

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ],
        Resource = "arn:aws:logs:*:*:log-group:${var.webserver_log_group_name}:log-stream:*"
      }
    ]
  })
}

resource "aws_iam_policy" "lambda_policy" {
  name = "lambda_policy"

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents",
          "ecs:StopTask",
        ],
        Resource = "*",
        Effect   = "Allow"
      }
    ]
  })
}

resource "aws_iam_policy" "lambda_invoke_policy" {
  name        = "lambda-invoke-policy"
  description = "Policy to allow stepFunctionsRole to invoke Lambda functions"
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect   = "Allow",
        Action   = "lambda:InvokeFunction",
        Resource = "arn:aws:lambda:${var.region}:${local.account_id}:function:lambda_function"
      }
    ]
  })
}

resource "aws_iam_policy" "eventbridge_to_stepfunctions_policy" {
  name = "EventBridgeToStepFunctionsPolicy"

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "states:StartExecution"
        ],
        Resource = "${var.sfn_ecs_stop_state_machine_arn}"
      }
    ]
  })
}
```


```terraform:infra/modules/iam/aws_iam_policy_attachment.tf
resource "aws_iam_policy_attachment" "ecr_attach" {
  name       = "${var.app_name}-ecr-attach"
  roles      = ["${aws_iam_role.ecs_role.name}"]
  policy_arn = aws_iam_policy.ecr_policy.arn
}

resource "aws_iam_policy_attachment" "cloudwatch_attach" {
  name       = "${var.app_name}-cloudwatch-attach"
  roles      = ["${aws_iam_role.ecs_role.name}"]
  policy_arn = aws_iam_policy.cloudwatch_policy.arn
}

resource "aws_iam_policy_attachment" "lambda_role_attachment" {
  name       = "${var.app_name}-lambda-attach"
  roles      = ["${aws_iam_role.lambda_role.name}"]
  policy_arn = aws_iam_policy.lambda_policy.arn
}

resource "aws_iam_policy_attachment" "cloudtrail_role_attachment" {
  name       = "${var.app_name}-cloudtrail-attach"
  roles      = ["${aws_iam_role.cloudtrail_role.name}"]
  policy_arn = aws_iam_policy.cloudtrail_policy.arn
}

resource "aws_iam_role_policy_attachment" "step_functions_policy_attachment" {
  role       = aws_iam_role.step_functions_role.name
  policy_arn = "arn:aws:iam::aws:policy/AWSStepFunctionsFullAccess"
}

resource "aws_iam_role_policy_attachment" "step_functions_to_lambda_policy_attachment" {
  role       = aws_iam_role.step_functions_role.name
  policy_arn = aws_iam_policy.lambda_invoke_policy.arn
}

resource "aws_iam_role_policy_attachment" "eventbridge_to_stepfunctions_policy_attachment" {
  role       = aws_iam_role.eventbridge_role.name
  policy_arn = aws_iam_policy.eventbridge_to_stepfunctions_policy.arn
}
```

これらのIAMロールおよびポリシーは、ECSタスク、Lambda関数、CloudTrail、Step Functions、およびEventBridgeが必要な権限を持つように設定されています。次のステップでは、ECRリポジトリの設定、ECSタスク定義、Step Functionsステートマシン、EventBridgeルール、およびLambda関数を設定します。

# CloudTrail

AWS CloudTrailは、AWSアカウントのAPIコールやその他のイベントを記録し、ログファイルをAmazon S3バケットに保存します。これにより、AWSリソースの操作を監査し、コンプライアンスを維持することができます。

```terraform:infra/modules/cloudtrail/aws_cloudtrail.tf
resource "aws_cloudtrail" "main" {
  name                          = "my-cloudtrail"
  s3_bucket_name                = var.s3_bucket_id
  include_global_service_events = true
  s3_key_prefix                 = "prefix"
  enable_logging                = true

  cloud_watch_logs_group_arn = "${var.ecs_cloudwatch_logs_group_arn}:*"
  cloud_watch_logs_role_arn  = var.iam_role_arn_for_cloudtrail

}
```

CloudTrailが適切に設定され、ECSコンテナのイベントを監査し、CloudWatchログに記録する準備が整いました。詳細な設定手順については、各サービスの公式ドキュメントを参照してください。


# EventBridge

Amazon EventBridgeは、異なるAWSサービス間でイベントをルーティングするためのイベントバスサービスです。ここでは、EventBridgeを使用してECSタスクの状態変化を監視し、Step Functionsステートマシンをトリガーする方法について説明します。

## EventBridgeルールの設定

ECSタスクの状態が「RUNNING」に変わった時にStep Functionsをトリガーするルールを作成します。

```terraform:infra/modules/eventbridge/aws_cloudwatch_event_rule.tf
resource "aws_cloudwatch_event_rule" "ecs_task_start_rule" {
  name        = "ecsTaskStartRule"
  description = "Rule to trigger Step Functions on ECS task start"
  event_pattern = jsonencode({
    source      = ["aws.ecs"]
    detail-type = ["ECS Task State Change"]
    detail = {
      lastStatus = ["RUNNING"]
    }
  })
}
```

## EventBridgeターゲットの設定
次に、上記のルールにマッチしたイベントが発生した時に、Step Functionsステートマシンをトリガーするターゲットを設定します。


```terraform:infra/modules/eventbridge/aws_cloudwatch_event_target.tf
# EventBridge Target
resource "aws_cloudwatch_event_target" "ecs_task_start_target" {
  rule      = aws_cloudwatch_event_rule.ecs_task_start_rule.name
  target_id = "ecsTaskStartTarget"
  arn       = var.sfn_ecs_stop_state_machine_arn
  role_arn  = var.iam_role_arn_for_eventbrifge
}
```



# step functiion

AWS Step Functionsは、複数のAWSサービスを連携させ、ビジネスプロセスを自動化するためのワークフローを構築するためのサービスです。ここでは、Step Functionsを使用してECSタスクを停止するためのステートマシンを設定する方法について説明します。

## Step Functionsステートマシンの作成

ECSタスクを停止するためのステートマシンを作成します。このステートマシンは、10秒の待機ステートの後にLambda関数を呼び出してECSタスクを停止します。


```terraform:infra/modules/stepfunction/aws_sfn_state_machine.tf
# Step Functions State Machine
resource "aws_sfn_state_machine" "ecs_stop_state_machine" {
  name     = "ecsStopStateMachine"
  role_arn = var.iam_role_arn_for_stepfunction

  definition = <<DEFINITION
{
  "Comment": "State machine to stop ECS task",
  "StartAt": "Wait",
  "States": {
    "Wait": {
      "Type": "Wait",
      "Seconds": 10,
      "Comment": "10s",
      "Next": "StopTask"
    },
    "StopTask": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": {
        "FunctionName": "${var.lambda_function_arn}",
        "Payload": {
          "Cluster": "${var.ecs_cluster_arn}",
          "Task": "$.TaskArn"
        }
      },
      "End": true
    }
  }
}
DEFINITION
}
```

これで、ECSタスクを停止するためのStep Functionsステートマシンの設定が完了しました。

## Lambda

AWS Lambdaは、コードをサーバーレスで実行するためのサービスです。ここでは、AWS Lambda関数を作成し、ECSタスクを停止するための設定方法について説明します。

以下に、必要なTerraformコードとLambda関数のPythonコードを示します。

```terraform:infra/modules/lambda/aws_lambda_function.tf
data "archive_file" "lambda" {
  type        = "zip"
  source_dir  = "./modules/lambda/src/in"
  output_path = "./modules/lambda/src/out/lambda_function_payload.zip"
}

resource "aws_lambda_function" "main" {
  filename         = "./modules/lambda/src/out/lambda_function_payload.zip"
  function_name    = var.lambda_function_name
  description      = "lambda_function"
  role             = var.iam_role_arn_for_lambda
  architectures    = ["x86_64"]
  handler          = "index.lambda_handler"
  source_code_hash = data.archive_file.lambda.output_base64sha256
  timeout          = 30
  runtime          = "python3.9"
  depends_on       = [aws_cloudwatch_log_group.lambda]

  environment {
    variables = {
      CLUSTER_ARN  = var.ecs_cluster_arn
      CLUSTER_NAME = var.ecs_cluster_name
      TASK_ARN     = var.ecs_task_arn
    }
  }

  tags = {
    Name = "${var.app_name}-lamdba"
  }
}
```

ECSタスクを停止するためのLambda関数のコードです。

```python:infra/modules/lambda/src/in/index.py
import boto3
import os

def lambda_handler(event, context):
    ecs = boto3.client('ecs')

    cluster_arn = os.environ['CLUSTER_ARN']
    cluster_name = os.environ['CLUSTER_NAME']
    task_definition_arn = os.environ['TASK_ARN']

    try:
        # ECSタスクの一覧を取得
        response = ecs.list_tasks(cluster=cluster_name, taskDefinition=task_definition_arn)
        task_arns = response['taskArns']

        # タスクを停止
        for task_arn in task_arns:
            ecs.stop_task(
                cluster=cluster_arn,
                task=task_arn,
                reason='Stopped by Lambda function after 10 seconds'
            )

        return {
            'statusCode': 200,
            'body': {
                'message': 'Tasks stopped successfully',
                'task_arns': task_arns
            }
        }

    except Exception as e:
        return {
            'statusCode': 500,
            'body': {
                'message': str(e)
            }
        }
```

EventBridgeがLambda関数を呼び出すための権限を設定します。

```terraform:infra/modules/lambda/aws_lambda_permission.tf
# Permissions for EventBridge to invoke Step Functions
resource "aws_lambda_permission" "allow_eventbridge_invoke" {
  statement_id  = "AllowExecutionFromEventBridge"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.main.function_name
  principal     = "events.amazonaws.com"
  source_arn    = var.cloudwatch_ecs_task_start_rule_arn
}
```

Lambda関数のログをCloudWatchに記録するためのロググループを作成します。

```terraform:infra/modules/lambda/aws_cloudwatch_log_group.tf
resource "aws_cloudwatch_log_group" "lambda" {
  name = "/aws/lambda/${var.lambda_function_name}"
}
```

これで、ECSタスクを停止するためのLambda関数の設定が完了しました。

# 結果

step functionsの画面で以下の画面が作られました。

![Screenshot 2024-07-29 at 15.01.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/820d0662-0962-5fe4-de92-872db87fcf10.png)

実行結果です。

![Screenshot 2024-07-29 at 15.02.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/04df62c8-6325-23df-e555-4f52ffe43af9.png)



# まとめ
この記事では、AWS Step Functionsを利用してECSコンテナを開始し、10秒後に停止させる方法を紹介しました。このインフラ構成を通じて、リソースの効率的な管理と運用の自動化が可能になります。詳細な設定手順については、各サービスの公式ドキュメントを参照してください。