先輩がノリで作ったAWSをちゃんとコード化してみた3 (EventBridge Schedulerのコード化)

## コード化中
1. [AWS CDKの導入](https://qiita.com/emi_yoshi/items/d775cd80e44095d468f1)
1. [CodePipelineのコード化](https://qiita.com/emi_yoshi/items/9e178c975cece3a5ad1b)
1. EventBridge Schedulerのコード化(この記事)
1. [ECS clusterとserviceのコード化](https://qiita.com/emi_yoshi/items/62311276cb84ca7e4db6)
1. [ElastiCacheのコード化、本番環境構築](https://qiita.com/emi_yoshi/items/93913cca44658067cd98)

## ECS task definistion
EventBridge Schedulerの中でタスク定義を使用するので合わせてコード化します
特筆することは特に無いです
先輩が作成した環境と同じになるように実装しました
```Kotlin
var taskExecutionRole: Role = Role.Builder
    .create(this, "developmentMyappBatchCdkTaskExecutionRole")
    .roleName("developmentMyappBatchCdkTaskExecutionRole")
    .assumedBy(ServicePrincipal.Builder
        .create("ecs-tasks.amazonaws.com")
        .build()
    )
    .managedPolicies(listOf(
        ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy")
    ))
    .build()

var taskDefinition: TaskDefinition = TaskDefinition.Builder
    .create(this, "developmentMyappBatchCdkEcsTaskDefinition")
    .family("developmentMyappBatchCdkEcsTaskDefinition")
    .compatibility(Compatibility.FARGATE)
    .cpu("1024")
    .memoryMiB("2048")
    .executionRole(taskExecutionRole)
    .taskRole(taskExecutionRole)
    .runtimePlatform(RuntimePlatform.builder()
        .operatingSystemFamily(OperatingSystemFamily.LINUX)
        .cpuArchitecture(CpuArchitecture.X86_64)
        .build())
    .build()

val logGroup: LogGroup = LogGroup.Builder
    .create(this, "developmentMyappBatchCdkEcsContainerLogGroup")
    .logGroupName("/ecs/" + "developmentMyappBatchCdkEcsContainer")
    .retention(RetentionDays.INFINITE)
    .build()

val ecsEnvironment = context["ecsEnvironment"] as Map<String, String>
val dbSettings = context["dbSettings"] as Map<String, String>

taskDefinition.addContainer(
    "developmentMyappBatchCdkTaskContainerDefinition",
    ContainerDefinitionOptions.builder()
        .containerName("batch")
        .image(ContainerImage.fromEcrRepository(ecrRepository))
        .cpu(1024)
        .memoryLimitMiB(2048)
        .essential(true)
        .environment(ecsEnvironment + dbSettings)
        .logging(LogDriver.awsLogs(AwsLogDriverProps.builder()
            .logGroup(logGroup)
            .streamPrefix("ecs")
            .build()))
        .build()
)
```


## EventBridge Scheduler
EventBridge SchedulerはL2のライブラリが無いのでL1のCfnScheduleを使用します
EventBridge Schedulerの設定については先輩の記事をどうぞ


https://qiita.com/yuya_sega/items/f1c1a1196de334c107fb

複数のバッチを定義したいのでListにしてループさせています

```Kotlin
val awsAccountId = this.getNode().tryGetContext("awsAccountId").toString()
val batchTaskDefinitionArn = ecsTaskDefinition.getTaskDefinitionArn()
val batchSecurityGroup = context["batchSecurityGroup"].toString()
val batchSubnet = context["batchSubnet"].toString()

// バッチを定義
// listOf("batch name", "cron式", "ENABLED or DISABLED", "command")
val scheduleArray = listOf(
    listOf(target + "MyappBatch1", "cron(0 0 * * ? *)", "ENABLED", "--batch.execute=batch1"),
    listOf(target + "MyappBatch2", "cron(0 0 * * ? *)", "ENABLED", "--batch.execute=batch2")
)

val group: CfnScheduleGroup = CfnScheduleGroup.Builder
    .create(this, target + "MyappBatchCdkScheduleGroup")
    .name(target + "MyappBatchCdkScheduleGroup")
    .build()

val runTaskPolicy: PolicyDocument = PolicyDocument.Builder
    .create()
    .statements(listOf(
        PolicyStatement.Builder
            .create()
            .actions(listOf("ecs:RunTask"))
            .resources(listOf(batchTaskDefinitionArn))
            .conditions(mapOf(
                "ArnLike" to mapOf("ecs:cluster" to ecsClusterArn)
            ))
            .build()
    ))
    .build()
val passRolePolicy: PolicyDocument = PolicyDocument.Builder
    .create()
    .statements(listOf(
        PolicyStatement.Builder
            .create()
            .actions(listOf("iam:PassRole"))
            .resources(listOf(ecsTaskExecutionRole.getRoleArn()))
            .build()
    ))
    .build()
val role: Role = Role.Builder
    .create(this, target + "MyappBatchCdkSchedulerRole")
    .roleName(target + "MyappBatchCdkSchedulerRole")
    .assumedBy(ServicePrincipal.Builder
        .create("scheduler.amazonaws.com")
        .conditions(mapOf(
            "StringEquals" to mapOf("aws:SourceAccount" to awsAccountId)
        ))
        .build()
    )
    .managedPolicies(listOf(
        ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy")
    ))
    .inlinePolicies(mapOf(
        "RunTaskPolicy" to runTaskPolicy,
        "PassRolePolicy" to passRolePolicy
    ))
    .build()

scheduleArray.forEach {
    val targetProperty: TargetProperty = TargetProperty.builder()
        .arn(ecsClusterArn)
        .ecsParameters(EcsParametersProperty.builder()
            .taskDefinitionArn(batchTaskDefinitionArn)
            .taskCount(1)
            .launchType("FARGATE")
            .networkConfiguration(NetworkConfigurationProperty.builder()
                .awsvpcConfiguration(AwsVpcConfigurationProperty.builder()
                    .subnets(listOf(batchSubnet))
                    .securityGroups(listOf(batchSecurityGroup))
                    .assignPublicIp("ENABLED")
                    .build())
                .build())
            .platformVersion("LATEST")
            .enableEcsManagedTags(true)
            .enableExecuteCommand(true)
            .build())
        .input("""{
"containerOverrides": [
{
    "name": "batch",
    "command": ["${it[3]}"]
}
]
}""")
        .roleArn(role.getRoleArn())
        .retryPolicy(RetryPolicyProperty.builder()
            .maximumEventAgeInSeconds(3600) // 1h = 60 * 60
            .maximumRetryAttempts(2)
            .build())
        .build()

    CfnSchedule.Builder
        .create(this, it[0])
        .flexibleTimeWindow(FlexibleTimeWindowProperty.builder()
            .mode("OFF")
            .build())
        .name(it[0])
        .groupName(group.getName())
        .scheduleExpression(it[1])
        .scheduleExpressionTimezone("Asia/Tokyo")
        .state(it[2])
        .target(targetProperty)
        .build()
}
```

## ファイル分割
これを全部 `BatchStack.kt`　にまとめてしまうのは読みにくいのでファイル分割を行いました

```Kotlin:BatchStack.kt
class BatchStack(
    parent: Construct,
    name: String,
    stackProps: StackProps,
    ecsClusterArn: String
): Stack(parent, name, stackProps) {
    init {
        val context = this.getNode().tryGetContext("development") as Map<String, Any>

        val pipeline = Pipeline(this, context)
        pipeline.createPipeline()

        val ecs = Ecs(this, context)
        ecs.createTaskDefinition(pipeline.ecrRepository)

        val eventBridge = EventBridge(this, context)
        eventBridge.createScheduler(
            ecs.taskDefinition,
            ecs.taskExecutionRole,
            ecsClusterArn)
    }
}
```

```Kotlin:recources/batch/Pipeline.kt
class Pipeline(val scope: Construct, val context: Map<String, Any>) {
    lateinit var ecrRepository: Repository

    fun createPipeline() {
        ...
    }
}
```

```Kotlin:resources/batch/Ecs.kt
class Ecs(val scope: Construct, val context: Map<String, Any>) {
    lateinit var taskDefinition: TaskDefinition
    lateinit var taskExecutionRole: Role

    fun createTaskDefinition(ecrRepository: Repository) {
        ...
    }
}
```

```Kotlin:resources/batch/EventBridge.kt
class EventBridge(val scope: Construct, val context: Map<String, Any>) {
    fun createScheduler(
        ecsTaskDefinition: TaskDefinition,
        ecsTaskExecutionRole: Role,
        ecsClusterArn: String
    ) {
        ...
    }
}
```

## 次回
ここまででバッチのリポジトリに関連するコード化が終了したので、次はDeployStage内のstackを増やしてAPIと管理画面のリポジトリに関するコード化を行います