1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初めてのCDK】CDK + SSM Automation でEC2を毎日自動停止

1
Last updated at Posted at 2026-04-17

はじめに

以前、AWSのチームジャムでハンズオンでのSystemManagerを使った踏み台インスタンスの自動停止を学習しました。
今回はその内容を、CDKを使って実装してみました。
同じような方の参考になれば嬉しいです。

AWS チームジャム
https://jam.aws.com/

教材名
【Automation Gone Rogue: Fixing Critical EC2 Downtime】

やりたいこと

起動しっぱなし防止のため、毎日24時にEC2を自動で停止する。

構成(ざっくり)

  • 必要なものをインポート
  • IAMロール作成
  • Association作成
  • スタックの呼び出し

スタックのコード

import { Stack, StackProps, Fn, Tags } from 'aws-cdk-lib';  // 骨格・Fn・タグ
import { Construct } from 'constructs';                     //Construct型を使用したリソースの管理
import * as iam from 'aws-cdk-lib/aws-iam';                 // IAM ロール
import * as ssm from 'aws-cdk-lib/aws-ssm';                 // Association

// デプロイする前にミスを検出するため、CDKのスタックに渡す設定(プロパティ)を定義
// CDK の Stack クラスには元々 env(アカウント・リージョン)や stackName などの props がある
// 今回は systemName と envName を追加
export interface SsmStackProps extends StackProps {
  systemName: string;
  envName: string;
}

export class SsmStack extends Stack {
  constructor(scope: Construct, id: string, props: SsmStackProps) {
    super(scope, id, props);
    const { systemName, envName } = props;  // props から値を取り出す
    const bastionInstanceId = Fn.importValue(`${systemName}-${envName}-BastionInstanceId`);
    // BastionStack(別スタック)が CfnOutput で同名のエクスポートを登録しているため、Fn.importValue で取り出せる。
    // BastionStack を先にデプロイ済みであることが前提。

    // IAMロール作成
    const ec2AutoStopRole = new iam.Role(this, 'Ec2AutoStopRole', {
      roleName: `${systemName}-${envName}-ec2-auto-stop-role`, // 任意の識別用の名前
      assumedBy: new iam.ServicePrincipal('ssm.amazonaws.com'), //「誰(どの主体)がこのロールを使えるか」を指定
      inlinePolicies:{ //ロールの中に直接組み込むポリシー を指定する
        'StopEC2Policy': new iam.PolicyDocument({ //「StopEC2Policy」は任意のポリシー名
          statements: [ //権限の内容を定義する
            new iam.PolicyStatement({ //IAM ポリシーの「1つの権限セット」を作る命令
              actions: ['ec2:StopInstances'], //このロールでできること(既存アクションを指定)
              resources: [`arn:aws:ec2:ap-northeast-1:${this.account}:instance/${bastionInstanceId}`],
              //「どのリソースに権限を適用するか」を指定する 
              // arn:aws:サービス名:リージョン:アカウントID:リソースタイプ/インスタンスID
            }),
            new iam.PolicyStatement({ //ec2:DescribeInstanceStatus用の権限セットを作る
              actions: ['ec2:DescribeInstanceStatus'],
              resources: ['*'],
              //DescribeInstanceStatusのリソースタイプは*が必須
            }),
          ],
        }),
      },
    });

    // Association作成
    new ssm.CfnAssociation(this, 'StopBastionAssociation', {
      name: 'AWS-StopEC2Instance', // 使用するSSMオートメーションドキュメント名
      associationName: 'Stop_EC2Instance', // 任意の識別用の名前
      automationTargetParameterName: 'InstanceId', // targetsで指定した値をInstanceIdに渡す(AWS-StopEC2Instance → InstanceIdと決まっている)
      targets: [
        { key: 'ParameterValues', values: [bastionInstanceId]}
      ],
      scheduleExpression: 'cron(0 15 * * ? *)',
      // 毎日 15:00 UTC に実行(日本時間(UTC+9)だと 24:00)
      parameters: {
        AutomationAssumeRole: [ec2AutoStopRole.roleArn],
        // ロールのARNを入力
        // ARN とは AWS のリソースすべてに割り当てられる一意の識別子
        // IAM ロールを作ると、CDK が自動で ARN を計算してくれる
        // 「作ったロール名.roleArn」でARNを取り出せる。
      },
    });
  }
};

呼び出しのコード(該当部分の抜粋)

import * as cdk from 'aws-cdk-lib/core';
import { SsmStack } from '../lib/ssm-stack';

const app = new cdk.App();

// -c envName=stg で切替(デフォルト: prod)
const systemName = app.node.tryGetContext('systemName') ?? 'snr-sns';
const envName: string = app.node.tryGetContext('envName') ?? 'prod';

// ----------------------------------------------------------------
// Step 8: 踏み台 EC2 の自動停止
// ⚠ BastionStack(app)が先にデプロイ済みであること(BastionInstanceId を参照するため)                                                                                       
// ⚠ 毎日 JST 24:00(UTC 15:00)に踏み台 EC2 を自動停止する
// ----------------------------------------------------------------
new SsmStack(app, `${systemName}-${envName}-ssm-stack`,{
  systemName,
  envName,
});

つまずいた点

別スタックからの値の参照

BastionStack で作った EC2 のインスタンスID を SsmStack で使いたかったが、スタックをまたいで直接参照できないことに最初気づかなかった。
調べると CloudFormation のエクスポート/インポートという仕組みがあり、
・渡す側:CfnOutput で exportName を指定
・受け取る側:Fn.importValue で同じ名前を指定
という構成で解決できた。デプロイ順序に依存関係ができる点も注意が必要。

ドキュメントの参照

「SSM の Association を CDK で作るには?」と調べ始めたが、
どのクラスを見ればいいかが分からず迷子になった。
(今も完全には慣れていない)

学んだこと

CDKの構成(lib/(定義)と bin/(呼び出し))

→初は「なぜ分かれてるんだろう?」と思っていたが、bin/ 側で props を変えるだけで同じスタックを prod/stg に展開できると知って納得した。

synth、diff、deployコマンド

・synth :
CDK のコード(TypeScript)を CloudFormation テンプレート(JSON/YAML)に変換する。「このコードは正しく書けているか?」を確認するのに使う。

・diff :
現在 AWS にデプロイされているスタックと、ローカルのコードの差分を表示する。

・deploy :
実際に AWS にデプロイする。内部では synth → CloudFormation の変更セット実行 の順に動く。

スタック間の値の受け渡し(Fn.importValue)

別スタックの値を直接参照できないことに最初戸惑ったが、
エクスポート/インポートの仕組みを理解したら腑に落ちた。

ドキュメントの探し方

該当ドキュメントにたどり着くには、最初の検索語が重要だと感じた。
また、ドキュメント内では「モジュール名 → クラス名」の順で探すと効率がよいことが分かった。

実際には、以下のような検索がスムーズだった:
aws cdk + モジュール名 + クラス名

まとめ

本記事では、AWSチームジャムのハンズオン内容をもとに、Systems Managerを使ったEC2の自動停止処理をCDKで実装しました。
実装を通して、CloudFormationとの関係性やスタック間連携など、CDKの全体構造への理解を深めることができました。
今後も実装を通して、より実務に近い形でCDKの設計・運用パターンを学びたいと思います。

※ この記事は新人研修の一環です

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?