AWS CDKでEC2をデプロイする

# はじめに
本記事はAWS環境にてAWS CDKでEC2をデプロイするための方法について記載しています。

AWS CDKについては以前書いた[5分で理解するAWS CDK](https://qiita.com/Brutus/items/6c8d9bfaab7af53d154a)を参照。

## CDKのデプロイ（EC2）
ウォークスルーとして[Amazon EC2 でウェブアプリケーションをデプロイする](https://aws.amazon.com/jp/getting-started/guides/deploy-webapp-ec2/)を参考にしながら、コード[^1]を一部変更してデプロイします。使用する言語はTypeScriptです。

[^1]: 出典：[Amazon EC2 でウェブアプリケーションをデプロイする](https://aws.amazon.com/jp/getting-started/guides/deploy-webapp-ec2/module-one/)

### 準備作業
作業ディレクトリで以下のコマンドを実行し、新しいTypeScript用のCDKプロジェクト作成します。

`$ mkdir ec2-cdk`
`$ cd ec2-cdk/`
`$ cdk init app --language typescript`

CDKプロジェクト作成後に生成されたファイルを確認します。
エントリポイントは`bin/ec2-cdk.ts`ファイルです。
従って、エントリポイントから呼ばれる`lib/ec2-cdk-stack.ts`ファイルを実装してAWSリソースの構築を行います。

- bin/ec2-cdk.ts

```typescript
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { Ec2CdkStack } from '../lib/ec2-cdk-stack';

const app = new cdk.App();
new Ec2CdkStack(app, 'Ec2CdkStack', {
  /* If you don't specify 'env', this stack will be environment-agnostic.
   * Account/Region-dependent features and context lookups will not work,
   * but a single synthesized template can be deployed anywhere. */

  /* Uncomment the next line to specialize this stack for the AWS Account
   * and Region that are implied by the current CLI configuration. */
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

  /* Uncomment the next line if you know exactly what Account and Region you
   * want to deploy the stack to. */
  // env: { account: '123456789012', region: 'us-east-1' },

  /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});% 
```

- lib/ec2-cdk-stack.ts

```typescript
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';

export class Ec2CdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here

    // example resource
    // const queue = new sqs.Queue(this, 'Ec2CdkQueue', {
    //   visibilityTimeout: cdk.Duration.seconds(300)
    // });
  }
}
```

上記コードを基にEC2インスタンスの定義情報を実装していきます。
VSCodeを使用している環境で`cdk-ec2-key-pair`モジュールが存在しない場合、以下のようなエラーが出力されます。

![スクリーンショット 2022-06-06 17.22.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/aaba7db4-ea66-5fbf-02c5-f9d0d26511f4.png)

上記エラーが出た場合は以下のコマンドを実行し、npmで`cdk-ec2-key-pair`をインストールします。

`$ npm install cdk-ec2-key-pair`

以下は出力結果です。「found 0 vulnerabilities」のメッセージより、パッケージに対する脆弱性が見つからなかったことが確認できます。
また、npmインストール後、ワークスペースで問題が検出されなかったことについて確認できます。

```console
npm WARN ec2-cdk@0.1.0 No repository field.
npm WARN ec2-cdk@0.1.0 No license field.

+ cdk-ec2-key-pair@3.3.1
added 1 package from 1 contributor and audited 375 packages in 2.312s

28 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
```

EC2インスタンスを1台構築するための定義情報を記載した`lib/ec2-cdk-stack.ts`ファイルは以下になります。セキュリティグループはSSHのみ許可しています。

- lib/ec2-cdk-stack.ts

```typescript
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from "aws-cdk-lib/aws-iam"; // Allows working with IAM resources
import * as s3assets from "aws-cdk-lib/aws-s3-assets"; // Allows managing files with S3
import * as keypair from "cdk-ec2-key-pair"; // Helper to create EC2 SSH keypairs
import * as path from "path"; // Helper for working with file paths

export class Ec2CdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
          
    // Look up the default VPC
    const vpc = ec2.Vpc.fromLookup(this, "VPC", {
      isDefault: true
    });

    // Create a key pair to be used with this EC2 Instance
    const key = new keypair.KeyPair(this, "KeyPair", {
      name: "cdk-keypair",
      description: "Key Pair created with CDK Deployment",
    });
    key.grantReadOnPublicKey; 

    // Security group for the EC2 instance
    const securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
      vpc,
      description: "Allow SSH (TCP port 22) and HTTP (TCP port 80) in",
      allowAllOutbound: true,
    });

    // Allow SSH access on port tcp/22
    securityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(22),
      "Allow SSH Access"
    );

    // IAM role to allow access to other AWS services
    const role = new iam.Role(this, "ec2Role", {
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
    });

    // IAM policy attachment to allow access to 
    role.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")
    );

    // Look up the AMI Id for the Amazon Linux 2 Image with CPU Type X86_64
    const ami = new ec2.AmazonLinuxImage({
      generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      cpuType: ec2.AmazonLinuxCpuType.X86_64,
    });

    // Create the EC2 instance using the Security Group, AMI, and KeyPair defined.
    const ec2Instance = new ec2.Instance(this, "Instance", {
      vpc,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      machineImage: ami,
      securityGroup: securityGroup,
      keyName: key.keyPairName,
      role: role,
    });  
  }
}
```

### デプロイ作業
以下のコマンドを実行し、CloudFormationの定義情報が出力されるので、定義するテンプレートの内容を確認します。

`$ cdk synth`

アカウントとリージョンの指定がない場合、エラーが出力されます。

```console
Cannot retrieve value from context provider vpc-provider since account/region are not specified at the stack level. Configure "env" with an account and region when you define your stack.See https://docs.aws.amazon.com/cdk/latest/guide/environments.html for more details.

Subprocess exited with error 1
```

そのため、`bin/ec2-cdk.ts`ファイルに下記、envの行を追記します。変更箇所は1行のみです。

- `bin/ec2-cdk.ts`

```typescript
const app = new cdk.App();
new Ec2CdkStack(app, 'Ec2CdkStack', {
  env: {account: 'XXXXXXXXXXXX', region: 'ap-northeast-1'},
});
```

再度、`synth`コマンドを実行すると正常な結果が出力されることが確認できます。
問題ないことを確認してデプロイします。

`$ cdk deploy`

```console
Do you wish to deploy these changes (y/n)? y
```

デプロイ完了後、以下のようなメッセージが出力されます。
EC2 Instance Connectや、セッションマネージャーを用いてSSHでEC2インスタンスに接続できます。

```console
 ✅  Ec2CdkStack

✨  Deployment time: 219.15s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/Ec2CdkStack/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

✨  Total time: 225.55s
```

## Tips
ウォークスルーで使用したコードのAMIの指定は以下の通りに[class AmazonLinuxImage](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.AmazonLinuxImage.html)クラスを使用しています。

```typescript
// Look up the AMI Id for the Amazon Linux 2 Image with CPU Type X86_64
    const ami = new ec2.AmazonLinuxImage({
      generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      cpuType: ec2.AmazonLinuxCpuType.X86_64,
    });
```

動作的にはAmazonLinuxの最新バージョンを選択し、AMI IDはSSMパラメータストアに公開された値が選択されます。

TipsとしてUbuntuのイメージを使用する例について以下に記載します。
以下の例では`fromSsmParameter`メソッドを使用して取得したAMI IDを`machineImage`の値に設定しています。

- 参考：[class MachineImage](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.MachineImage.html)

```typescript
    // Look up the AMI Id for the Amazon Linux 2 Image with CPU Type X86_64
    /* const ami = new ec2.AmazonLinuxImage({
      generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      cpuType: ec2.AmazonLinuxCpuType.X86_64,
    }); */

    const machineImage = ec2.MachineImage.fromSsmParameter(
      '/aws/service/canonical/ubuntu/server/focal/stable/current/amd64/hvm/ebs-gp2/ami-id',
    )

 // Create the EC2 instance using the Security Group, AMI, and KeyPair defined.
    const ec2Instance = new ec2.Instance(this, "Instance", {
      vpc,
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      machineImage: machineImage,
      securityGroup: securityGroup,
      keyName: key.keyPairName,
      role: role,
    });  

```

AWS CLIで実行すると詳細を確認できます。

```console
bash-3.2$ aws ssm get-parameter --name "/aws/service/canonical/ubuntu/server/focal/stable/current/amd64/hvm/ebs-gp2/ami-id"
{
    "Parameter": {
        "Name": "/aws/service/canonical/ubuntu/server/focal/stable/current/amd64/hvm/ebs-gp2/ami-id",
        "Type": "String",
        "Value": "ami-0bf8833870a0f064c",
        "Version": 249,
        "LastModifiedDate": "2022-06-16T10:35:48.842000+09:00",
        "ARN": "arn:aws:ssm:ap-northeast-1::parameter/aws/service/canonical/ubuntu/server/focal/stable/current/amd64/hvm/ebs-gp2/ami-id",
        "DataType": "aws:ec2:image"
    }
}
```

このAMI（Ubuntu, 20.04 LTS, amd64）はコミュニティとしてUbuntuを開発している[Canonical](https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%8E%E3%83%8B%E3%82%AB%E3%83%AB)によって提供されていますす。

![スクリーンショット 2022-06-22 20.02.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/43834474-12f5-4644-e3de-9675b5026cd3.png)

Ubuntuの場合、[Amazon EC2 AMI Locator](https://cloud-images.ubuntu.com/locator/ec2/)から専用のAMIについて調べることができます。
以下は上記のAMI ID `ami-0bf8833870a0f064c`で検索した例です。

![スクリーンショット 2022-06-22 20.09.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221759/61d55f86-117d-0c9e-0b03-e94d148d5b96.png)

# おわりに
ウォークスルーで使用した公式ドキュメントでは[ユーザーデータ](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/user-data.html)を渡し、インスタンス起動時に任意のスクリプトを実行して初期構築を実現しています。

実際に運用を考慮した考え方としてリソースの作成・削除が頻繁に行われる環境などではCDKに全てを組み込みAWSで一元管理する運用でも良いと思いますが、Ansibleなど構成管理ツールを使用している環境などでは、インフラの構成管理について検討が必要です。

クラウドファーストの今、クラウドインフラの資産をAWSに寄せるかAnsibleなどのIaCツールに寄せるかについては、インフラを利用する組織の環境や状況に応じて変わってくため、手段を目的化しないことが重要です。