はじめに
以前、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の設計・運用パターンを学びたいと思います。
※ この記事は新人研修の一環です