【コスト削減したい方必見】簡単かつ汎用的にAWSリソースを止める方法 ～AWS100本ノック～ 11/100

# はじめに

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/eb0b8e3b-f74f-1a52-fc28-d858ca8cc038.png)


<h4>みなさんコスト削減してますか？</h4>
AWSを構築する上で、出来るだけコスト削減したいっていうのはみなさんよく感じることだと思います。
開発環境でも意外とお金かかりますよね...(ぼそっ)

しかし、平日の夜間や土日は使わないのに、EC2やRDSをずっと動かしっぱなしの方も、結構いるんじゃないでしょうか？
そこで今回は`不要な時間に停止し、必要な時間に起動する複数のアカウントで使いまわせる方法`をご紹介します。

いきなり結論になりますが、最終形は以下になります。

![学ぶべきこと-不要な時間停止-最終形.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/013a7c2a-8b69-fb91-d63a-e5622251305a.png)


# 定期的に起動・停止をする方法

定期的にAWSリソースを起動したり・停止したりする方法としては以下が考えられます。

1. 決まった時間に担当者が`AWSマネジメントコンソール or AWS CLIで手動操作`する
2. AWSマネジメントコンソールでLambdaを作成。`Lambdaは定期実行し、AWS SDKで操作`する
3. AWSマネジメントコンソールでEventBridge Schedulerを作成。`EventBridge Schedulerは、AWS APIを定期実行`する
4. `AWS CDKで`EventBridge Schedulerを作成。EventBridge Schedulerは、AWS APIを定期実行する👈おすすめ

# それぞれの方法を解説

では、上記に挙げた4つの方法について説明していきます。

## 1. 決まった時間に担当者がAWSマネジメントコンソール or AWS CLIで手動操作する

これは愚直に担当者がマネジメントコンソールか、AWS CLIで操作する方法です。
今、起動したい/停止したいっていうケースではこの方法が一番良いのですが、定期実行の場合は現実的ではないですね...私ならやり忘れます... :cry: 

![学ぶべきこと-不要な時間停止-aws cli.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/98c32a99-2382-fdec-f751-572ccb68737c.png)

なんとか自動化したいところ... :thinking: 

## 2. AWSマネジメントコンソールでLambdaを作成。Lambdaは定期実行し、AWS SDKで操作する

次に考えられる方法としては、プログラムを定期実行して起動/停止する方法です。
後述の`EventBridge Scheduler`が登場するまでは、このやり方が主流だったと思います。

EventBridgeで定期実行設定を行い、Lambdaを実行する方法です。
Lambda関数には、AWS SDKを利用して対象のAWSリソースを操作して起動/停止するコードを実装します。

自動的に実行するようになって、だいぶ楽になりましたね:smile: 

![学ぶべきこと-不要な時間停止-lambda.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/b1a26a33-1451-80d0-a5ab-83b83f3fcd49.png)

ただまだ、Lambdaの実装という手間があるので、もっと簡単にしたいところ... :thinking: 

## 3. AWSマネジメントコンソールでEventBridge Schedulerを作成。EventBridge Schedulerは、AWS APIを定期実行する

次に考えられる方法としては、コードレスで定期実行して起動/停止する方法です。

EventBridge Schedulerの登場により、定期実行、および、API実行をこのサービスだけで行えるようになりました :smile: 
今の主流はこれだと思います。

Lambdaのようにコードを書かずに実現できて、めちゃめちゃ楽になりましたね！

![学ぶべきこと-不要な時間停止.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/f6f7f38f-ff97-2385-d437-afba6f6612f3.png)

ここまでいったら更に欲を出して、複数のAWSアカウントに簡単にEventBridge Schedulerを作れるようにしたいころ... :thinking: 

## 4. AWS CDKでEventBridge Schedulerを作成。EventBridge Schedulerは、AWS APIを定期実行する

では、最後に複数のアカウントに定期実行の仕組みをデプロイ出来るようにしましょう。
そのために、AWS CDKを利用します。

CDKの詳細は本記事では省略しますが、みなさん一度は聞いたことがあるであろう`IaC(Infrastructure as Code)`です！
TypeScriptやPythonなどみなさんが慣れ親しんだ言語で、インフラ構築をすることが出来るツールです！

![学ぶべきこと-不要な時間停止-最終形.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/013a7c2a-8b69-fb91-d63a-e5622251305a.png)

みなさんよく使うであろう、`EC2`、`RDS`、`Aurora`、`ECS`で定期起動/停止のサンプルコードを紹介します！
※CDKのインストールなどは事前に行ってください。
※aws-cdkの2.75で試したものになります

まず、cdk deployで実行されるファイルは以下になります。いろいろなスタックを作成する定義を記述しています。

:::note info
複数のAWSリソースのかたまりをCDK(CloudFormation)で定義したものを`スタック`といいます
:::

```typescript:bin/aws-cost-reduction-apps.ts
import * as cdk from 'aws-cdk-lib';
import { RdsClusterStatusChangeStack } from '../lib/rds-cluster-status-change-stack';
import { RdsInstanceStatusChangeStack } from '../lib/rds-instance-status-change-stack';
import { EcsFargateServiceDesiredCountChangeStack } from '../lib/ecs-fargate-service-desired-count-change-stack';
import { Ec2InstanceStatusChangeStack } from '../lib/ec2-instance-status-change-stack';

const app = new cdk.App();

const env = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: process.env.CDK_DEPLOY_REGION,
};

const ec2InstanceTargets: string = app.node.tryGetContext(
  'EC2_INSTANCE_TARGETS'
);
const rdsClusterTargets: string = app.node.tryGetContext('RDS_CLUSTER_TARGETS');
const rdsInstanceTargets: string = app.node.tryGetContext(
  'RDS_INSTANCE_TARGETS'
);
const ecsClusterTarget: string = app.node.tryGetContext('ECS_CLUSTER_TARGET');
const ecsServiceTargets: string = app.node.tryGetContext('ECS_SERVICE_TARGETS');
const ecsServiceDesiredCount: number = app.node.tryGetContext('DESIRED_COUNT');
const startCron: string = app.node.tryGetContext('START_CRON');
const stopCron: string = app.node.tryGetContext('STOP_CRON');
const timeZone: string = app.node.tryGetContext('TIME_ZONE');

// EC2 Instance Start Schedule
new Ec2InstanceStatusChangeStack(app, 'Ec2InstanceStatusChangeStartStack', {
  env,
  description: `ec2 instance start ${env.region} event schedule`,
  instanceIds: ec2InstanceTargets,
  action: 'start',
  cron: startCron,
  timezone: timeZone,
});

// EC2 Instance Stop Schedule
new Ec2InstanceStatusChangeStack(app, 'Ec2InstanceStatusChangeStopStack', {
  env,
  description: `ec2 instance stop ${env.region} event schedule`,
  instanceIds: ec2InstanceTargets,
  action: 'stop',
  cron: stopCron,
  timezone: timeZone,
});

// RDS Cluster Start Schedule
new RdsClusterStatusChangeStack(app, 'RdsClusterStatusChangeStartStack', {
  env,
  description: `rds cluster start ${env.region} event schedule`,
  clusterIds: rdsClusterTargets.replace(/\s+/g, '').split(','),
  action: 'start',
  cron: startCron,
  timezone: timeZone,
});

// RDS Cluster Stop Schedule
new RdsClusterStatusChangeStack(app, 'RdsClusterStatusChangeStopStack', {
  env,
  description: `rds cluster stop ${env.region} event schedule`,
  clusterIds: rdsClusterTargets.replace(/\s+/g, '').split(','),
  action: 'stop',
  cron: stopCron,
  timezone: timeZone,
});

// RDS Instance Start Schedule
new RdsInstanceStatusChangeStack(app, 'RdsInstanceStatusChangeStartStack', {
  env,
  description: `rds instance start ${env.region} event schedule`,
  instanceIds: rdsInstanceTargets.replace(/\s+/g, '').split(','),
  action: 'start',
  cron: startCron,
  timezone: timeZone,
});

// RDS Instance Stop Schedule
new RdsInstanceStatusChangeStack(app, 'RdsInstanceStatusChangeStopStack', {
  env,
  description: `rds instance stop ${env.region} event schedule`,
  instanceIds: rdsInstanceTargets.replace(/\s+/g, '').split(','),
  action: 'stop',
  cron: stopCron,
  timezone: timeZone,
});

// ECS Fargate Service Start Schedule
new EcsFargateServiceDesiredCountChangeStack(
  app,
  `EcsFargateServiceDesiredCountChange${ecsClusterTarget}StartStack`,
  {
    env,
    description: `ecs fargate service ${ecsClusterTarget} start ${env.region} event schedule`,
    clusterName: ecsClusterTarget,
    serviceNames: ecsServiceTargets.replace(/\s+/g, '').split(','),
    desiredCount: ecsServiceDesiredCount,
    action: 'start',
    cron: startCron,
    timezone: timeZone,
  }
);

// ECS Fargate Service Stop Schedule
new EcsFargateServiceDesiredCountChangeStack(
  app,
  `EcsFargateServiceDesiredCountChange${ecsClusterTarget}StopStack`,
  {
    env,
    description: `ecs fargate service ${ecsClusterTarget} stop ${env.region} event schedule`,
    clusterName: ecsClusterTarget,
    serviceNames: ecsServiceTargets.replace(/\s+/g, '').split(','),
    desiredCount: 0,
    action: 'stop',
    cron: stopCron,
    timezone: timeZone,
  }
);
```

### EC2を起動/停止

EC2を起動・停止するスタックは以下になります。
作成するAWSリソースは、
- `IAMロールとポリシー`：EventBridge SchedulerがEC2インスタンスを起動/停止する権限を付与したロール
- `EventBridge Schedule`：指定した時間に起動or停止を行うスケジュール
になります。
※RDS、Aurora、ECSに関しても作るAWSリソースは同じです
```typescript:lib/ec2-instance-status-change-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { pascalCase } from 'change-case';
import * as Iam from 'aws-cdk-lib/aws-iam';
import * as Scheduler from 'aws-cdk-lib/aws-scheduler';

export interface Ec2InstanceStatusChangeStackProps extends StackProps {
  readonly instanceIds: string;
  readonly action: string;
  readonly cron: string;
  readonly timezone: string;
}

export class Ec2InstanceStatusChangeStack extends Stack {
  constructor(
    scope: Construct,
    id: string,
    props: Ec2InstanceStatusChangeStackProps
  ) {
    super(scope, id, props);

    const eventScheduleRole = new Iam.Role(
      this,
      `${pascalCase(this.node.id)}CreateEventScheduleRole`,
      {
        roleName: `ec2-instance-${props.action}-event-schedule-${this.region}-role`,
        assumedBy: new Iam.ServicePrincipal('scheduler.amazonaws.com'),
      }
    );
    eventScheduleRole.addToPolicy(
      new Iam.PolicyStatement({
        effect: Iam.Effect.ALLOW,
        actions: ['ec2:StartInstances', 'ec2:StopInstances'],
        resources: [`arn:aws:ec2:${this.region}:${this.account}:instance/*`],
      })
    );

    new Scheduler.CfnSchedule(
      this,
      `Ec2Instances${pascalCase(props.action)}EventSchedule`,
      {
        name: `ec2-instance-${props.action}-event-schedule`,
        description: `ec2 instance ${props.action}`,
        flexibleTimeWindow: {
          mode: 'OFF',
        },
        scheduleExpression: props.cron,
        scheduleExpressionTimezone: props.timezone,
        state: 'ENABLED',
        target: {
          arn: `arn:aws:scheduler:::aws-sdk:ec2:${props.action}Instances`,
          input: `{ "InstanceIds": [ "${props.instanceIds}" ] }`,
          roleArn: eventScheduleRole.roleArn,
          retryPolicy: {
            maximumEventAgeInSeconds: 43200,
            maximumRetryAttempts: 1,
          },
        },
      }
    );
  }
}
```

起動/停止する時間や、対象インスタンスは時と場合によって変えれたほうが良いため、shファイル実行時に指定できるようにしています。
以下を指定できるようにしています。

- 起動する日時(cron)
- 停止する日時(cron)
- タイムゾーン
- 対象(全て or 特定のリソースを指定

```sh:deploy-ec2-instance-status-change-stack.sh
#!/bin/bash
set -eu

cd `dirname $0`

CDK_DEPLOY_ACCOUNT=$(aws sts get-caller-identity --query "Account" --output text)
echo current aws account is $CDK_DEPLOY_ACCOUNT
read -p "DEPLOY AWS REGION: " CDK_DEPLOY_REGION
export CDK_DEPLOY_REGION

read -p "インスタンスを開始するCron設定を入力してください。ex. cron(00 8 ? * MON-FRI *): " START_CRON
read -p "インスタンスを停止するCron設定を入力してください。ex. cron(00 20 ? * MON-FRI *): " STOP_CRON
read -p "スケジュールのタイムゾーンを指定してください。ex. Asia/Tokyo: " TIME_ZONE
read -p "全てのEC2インスタンスが対象ですか?(y/n): " IS_ALL

if [ $IS_ALL = 'y' ]; then
    INSTANCE_IDS=$(aws ec2 describe-instances --region ${CDK_DEPLOY_REGION} --query 'Reservations[].Instances[].InstanceId | join(`","`, @)' --output text)
elif [ $IS_ALL = 'n' ]; then
    read -p "対象EC2のインスタンスIDをカンマ区切りで入力してください。ex. i-aaaaa,i-bbbbb: " INSTANCE_IDS
else
    echo 'yまたはnを入力してください。'
    exit;
fi

cdk deploy Ec2InstanceStatusChangeStartStack \
    --context EC2_INSTANCE_TARGETS="$INSTANCE_IDS" \
    --context START_CRON="$START_CRON" \
    --context TIME_ZONE="$TIME_ZONE"

cdk deploy Ec2InstanceStatusChangeStopStack \
    --context EC2_INSTANCE_TARGETS="$INSTANCE_IDS" \
    --context STOP_CRON="$STOP_CRON" \
    --context TIME_ZONE="$TIME_ZONE"
```

### RDSを起動/停止

RDSを起動・停止するスタックは以下になります。
※構成などEC2と同様です

```typescript:lib/rds-instance-status-change-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { pascalCase } from 'change-case';
import * as Iam from 'aws-cdk-lib/aws-iam';
import * as Scheduler from 'aws-cdk-lib/aws-scheduler';

export interface RdsInstanceStatusChangeStackProps extends StackProps {
  readonly instanceIds: string[];
  readonly action: string;
  readonly cron: string;
  readonly timezone: string;
}

export class RdsInstanceStatusChangeStack extends Stack {
  constructor(
    scope: Construct,
    id: string,
    props: RdsInstanceStatusChangeStackProps
  ) {
    super(scope, id, props);

    const eventScheduleRole = new Iam.Role(
      this,
      `${pascalCase(this.node.id)}CreateEventScheduleRole`,
      {
        roleName: `rds-instance-${props.action}-event-schedule-${this.region}-role`,
        assumedBy: new Iam.ServicePrincipal('scheduler.amazonaws.com'),
      }
    );
    eventScheduleRole.addToPolicy(
      new Iam.PolicyStatement({
        effect: Iam.Effect.ALLOW,
        actions: ['rds:StartDBInstance', 'rds:StopDBInstance'],
        resources: [`arn:aws:rds:${this.region}:${this.account}:db:*`],
      })
    );

    for (const identifier of props.instanceIds) {
      new Scheduler.CfnSchedule(
        this,
        `RdsInstance${pascalCase(identifier)}${pascalCase(
          props.action
        )}EventSchedule`,
        {
          name: `rds-instance-${props.action}-${identifier}-event-schedule`,
          description: `rds instance ${props.action} ${identifier}`,
          flexibleTimeWindow: {
            mode: 'OFF',
          },
          scheduleExpression: props.cron,
          scheduleExpressionTimezone: props.timezone,
          state: 'ENABLED',
          target: {
            arn: `arn:aws:scheduler:::aws-sdk:rds:${props.action}DBInstance`,
            input: `{ "DbInstanceIdentifier": "${identifier}" }`,
            roleArn: eventScheduleRole.roleArn,
            retryPolicy: {
              maximumEventAgeInSeconds: 43200,
              maximumRetryAttempts: 1,
            },
          },
        }
      );
    }
  }
}
```

```sh:deploy-rds-instance-status-change-stack.sh
#!/bin/bash
set -eu

cd `dirname $0`

CDK_DEPLOY_ACCOUNT=$(aws sts get-caller-identity --query "Account" --output text)
echo current aws account is $CDK_DEPLOY_ACCOUNT
read -p "DEPLOY AWS REGION: " CDK_DEPLOY_REGION
export CDK_DEPLOY_REGION

read -p "RDSインスタンスを開始するCron設定を入力してください。ex. cron(00 8 ? * MON-FRI *): " START_CRON
read -p "RDSインスタンスを停止するCron設定を入力してください。ex. cron(00 20 ? * MON-FRI *): " STOP_CRON
read -p "スケジュールのタイムゾーンを指定してください。ex. Asia/Tokyo: " TIME_ZONE
read -p "全てのRDSインスタンスが対象ですか?(y/n): " IS_ALL

if [ $IS_ALL = 'y' ]; then
    # DBエンジンがAurora以外のインスタンスIDを取得
    RDS_INSTANCE_IDS=$(aws rds describe-db-instances --region ${CDK_DEPLOY_REGION} --query 'DBInstances[? !contains(Engine,`aurora`)].DBInstanceIdentifier | join(`","`, @)' --output text)
elif [ $IS_ALL = 'n' ]; then
    read -p "対象のRDSのインスタンスIDをカンマ区切りで入力してください。ex. db-main-instance,db-sub-instance: " RDS_INSTANCE_IDS
else
    echo 'yまたはnを入力してください。'
    exit;
fi

cdk deploy RdsInstanceStatusChangeStartStack \
    --context RDS_INSTANCE_TARGETS="$RDS_INSTANCE_IDS" \
    --context START_CRON="$START_CRON" \
    --context TIME_ZONE="$TIME_ZONE"

cdk deploy RdsInstanceStatusChangeStopStack \
    --context RDS_INSTANCE_TARGETS="$RDS_INSTANCE_IDS" \
    --context STOP_CRON="$STOP_CRON" \
    --context TIME_ZONE="$TIME_ZONE"
```

### Auroraを起動/停止

Auroraを起動・停止するスタックは以下になります。
※構成などEC2と同様です

```typescript:lib/rds-cluster-status-change-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { pascalCase } from 'change-case';
import * as Iam from 'aws-cdk-lib/aws-iam';
import * as Scheduler from 'aws-cdk-lib/aws-scheduler';

export interface RdsClusterStatusChangeStackProps extends StackProps {
  readonly clusterIds: string[];
  readonly action: string;
  readonly cron: string;
  readonly timezone: string;
}

export class RdsClusterStatusChangeStack extends Stack {
  constructor(
    scope: Construct,
    id: string,
    props: RdsClusterStatusChangeStackProps
  ) {
    super(scope, id, props);

    const eventScheduleRole = new Iam.Role(
      this,
      `${pascalCase(this.node.id)}CreateEventScheduleRole`,
      {
        roleName: `rds-cluster-${props.action}-event-schedule-${this.region}-role`,
        assumedBy: new Iam.ServicePrincipal('scheduler.amazonaws.com'),
      }
    );
    eventScheduleRole.addToPolicy(
      new Iam.PolicyStatement({
        effect: Iam.Effect.ALLOW,
        actions: ['rds:StartDBCluster', 'rds:StopDBCluster'],
        resources: [`arn:aws:rds:${this.region}:${this.account}:cluster:*`],
      })
    );

    for (const identifier of props.clusterIds) {
      new Scheduler.CfnSchedule(
        this,
        `RdsCluster${pascalCase(identifier)}${pascalCase(
          props.action
        )}EventSchedule`,
        {
          name: `rds-cluster-${props.action}-${identifier}-event-schedule`,
          description: `rds cluster ${props.action} ${identifier}`,
          flexibleTimeWindow: {
            mode: 'OFF',
          },
          scheduleExpression: props.cron,
          scheduleExpressionTimezone: props.timezone,
          state: 'ENABLED',
          target: {
            arn: `arn:aws:scheduler:::aws-sdk:rds:${props.action}DBCluster`,
            input: `{ "DbClusterIdentifier": "${identifier}" }`,
            roleArn: eventScheduleRole.roleArn,
            retryPolicy: {
              maximumEventAgeInSeconds: 43200,
              maximumRetryAttempts: 1,
            },
          },
        }
      );
    }
  }
}

```

```sh:deploy-rds-cluster-status-change-stack.sh
#!/bin/bash
set -eu

cd `dirname $0`

CDK_DEPLOY_ACCOUNT=$(aws sts get-caller-identity --query "Account" --output text)
echo current aws account is $CDK_DEPLOY_ACCOUNT
read -p "DEPLOY AWS REGION: " CDK_DEPLOY_REGION
export CDK_DEPLOY_REGION

read -p "RDSクラスターを開始するCron設定を入力してください。ex. cron(00 8 ? * MON-FRI *): " START_CRON
read -p "RDSクラスターを停止するCron設定を入力してください。ex. cron(00 20 ? * MON-FRI *): " STOP_CRON
read -p "スケジュールのタイムゾーンを指定してください。ex. Asia/Tokyo: " TIME_ZONE
read -p "全てのRDSクラスターが対象ですか?(y/n): " IS_ALL

if [ $IS_ALL = 'y' ]; then
    RDS_CLUSTER_IDS=$(aws rds describe-db-clusters --region ${CDK_DEPLOY_REGION} --query 'DBClusters[].DBClusterIdentifier | join(`","`, @)' --output text)
elif [ $IS_ALL = 'n' ]; then
    read -p "対象のRDSのクラスターIDをカンマ区切りで入力してください。ex. db-main-cluster,db-sub-cluster: " RDS_CLUSTER_IDS
else
    echo 'yまたはnを入力してください。'
    exit;
fi

cdk deploy RdsClusterStatusChangeStartStack \
    --context RDS_CLUSTER_TARGETS="$RDS_CLUSTER_IDS" \
    --context START_CRON="$START_CRON" \
    --context TIME_ZONE="$TIME_ZONE"

cdk deploy RdsClusterStatusChangeStopStack \
    --context RDS_CLUSTER_TARGETS="$RDS_CLUSTER_IDS" \
    --context STOP_CRON="$STOP_CRON" \
    --context TIME_ZONE="$TIME_ZONE"
```

### ECSを起動/停止

ECSを起動・停止するスタックは以下になります。
※構成などEC2と同様です

```typescript:lib/ecs-fargate-service-desired-count-change-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { pascalCase, paramCase } from 'change-case';
import * as Iam from 'aws-cdk-lib/aws-iam';
import * as Scheduler from 'aws-cdk-lib/aws-scheduler';

export interface EcsFargateServiceDesiredCountChangeStackProps
  extends StackProps {
  readonly clusterName: string;
  readonly serviceNames: string[];
  readonly desiredCount: number;
  readonly action: string;
  readonly cron: string;
  readonly timezone: string;
}

export class EcsFargateServiceDesiredCountChangeStack extends Stack {
  constructor(
    scope: Construct,
    id: string,
    props: EcsFargateServiceDesiredCountChangeStackProps
  ) {
    super(scope, id, props);

    const eventScheduleRole = new Iam.Role(
      this,
      `${pascalCase(this.node.id)}CreateEventScheduleRole`,
      {
        roleName: `${paramCase(props.clusterName)}-${props.action}-sch-${
          this.region
        }-role`,
        assumedBy: new Iam.ServicePrincipal('scheduler.amazonaws.com'),
      }
    );
    eventScheduleRole.addToPolicy(
      new Iam.PolicyStatement({
        effect: Iam.Effect.ALLOW,
        actions: ['ecs:UpdateService'],
        resources: [`arn:aws:ecs:${this.region}:${this.account}:service/*`],
      })
    );

    for (const serviceName of props.serviceNames) {
      new Scheduler.CfnSchedule(
        this,
        `${pascalCase(serviceName)}${pascalCase(props.action)}${pascalCase(
          this.region
        )}Schedule`,
        {
          name: `${paramCase(serviceName)}-${props.action}-${
            this.region
          }-schedule`,
          description: `ecs fargate service ${props.action} ${serviceName} in ${props.clusterName}`,
          flexibleTimeWindow: {
            mode: 'OFF',
          },
          scheduleExpression: props.cron,
          scheduleExpressionTimezone: props.timezone,
          state: 'ENABLED',
          target: {
            arn: `arn:aws:scheduler:::aws-sdk:ecs:updateService`,
            input: `{ "Cluster": "${props.clusterName}", "Service": "${serviceName}", "DesiredCount": ${props.desiredCount} }`,
            roleArn: eventScheduleRole.roleArn,
            retryPolicy: {
              maximumEventAgeInSeconds: 43200,
              maximumRetryAttempts: 1,
            },
          },
        }
      );
    }
  }
}
```

```sh:deploy-ecs-fargate-service-status-change-stack.sh
#!/bin/bash
set -eu

cd `dirname $0`

CDK_DEPLOY_ACCOUNT=$(aws sts get-caller-identity --query "Account" --output text)
echo current aws account is $CDK_DEPLOY_ACCOUNT
read -p "DEPLOY AWS REGION: " CDK_DEPLOY_REGION
export CDK_DEPLOY_REGION

read -p "ECSタスクを実行するCron設定を入力してください。ex. cron(00 8 ? * MON-FRI *): " START_CRON
read -p "ECSタスクを停止するCron設定を入力してください。ex. cron(00 20 ? * MON-FRI *): " STOP_CRON
read -p "スケジュールのタイムゾーンを指定してください。ex. Asia/Tokyo: " TIME_ZONE
read -p "ECSサービスの必要タスク数を半角数字で入力してください。: " DESIRED_COUNT
read -p "全てのECSクラスターが対象ですか?(y/n): " IS_ALL

if [ $IS_ALL = 'y' ]; then
    ECS_CLUSTERS=$(aws ecs list-clusters --region ${CDK_DEPLOY_REGION} --query 'clusterArns[]' --output text)
    for CLUSTER_ARN in ${ECS_CLUSTERS[@]}
        do
            ECS_CLUSTER_NAME=(${CLUSTER_ARN//\// })
            ECS_SERVICE_ARNS=$(aws ecs list-services --cluster ${ECS_CLUSTER_NAME[1]} --region ${CDK_DEPLOY_REGION} --query 'serviceArns[] | join(`","`, @)' --output text)

            ECS_SERVICE_NAMES=()
            for ECS_SERVICE_ARN in ${ECS_SERVICE_ARNS[@]}
            do
                ECS_SERVICE_NAME=(${ECS_SERVICE_ARN//\// })
                ECS_SERVICE_NAMES+=(${ECS_SERVICE_NAME[2]})
            done

            cdk deploy EcsFargateServiceDesiredCountChange${ECS_CLUSTER_NAME[1]}StartStack \
                --context ECS_CLUSTER_TARGET=${ECS_CLUSTER_NAME[1]} \
                --context ECS_SERVICE_TARGETS="$(IFS=","; echo "${ECS_SERVICE_NAMES[*]}")" \
                --context DESIRED_COUNT=$DESIRED_COUNT \
                --context START_CRON="$START_CRON" \
                --context TIME_ZONE="$TIME_ZONE"

            cdk deploy EcsFargateServiceDesiredCountChange${ECS_CLUSTER_NAME[1]}StopStack \
                --context ECS_CLUSTER_TARGET=${ECS_CLUSTER_NAME[1]} \
                --context ECS_SERVICE_TARGETS="$(IFS=","; echo "${ECS_SERVICE_NAMES[*]}")" \
                --context STOP_CRON="$STOP_CRON" \
                --context TIME_ZONE="$TIME_ZONE"
        done
elif [ $IS_ALL = 'n' ]; then
    read -p "対象のECSクラスター名を1つ入力してください。ex. admin-cluster: " ECS_CLUSTER_NAME
    read -p "${ECS_CLUSTER_NAME}の対象のECSサービス名をカンマ区切りで入力してください。ex. admin-service,user-app-service: " ECS_SERVICE_NAMES

    cdk deploy EcsFargateServiceDesiredCountChange${ECS_CLUSTER_NAME}StartStack \
        --context ECS_CLUSTER_TARGET=$ECS_CLUSTER_NAME \
        --context ECS_SERVICE_TARGETS="$ECS_SERVICE_NAMES" \
        --context DESIRED_COUNT=$DESIRED_COUNT \
        --context START_CRON="$START_CRON" \
        --context TIME_ZONE="$TIME_ZONE"

    cdk deploy EcsFargateServiceDesiredCountChange${ECS_CLUSTER_NAME}StopStack \
        --context ECS_CLUSTER_TARGET=$ECS_CLUSTER_NAME \
        --context ECS_SERVICE_TARGETS="$ECS_SERVICE_NAMES" \
        --context STOP_CRON="$STOP_CRON" \
        --context TIME_ZONE="$TIME_ZONE"
else
    echo 'yまたはnを入力してください。'
    exit;
fi
```

### 後は実行するだけ

後は、対象のAWSアカウントのAWSリソースにアクセスできるように、アクセスキーやスイッチロールを設定して、このshファイルを実行するだけです！
手動作業もないですし、複数のAWSアカウントへのデプロイも簡単に行えます :smile::smile::smile: 

:::note info
現在の推奨はIAM Identity Centerによるアクセスです。[こちらの記事](https://qiita.com/s_moriyama/items/14b703cc0dfa91a6f464)がわかりやすかったです
:::

# おわりに

今回は必要な時間に起動し、不要な時間に停止することでコストを削減していく方法を説明いたしました。
開発環境では積極的に行ったほうが良い内容なので、みなさんもどんどん実施していきましょう！！！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/87924/cbc86098-db4a-a217-66c6-cd7ae387a6ec.png)