[AWS CDK] パラメーターで渡されたEC2インスタンスのOSイメージ情報で条件分岐してみた

真面目にやろうとすると意外と大変
2024.03.22

指定されたOSイメージ情報で条件分岐させたい

こんにちは、のんピ(@non____97)です。

皆さんはAWS CDKでパラメーターで渡されたEC2インスタンスのOSイメージ情報で条件分岐させたいと思ったことはありますか? 私はあります。

受け取ったOSイメージ情報に応じて、そのEC2インスタンスに設定するユーザーデータや許可するポート、SSM Patch Managerのベースラインをカスタマイズしたい場面があります。

OSイメージ情報はIMachineImageで渡したいところです。別のプロパティでal2windowsと入力させるのは何となく嫌です。

実際にやってみました。

やってみた

AWS CDKのコード

EC2インスタンスの情報は以下のように指定してあげます。

./parameter/index.ts

export const ec2StackParams: ec2StackParams = {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
  props: {
    systemParams: {
      systemPrefix: "non-97",
      envName: "sandbox",
    },
    networkParams: {
      vpcCidr: "10.10.0.0/20",
      subnetConfigurations: [
        {
          name: "public",
          subnetType: cdk.aws_ec2.SubnetType.PUBLIC,
          cidrMask: 27,
        },
      ],
      maxAzs: 2,
      natGateways: 0,
    },
    ec2Params: {
      instances: [
        {
          instanceName: "web",
          machineImage: cdk.aws_ec2.MachineImage.latestAmazonLinux2023({
            cachedInContext: true,
          }),
          instanceType: new cdk.aws_ec2.InstanceType("t3.micro"),
          blockDevices: [
            {
              deviceName: "/dev/xvda",
              volume: cdk.aws_ec2.BlockDeviceVolume.ebs(11, {
                volumeType: cdk.aws_ec2.EbsDeviceVolumeType.GP3,
                encrypted: true,
              }),
            },
          ],
          subnetSelection: {
            subnetType: cdk.aws_ec2.SubnetType.PUBLIC,
          },
        },
        {
          instanceName: "ubuntu",
          machineImage: cdk.aws_ec2.MachineImage.lookup({
            name: "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*",
            owners: ["099720109477"],
          }),
          instanceType: new cdk.aws_ec2.InstanceType("t3.micro"),
          blockDevices: [
            {
              deviceName: "/dev/sda1",
              volume: cdk.aws_ec2.BlockDeviceVolume.ebs(10, {
                volumeType: cdk.aws_ec2.EbsDeviceVolumeType.GP3,
              }),
            },
          ],
          subnetSelection: {
            subnetType: cdk.aws_ec2.SubnetType.PUBLIC,
          },
        },
        {
          instanceName: "windows",
          machineImage: cdk.aws_ec2.MachineImage.latestWindows(
            cdk.aws_ec2.WindowsVersion.WINDOWS_SERVER_2022_JAPANESE_FULL_BASE
          ),
          instanceType: new cdk.aws_ec2.InstanceType("t3.medium"),
          blockDevices: [
            {
              deviceName: "/dev/sda1",
              volume: cdk.aws_ec2.BlockDeviceVolume.ebs(30, {
                volumeType: cdk.aws_ec2.EbsDeviceVolumeType.GP3,
              }),
            },
          ],
          subnetSelection: {
            subnetType: cdk.aws_ec2.SubnetType.PUBLIC,
          },
        },
        {
          instanceName: "al2",
          machineImage: cdk.aws_ec2.MachineImage.latestAmazonLinux2({
            cachedInContext: false,
          }),
          instanceType: new cdk.aws_ec2.InstanceType("t3.micro"),
          blockDevices: [
            {
              deviceName: "/dev/xvda",
              volume: cdk.aws_ec2.BlockDeviceVolume.ebs(10, {
                volumeType: cdk.aws_ec2.EbsDeviceVolumeType.GP3,
                encrypted: true,
              }),
            },
          ],
          subnetSelection: {
            subnetType: cdk.aws_ec2.SubnetType.PUBLIC,
          },
        },
        {
          instanceName: "rhel",
          machineImage: cdk.aws_ec2.MachineImage.lookup({
            name: "RHEL-9.3.0_HVM-20240117-x86_64-49-Hourly2-GP3",
            owners: ["309956199498"],
          }),
          instanceType: new cdk.aws_ec2.InstanceType("t3.micro"),
          blockDevices: [
            {
              deviceName: "/dev/sda1",
              volume: cdk.aws_ec2.BlockDeviceVolume.ebs(12),
            },
          ],
          subnetSelection: {
            subnetType: cdk.aws_ec2.SubnetType.PUBLIC,
          },
        },
      ],
    },
  },
};

machineImageとは別にal2など指定したくありません。machineImageで指定できるIMachineImageの実装には以下のようなものがあります。

そのためinstanceofで判定をすれば、Amazon Linux 2、Amazon Linux 2023、Windowsの判定は簡単に行えそうです。

では、RHELやUbuntuなどLookupMachineImageの判定はどうすれば良いでしょうか。

考えられる案は以下の3パターンです。

  1. LookupMachineImageのGetterやpublicなメンバー変数があれば、そこから指定されたnameownersを元に判定する
  2. AWS CDKでsynthした際にAMIのImage IDが取得できるので、AWS SDKでDescribeImagesを叩いて、取得した情報を元に判定する
  3. AWS CDKでsynthした際にAMIのImage IDが取得できるので、AWS CDKのcontextキーを逆引きして、取得したキー情報を元に判定する

1つ目のパターンはLookupMachineImageの関数はgetImage()しか存在せず、メンバー変数に直接アクセスすることもできないため、対応できません。要するに以下のようなことはできません。

const lookupMachineImage = new ec2.LookupMachineImage({
  name: 'name',
  owners: ['owners'],
});

const imageName = lookupMachineImage.name
const imageOwners = lookupMachineImage.getOwners()

2つ目のパターンについてはMFAを強制していない環境では対応が可能と考えます。以下のようにすれ取得したImage IDから、より詳細な情報を取得して判定ができそうです。

  const image = await this.getImageInfo(
    machineImage.getImage(this).imageId
  );
.
.
(中略)
.
.

  async getImageInfo(imageId: string): Promise<Image | undefined> {
    const ec2Client = new EC2Client({})

    const describeImagesCommand = new DescribeImagesCommand({
      ImageIds: [imageId],
    });

    try {
      const response = await ec2Client.send(describeImagesCommand);

      return response.Images?.[0];
    } catch (error) {
      console.error(error);
      throw new Error("Error describing images");
    }
  }

ただし、MFAを強制している環境では上手くいきません。AWS CDKを実行するときにMFAを入力することになるのですが、よしなにAWS SDKに渡してくれはしません。AWS SDKを実行する際に再度MFAの入力を行う必要があります。しかし、AWS CDKでsynthをしている際は、どうも標準入力の受付を待機してくれないようです。MFAの入力さえ受け付けてくれれば@aws-sdk/credential-providers の fromTemporaryCredentials()で何とかできそうではあります。

他の回避策としてはAWS CDKで使用している認証情報をAWS SDKのクライアントに渡すことが考えられます。残念ながらこちらも2.133.0時点では都合の良い連携方法はありませんでした。

となると、残りは3つ目のパターンです。

AWS CDKではcontextの値から直接キーを取得する方法はありません。また、contextの一覧を取得するというのもsynth中に行うことはできなさそうでした。ということでcdk.context.jsonを読み込んで、そこから判定します。

取得するcontextのキーはami:account=123456789012:filters.image-type.0=machine:filters.name.0=ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*:filters.state.0=available:owners.0=099720109477:region=us-east-1": "ami-0e21465cede02fd1eというようなフォーマットです。

このままだと扱いにくいので以下のようにパースをしてあげてから判定をします。判定されたOSイメージ情報に応じて実行するユーザーデータを変更してみます。

{
  account: '自身のAWSアカウントID',
  filters: {
    'image-type': [ 'machine' ],
    name: [ 'LookupMachineImage で指定したAMIの名前' ],
    state: [ 'available' ]
  },
  owners: [ 'LookupMachineImage で指定したAMIの所有者のAWSアカウントID' ],
  region: 'us-east-1'

EC2インスタンスを作成するコンストラクトのコードは以下の通りです。

./lib/construct/ec2-construct.ts

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { SystemParams, Ec2Params } from "../../parameter/index";
import { NetworkConstruct } from "./network-construct";
import * as fs from "fs";
import * as path from "path";

export interface Ec2ConstructProps extends SystemParams, Ec2Params {
  networkConstruct: NetworkConstruct;
}

interface ImageContext {
  account: string;
  filters: {
    "image-type"?: string[];
    name?: string[];
    state?: string[];
    [key: string]: string[] | undefined;
  };
  owners: string[];
  region: string;
}

interface SupportOs {
  osName: string;
  machineImage: {
    namePattern: string;
    ownerId: string;
  };
}

const supportOses: SupportOs[] = [
  {
    osName: "al2",
    machineImage: {
      namePattern: "amzn2-ami.*",
      ownerId: "137112412989",
    },
  },
  {
    osName: "al2023",
    machineImage: {
      namePattern: "al2023-ami.*",
      ownerId: "137112412989",
    },
  },
  {
    osName: "windows",
    machineImage: {
      namePattern: "Windows_Server-.*",
      ownerId: "801119661308",
    },
  },
  {
    osName: "rhel",
    machineImage: {
      namePattern: "RHEL-.*",
      ownerId: "309956199498",
    },
  },
  {
    osName: "ubuntu",
    machineImage: {
      namePattern: ".*ubuntu-.*",
      ownerId: "099720109477",
    },
  },
];

export class Ec2Construct extends Construct {
  readonly instances: {
    instanceName: string;
    instance: cdk.aws_ec2.Instance;
  }[];

  constructor(scope: Construct, id: string, props: Ec2ConstructProps) {
    super(scope, id);

    // サポートOS判定
    const osNames = [
      ...new Set(
        props.instances
          .map((instanceProps) => {
            return this.isSupportedOs(instanceProps.machineImage);
          })
          .filter((osName): osName is string => !!osName)
      ),
    ];

    // サポート対象のOSだった場合はユーザーデータを組み立て
    const userDataOsNameMappings = osNames.map((osName) => {
      const userData =
        osName === "windows"
          ? cdk.aws_ec2.UserData.forWindows()
          : cdk.aws_ec2.UserData.forLinux();
      const userDataScript =
        osName === "windows"
          ? fs.readFileSync(
              path.join(__dirname, `../ec2-settings/user-data/windows.ps1`),
              "utf8"
            )
          : fs.readFileSync(
              path.join(__dirname, `../ec2-settings/user-data/${osName}.sh`),
              "utf8"
            );

      userData.addCommands(
        userDataScript
          .replace(/__SYSTEM_PREFIX__/g, props.systemPrefix)
          .replace(/__ENV_NAME__/g, props.envName)
      );
      return { osName, userData };
    });

    // IAM Role
    const role = new cdk.aws_iam.Role(this, "Role", {
      assumedBy: new cdk.aws_iam.ServicePrincipal("ec2.amazonaws.com"),
      managedPolicies: [
        cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
          "AmazonSSMManagedInstanceCore"
        ),
        cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
          "CloudWatchAgentServerPolicy"
        ),
      ],
      roleName: `${props.systemPrefix}-${props.envName}-role-ec2`,
    });
    const cfnInstanceProfile = new cdk.aws_iam.CfnInstanceProfile(
      this,
      "CfnInstanceProfile",
      {
        instanceProfileName: `${props.systemPrefix}-${props.envName}-instance-profile-ec2`,
        roles: [role.roleName],
      }
    );

    // EC2 Instances
    this.instances = props.instances.map((instanceProps) => {
      const instanceSuffix = instanceProps.instanceName
        ? `-${instanceProps.instanceName}`
        : "";
      const instanceName = `${props.systemPrefix}-${props.envName}-ec2${instanceSuffix}`;

      // Security Group
      const securityGroupName = `${props.systemPrefix}-${props.envName}-sg-ec2${instanceSuffix}`;
      const securityGroup = new cdk.aws_ec2.SecurityGroup(
        this,
        `SecurityGroup${instanceSuffix}`,
        {
          vpc: props.networkConstruct.vpc,
          securityGroupName,
          description: `Security Group for ${props.systemPrefix} ${props.envName} EC2 Instance ${instanceSuffix}`,
        }
      );
      cdk.Tags.of(securityGroup).add("Name", securityGroupName);

      // Instance
      const instance = new cdk.aws_ec2.Instance(
        this,
        `Instance${instanceSuffix}`,
        {
          machineImage: instanceProps.machineImage,
          instanceType: instanceProps.instanceType,
          vpc: props.networkConstruct.vpc,
          vpcSubnets: props.networkConstruct.vpc.selectSubnets(
            instanceProps.subnetSelection
          ),
          blockDevices: instanceProps.blockDevices,
          propagateTagsToVolumeOnCreation: true,
          role,
          requireImdsv2: true,
          userData: userDataOsNameMappings.find((userDataOsNameMapping) => {
            return (
              userDataOsNameMapping.osName ===
              this.isSupportedOs(instanceProps.machineImage)
            );
          })?.userData,
          securityGroup,
          instanceName: `${props.systemPrefix}-${props.envName}-ec2${instanceSuffix}`,
        }
      );

      // Instance profile
      instance.node.tryRemoveChild("InstanceProfile");
      const cfnInstance = instance.node.tryFindChild(
        "Resource"
      ) as cdk.aws_ec2.CfnInstance;
      cfnInstance.addDependency(cfnInstanceProfile);
      cfnInstance.addPropertyOverride(
        "IamInstanceProfile",
        cfnInstanceProfile.ref
      );

      return { instanceName, instance };
    });
  }

  // サポート対象のOSかどうか判定
  isSupportedOs = (machineImage: cdk.aws_ec2.IMachineImage) => {
    // AL 2023
    if (machineImage instanceof cdk.aws_ec2.AmazonLinux2023ImageSsmParameter) {
      return "al2023";
    }
    // AL 2
    else if (
      machineImage instanceof cdk.aws_ec2.AmazonLinux2ImageSsmParameter
    ) {
      return "al2";
    }
    // Windows
    else if (machineImage instanceof cdk.aws_ec2.WindowsImage) {
      return "windows";
    }
    // Lookup
    else if (machineImage instanceof cdk.aws_ec2.LookupMachineImage) {
      // Image IDの取得
      const imageId = machineImage.getImage(this).imageId;

      // 取得したImage IDが ami-1234 の場合はcontext.cdk.jsonに記録されていない場合
      // この場合は再度AMIの検索がかかる
      if (imageId === "ami-1234") {
        console.log("AMI not found in context.cdk.json");

        return undefined;
      }
      // ami-1234 以外でImage IDが指定されていた場合
      else if (imageId) {
        // cdk.context.jsonから対象のImage IDのキーを取得する
        const contextJson = JSON.parse(
          fs.readFileSync(
            path.join(__dirname, "../../cdk.context.json"),
            "utf8"
          )
        );

        const imageContextKey = Object.keys(contextJson).find((key: string) => {
          return contextJson[key] === imageId;
        });

        if (!imageContextKey) {
          return;
        }

        // cdk.context.jsonのキーをパースする
        const imageContext = this.parseImageContext(imageContextKey);

        // パースしたcdk.context.jsonのキーとサポート対象のOSのオブジェクトの配列を突き合わせて、サポート対象かどうか判定する
        const supportOs = supportOses.find((supportOs) => {
          const imageNamePattern = new RegExp(
            supportOs.machineImage.namePattern
          );
          return (
            imageContext.filters.name &&
            imageContext.filters.name.filter((name) =>
              imageNamePattern.test(name)
            ).length > 0 &&
            imageContext.owners.filter(
              (owner) => owner === supportOs.machineImage.ownerId
            ).length > 0
          );
        });

        if (supportOs) {
          return supportOs.osName;
        } else {
          throw new Error("Unsupported machine image type");
        }
      } else {
        throw new Error("AMI not found");
      }
    }
    // 不明な型の場合はサポート対象外と判定する
    else {
      throw new Error("Unsupported machine image type");
    }
  };

  // cdk.context.jsonのImage keyのParse
  parseImageContext(imageContextKey: string): ImageContext {
    // imageContextKey は以下のようなフォーマット
    // "ami:account=123456789012:filters.image-type.0=machine:filters.name.0=ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*:filters.state.0=available:owners.0=099720109477:region=us-east-1": "ami-0e21465cede02fd1e"
    const imageContext: ImageContext = {
      account: "",
      filters: {},
      owners: [],
      region: "",
    };

    const keyValueStrings = imageContextKey.split(":");
    const prefix = keyValueStrings.shift();

    if (prefix !== "ami") {
      throw new Error("Invalid image context key format");
    }

    // "=" をデリミタとしてパース
    for (const keyValueString of keyValueStrings) {
      const [key, value] = keyValueString.split("=");
      const parsedKey = key.replace(/\./g, "_");

      if (parsedKey === "account") {
        imageContext.account = value;
      } else if (parsedKey.startsWith("filters_")) {
        const [_, filterName] = parsedKey.split("_");
        if (!imageContext.filters[filterName]) {
          imageContext.filters[filterName] = [];
        }
        imageContext.filters[filterName]!.push(value);
      } else if (parsedKey.startsWith("owners_")) {
        imageContext.owners = value.split(",");
      } else if (parsedKey === "region") {
        imageContext.region = value;
      }
    }

    return imageContext;
  }
}

その他のコードは以下GitHubリポジトリをご覧ください。

デプロイしてみる

それでは実際にデプロイしてみましょう。

AWS CDKのcontextの情報は以下のとおりです。AMIの情報はキャッシュされていません。

$ npx cdk context
Context found in cdk.json:

┌────┬───────────────────────────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────┐
│ #  │ Key                                                                                   │ Value                                                                                  │
├────┼───────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ 1  │ @aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver                     │ true                                                                                   │
├────┼───────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ 2  │ @aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId                           │ true                                                                                   │
├────┼───────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
.
.
(中略)
.
.
├────┼───────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ 44 │ availability-zones:account=<AWSアカウントID>:region=us-east-1                              │ [ "us-east-1a", "us-east-1b", "us-east-1c", "us-east-1d", "us-east-1e", "us-east-1f" ] │
└────┴───────────────────────────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────┘
Run cdk context --reset KEY_OR_NUMBER to remove a context key. It will be refreshed on the next CDK synthesis run.

デプロイします。

$ npx cdk deploy
AMI not found in context.cdk.json
AMI not found in context.cdk.json
AMI not found in context.cdk.json
AMI not found in context.cdk.json
AMI not found in context.cdk.json
AMI not found in context.cdk.json
AMI not found in context.cdk.json
AMI not found in context.cdk.json
Searching for AMI in <AWSアカウントID>:us-east-1
Searching for AMI in <AWSアカウントID>:us-east-1

✨  Synthesis time: 20.44s

cicd-non-97-sandbox:  start: Building 0d69c41138981a5aef0cbdb83a1414dedb3d704752c4162bbc7164b12b2c221e:<AWSアカウントID>-us-east-1
cicd-non-97-sandbox:  success: Built 0d69c41138981a5aef0cbdb83a1414dedb3d704752c4162bbc7164b12b2c221e:<AWSアカウントID>-us-east-1
cicd-non-97-sandbox:  start: Publishing 0d69c41138981a5aef0cbdb83a1414dedb3d704752c4162bbc7164b12b2c221e:<AWSアカウントID>-us-east-1
cicd-non-97-sandbox:  success: Published 0d69c41138981a5aef0cbdb83a1414dedb3d704752c4162bbc7164b12b2c221e:<AWSアカウントID>-us-east-1
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬──────────────────────────────────────────────────────────────────┬────────┬──────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────┬───────────┐
│   │ Resource                                                         │ Effect │ Action                                                           │ Principal                                                         │ Condition │
├───┼──────────────────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────┼───────────┤
│ + │ ${Custom::VpcRestrictDefaultSGCustomResourceProvider/Role.Arn}   │ Allow  │ sts:AssumeRole                                                   │ Service:lambda.amazonaws.com                                      │           │
├───┼──────────────────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────┼───────────┤
│ + │ ${Ec2Construct/Role.Arn}                                         │ Allow  │ sts:AssumeRole                                                   │ Service:ec2.amazonaws.com                                         │           │
├───┼──────────────────────────────────────────────────────────────────┼────────┼──────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────┼───────────┤
│ + │ arn:aws:ec2:us-east-1:<AWSアカウントID>:security-group/${NetworkConst │ Allow  │ ec2:AuthorizeSecurityGroupEgress                                 │ AWS:${Custom::VpcRestrictDefaultSGCustomResourceProvider/Role}    │           │
│   │ ruct/Default.DefaultSecurityGroup}                               │        │ ec2:AuthorizeSecurityGroupIngress                                │                                                                   │           │
│   │                                                                  │        │ ec2:RevokeSecurityGroupEgress                                    │                                                                   │           │
│   │                                                                  │        │ ec2:RevokeSecurityGroupIngress                                   │                                                                   │           │
└───┴──────────────────────────────────────────────────────────────────┴────────┴──────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                                                   │ Managed Policy ARN                                                                           │
├───┼────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Custom::VpcRestrictDefaultSGCustomResourceProvider/Role} │ {"Fn::Sub":"arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"} │
├───┼────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Ec2Construct/Role}                                       │ arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore                           │
│ + │ ${Ec2Construct/Role}                                       │ arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy                            │
└───┴────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────┘
Security Group Changes
┌───┬───────────────────────────────────────────────┬─────┬────────────┬─────────────────┐
│   │ Group                                         │ Dir │ Protocol   │ Peer            │
├───┼───────────────────────────────────────────────┼─────┼────────────┼─────────────────┤
│ + │ ${Ec2Construct/SecurityGroup-al2.GroupId}     │ Out │ Everything │ Everyone (IPv4) │
├───┼───────────────────────────────────────────────┼─────┼────────────┼─────────────────┤
│ + │ ${Ec2Construct/SecurityGroup-rhel.GroupId}    │ Out │ Everything │ Everyone (IPv4) │
├───┼───────────────────────────────────────────────┼─────┼────────────┼─────────────────┤
│ + │ ${Ec2Construct/SecurityGroup-ubuntu.GroupId}  │ Out │ Everything │ Everyone (IPv4) │
├───┼───────────────────────────────────────────────┼─────┼────────────┼─────────────────┤
│ + │ ${Ec2Construct/SecurityGroup-web.GroupId}     │ Out │ Everything │ Everyone (IPv4) │
├───┼───────────────────────────────────────────────┼─────┼────────────┼─────────────────┤
│ + │ ${Ec2Construct/SecurityGroup-windows.GroupId} │ Out │ Everything │ Everyone (IPv4) │
└───┴───────────────────────────────────────────────┴─────┴────────────┴─────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
cicd-non-97-sandbox: deploying... [1/1]
cicd-non-97-sandbox: creating CloudFormation changeset...

 ✅  cicd-non-97-sandbox

✨  Deployment time: 186.5s

Stack ARN:
arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/cicd-non-97-sandbox/e5622290-e822-11ee-9d49-0affda88fcdf

✨  Total time: 206.94s


$ npx cdk context
Context found in cdk.json:

┌────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ #  │ Key                                                                                                          │ Value                                                                                                        │
├────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 1  │ @aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver                                            │ true                                                                                                         │
├────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2  │ @aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId                                                  │ true                                                                                                         │
├────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
.
.
(中略)
.
.
├────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 43 │ @aws-cdk/customresources:installLatestAwsSdkDefault                                                          │ false                                                                                                        │
├────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 44 │ ami:account=<AWSアカウントID>:filters.image-type.0=machine:filters.name.0=RHEL-9.3.0_HVM-20240117-x86_64-49-Hourl │ "ami-0fe630eb857a6ec83"                                                                                      │
│    │ y2-GP3:filters.state.0=available:owners.0=309956199498:region=us-east-1                                      │                                                                                                              │
├────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 45 │ ami:account=<AWSアカウントID>:filters.image-type.0=machine:filters.name.0=ubuntu/images/hvm-ssd/ubuntu-jammy-22.0 │ "ami-0e21465cede02fd1e"                                                                                      │
│    │ 4-amd64-server-*:filters.state.0=available:owners.0=099720109477:region=us-east-1                            │                                                                                                              │
├────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 46 │ availability-zones:account=<AWSアカウントID>:region=us-east-1                                                     │ [ "us-east-1a", "us-east-1b", "us-east-1c", "us-east-1d", "us-east-1e", "us-east-1f" ]                       │
├────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 47 │ ssm:account=<AWSアカウントID>:parameterName=/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64:reg │ "ami-0c101f26f147fa7fd"                                                                                      │
│    │ ion=us-east-1                                                                                                │                                                                                                              │
└────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
Run cdk context --reset KEY_OR_NUMBER to remove a context key. It will be refreshed on the next CDK synthesis run.

問題なくデプロイできました。contextにもcachedInContexttrueにしたものやMachineImage.lookupで指定したものはキャッシュされていますね。

作成されたEC2インスタンスのユーザーデータは以下のとおりです。

Amazon Linux 2023

$ cat /etc/os-release
NAME="Amazon Linux"
VERSION="2023"
ID="amzn"
ID_LIKE="fedora"
VERSION_ID="2023"
PLATFORM_ID="platform:al2023"
PRETTY_NAME="Amazon Linux 2023.4.20240319"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2023"
HOME_URL="https://aws.amazon.com/linux/amazon-linux-2023/"
DOCUMENTATION_URL="https://docs.aws.amazon.com/linux/"
SUPPORT_URL="https://aws.amazon.com/premiumsupport/"
BUG_REPORT_URL="https://github.com/amazonlinux/amazon-linux-2023"
VENDOR_NAME="AWS"
VENDOR_URL="https://aws.amazon.com/"
SUPPORT_END="2028-03-15"

$ cat /var/log/user-data.log
+ declare -r SYSTEM_PREFIX=non-97
+ declare -r ENV_NAME=sandbox
+ echo 'non-97 sandbox AL2023 Instance'
non-97 sandbox AL2023 Instance

Ubuntu

$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.4 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

$ cat /var/log/user-data.log
+ declare -r SYSTEM_PREFIX=non-97
+ declare -r ENV_NAME=sandbox
+ echo 'non-97 sandbox Ubuntu Instance'
non-97 sandbox Ubuntu Instance

Amazon Linux 2

$ cat /etc/os-release
NAME="Amazon Linux"
VERSION="2"
ID="amzn"
ID_LIKE="centos rhel fedora"
VERSION_ID="2"
PRETTY_NAME="Amazon Linux 2"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2"
HOME_URL="https://amazonlinux.com/"
SUPPORT_END="2025-06-30"

$ cat /var/log/user-data.log
+ declare -r SYSTEM_PREFIX=non-97
+ declare -r ENV_NAME=sandbox
+ echo 'non-97 sandbox AL2 Instance'
non-97 sandbox AL2 Instance

Windows Server

> Get-WmiObject Win32_OperatingSystem


SystemDirectory : C:\Windows\system32
Organization    : Amazon.com
BuildNumber     : 20348
RegisteredUser  : EC2
SerialNumber    : 00454-60000-00001-AA671
Version         : 10.0.20348



> cat C:\Windows\system32\config\systemprofile\AppData\Local\Temp\EC2Launch2080308524\output.tmp
non-97 sandbox Windows Instance

RHEL

$ cat /etc/os-release
NAME="Red Hat Enterprise Linux"
VERSION="9.3 (Plow)"
ID="rhel"
ID_LIKE="fedora"
VERSION_ID="9.3"
PLATFORM_ID="platform:el9"
PRETTY_NAME="Red Hat Enterprise Linux 9.3 (Plow)"
ANSI_COLOR="0;31"
LOGO="fedora-logo-icon"
CPE_NAME="cpe:/o:redhat:enterprise_linux:9::baseos"
HOME_URL="https://www.redhat.com/"
DOCUMENTATION_URL="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9"
BUG_REPORT_URL="https://bugzilla.redhat.com/"

REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 9"
REDHAT_BUGZILLA_PRODUCT_VERSION=9.3
REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
REDHAT_SUPPORT_PRODUCT_VERSION="9.3"

$ cat /var/log/user-data.log
+ declare -r SYSTEM_PREFIX=non-97
+ declare -r ENV_NAME=sandbox
++ curl -s -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600' http://169.254.169.254/latest/api/token
+ token=AQAEAINkCALNEEsNiOZPMH-KMT-vKCZd0jh29RPQkncyq_BzCaMlew==
++ sed -e 's/.$//'
++ curl -s -H 'X-aws-ec2-metadata-token: AQAEAINkCALNEEsNiOZPMH-KMT-vKCZd0jh29RPQkncyq_BzCaMlew==' http://169.254.169.254/latest/meta-data/placement/availability-zone
+ region_name=us-east-1
+ dnf install -y https://s3.us-east-1.amazonaws.com/amazon-ssm-us-east-1/latest/linux_amd64/amazon-ssm-agent.rpm
Updating Subscription Management repositories.
Unable to read consumer identity

This system is not registered with an entitlement server. You can use subscription-manager to register.

Red Hat Enterprise Linux 9 for x86_64 - AppStre  35 MB/s |  30 MB     00:00
Red Hat Enterprise Linux 9 for x86_64 - BaseOS  1.2 MB/s |  19 MB     00:16
Red Hat Enterprise Linux 9 Client Configuration 422  B/s | 2.2 kB     00:05
amazon-ssm-agent.rpm                             79 MB/s |  26 MB     00:00
Dependencies resolved.
================================================================================
 Package                Architecture Version           Repository          Size
================================================================================
Installing:
 amazon-ssm-agent       x86_64       3.3.131.0-1       @commandline        26 M

Transaction Summary
================================================================================
Install  1 Package

Total size: 26 M
Installed size: 108 M
Downloading Packages:
Running transaction check
Transaction check succeeded.
Running transaction test
Transaction test succeeded.
Running transaction
  Running scriptlet: amazon-ssm-agent-3.3.131.0-1.x86_64                    1/1
  Preparing        :                                                        1/1
  Running scriptlet: amazon-ssm-agent-3.3.131.0-1.x86_64                    1/1
  Installing       : amazon-ssm-agent-3.3.131.0-1.x86_64                    1/1
  Running scriptlet: amazon-ssm-agent-3.3.131.0-1.x86_64                    1/1
Created symlink /etc/systemd/system/multi-user.target.wants/amazon-ssm-agent.service → /etc/systemd/system/amazon-ssm-agent.service.

  Verifying        : amazon-ssm-agent-3.3.131.0-1.x86_64                    1/1
Installed products updated.

Installed:
  amazon-ssm-agent-3.3.131.0-1.x86_64

Complete!
+ systemctl enable amazon-ssm-agent --now
+ echo 'non-97 sandbox RHEL Instance'
non-97 sandbox RHEL Instance

それぞれのOSイメージごとに用意したユーザーデータのスクリプトを使っていますね。

真面目にやろうとすると意外と大変

AWS CDKでパラメーターとして渡されたEC2インスタンスのOSイメージ情報で条件分岐してみました。

真面目にやろうとすると意外と大変でした。

ちなみにLookupMachineImageで指定するfiltersはDescribeImages APIのFilterです。CPUアーキテクチャやタグなどを使って判定することもできそうですね。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!