AWS CDK で Infrastructure as Code する: EC2編



こんにちは。AWS勉強中の @masatomix です。
最近ずっとAWSのAWS CDKをいじってます。今回は、VPC上にEC2インスタンスを立てていきます！



## 前提

- [AWS CDK で Infrastructure as Code する: VPC編](https://qiita.com/masatomix/items/fc414e98d6d0091db90d) でVPCが構築できていること

## やってみる

作りたい環境はこんな感じ。

![v7I2tyC](https://imgur.com/FBvwjmt.png)


EC2につけるセキュリティグループの作成、EC2にログインするためのキーペアの作成、そしてEC2インスタンスを作成しています。インスタンスはよくあるAmazon Linuxのami (ami-00d101850e971728d)を指定しました。

作成したキーペア のダウンロードとか、そのへんものちほど説明していきます。

### CDKのソースコード

コードです。


```typescript:lib/BastionStack.ts
import { App, CfnParameter, Stack, StackProps } from 'aws-cdk-lib'
import {
  CfnInstance,
  CfnKeyPair,
  CfnSecurityGroup,
  CfnSecurityGroupIngress,
  CfnSubnet,
  CfnVPC,
} from 'aws-cdk-lib/aws-ec2'
import { Profile, getProfile } from './Utils'

export class BastionStack extends Stack {
  constructor(scope: App, id: string, vpc: CfnVPC, subnet: CfnSubnet, props?: StackProps) {
    super(scope, id, props)
    const p = getProfile(this)

    // SecurityGroupの作成
    const PublicSubnetEC2SecurityGroup = createSecurityGroup(this, vpc, p)
    // キーペアの作成
    const keyPair = new CfnKeyPair(this, 'KeyPair', {
      keyName: `${p.name}-KeyPair`,
    })
    // EC2インスタンスの作成
    createEC2(this, subnet, [PublicSubnetEC2SecurityGroup.ref], keyPair.ref, true, p)
  }
}

const createEC2 = (
  stack: Stack,
  subnet: CfnSubnet,
  groupSet: string[],
  keyName: string,
  associatePublicIpAddress: boolean,
  p: Profile,
): CfnInstance => {
  return new CfnInstance(stack, 'BastionEC2', {
    imageId: 'ami-00d101850e971728d',
    keyName,
    instanceType: 't2.micro',
    networkInterfaces: [
      {
        associatePublicIpAddress,
        deviceIndex: '0',
        subnetId: subnet.attrSubnetId,
        groupSet,
      },
    ],
    tags: [{ key: 'Name', value: `${p.name}-ec2` }],
  })
}

// セキュリティグループはsshのポート22だけ許可
const createSecurityGroup = (stack: Stack, vpc: CfnVPC, profile: Profile): CfnSecurityGroup => {
  const group = new CfnSecurityGroup(stack, 'PublicSubnetEC2SecurityGroup', {
    groupName: 'public-ec2-sg',
    groupDescription: 'Allow SSH access from MyIP',
    vpcId: vpc.attrVpcId,
    tags: [{ key: 'Name', value: `${profile.name}-public-sg` }],
  })

  new CfnSecurityGroupIngress(stack, 'PublicSubnetEC2SecurityGroupIngress000', {
    ipProtocol: '-1',
    groupId: group.ref,
    sourceSecurityGroupId: group.ref,
  })

  new CfnSecurityGroupIngress(stack, 'PublicSubnetEC2SecurityGroupIngress001', {
    ipProtocol: 'tcp',
    fromPort: 22,
    toPort: 22,
    groupId: group.ref,
    cidrIp: '0.0.0.0/0',
  })

  return group
}
```
各リソースを作るメイン部分の記述は以上です。

そして下記のコードでは、すでに作成済みのVPCのStackから、VPCの情報、Public Subnetの情報などを渡してもらっています。
参考: [別のスタックの値を参照したい](https://qiita.com/masatomix/items/7087e7813637ff5e9c36#%E5%88%A5%E3%81%AE%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%AE%E5%80%A4%E3%82%92%E5%8F%82%E7%85%A7%E3%81%97%E3%81%9F%E3%81%84)

```typescript:bin/cdk-samples.ts
#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { VPCStack } from '../lib/VPCStack'

const app = new cdk.App()

const vpcStack = new VPCStack(app, 'VPCStack')
new BastionStack(app, 'BastionStack', vpcStack.vpc, vpcStack.publicSubnets[0]) // 別Stackで作成した情報を利用
```

```json:cdk.json(主要な部分だけ)
{
  "app": "npx ts-node --prefer-ts-exts bin/cdk-samples.ts",
  ...
  "context": {
    ...
    "dev": {
      "name": "dev-20230815"
    }
  }
}
```

コード中出てくる ``${p.name}`` などは``dev-20230815``と置換されます。

### コードの説明つづき


キーペアを

```typescript
const keyPair = new CfnKeyPair(this, 'KeyPair', {
  keyName: `${p.name}-KeyPair`,
})
```

で新規作成し、 (Key名: ``${p.name}-KeyPair =dev-20230815-KeyPair``)、EC2インスタンスを作成するときにそのキーをセットしています。ちなみにインスタンスの名前は「``${p.name}-ec2=dev-20230815-ec2``」です。
またセキュリティグループは、自身にはフルアクセス、外からのアクセスについてssh(ポート22) だけ許可、という設定をしています。



### 実行してみる

さてCDKを実行してEC2インスタンスを立ててみましょう。

```console
$ yarn cdk deploy BastionStack
... しばらくかかります
$
```

終わったようです。ちゃんとできているかsshでアクセスしてみようと思います。

そのためにはキー情報を取得したいのですが、まず上記のCDKとかCloudFormationで作成したキーペアは「[AWS Systems Manager >> パラメータストア](https://ap-northeast-1.console.aws.amazon.com/systems-manager/parameters/?region=ap-northeast-1&tab=Table)」ってところで管理されています。

![image-20230815130316373](https://imgur.com/VYUARKx.png)

そして、この情報は下記のコマンドで取得する事ができます。

```console
$ keyName=dev-20230815-KeyPair
$ KeyPairId=`aws ec2 describe-key-pairs --key-names ${keyName} \
    --query "KeyPairs[*].[KeyPairId]" --output text`

$ echo ${KeyPairId}
key-0997a00cxxxxxx // Web画面にも表示されていたID

$ aws ssm get-parameter --name /ec2/keypair/${KeyPairId} \
 --with-decryption --query Parameter.Value --output text

-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAtbAyIJ4opIOjMiq8vNv12KoERyPX7Ao2hGwuZL3hm14w2nuj
YZcn0MAlHn71DdiBLNkqOiy75PsTAE1iuUDr8akPF+O6AGD8luYI6veQ3dAFkL/N
xbkhw3h2dr0e79LMWf58RVvzfq+Vo0fN1ICCpqDiPavpcIEgEoDa32m8AT2v0n1r
zqcMr/wv4RwUAwyF83svozNQPCgx...Bjx0/Oc9ULb2O+L2A==
-----END RSA PRIVATE KEY-----
$
```

生成した秘密鍵の情報が取得できました。この情報をもとにEC2へアクセスできるはずですね。やってみましょう。

あ、そのまえにこの秘密鍵はファイルに出力しておきます。

```console
$ aws ssm get-parameter --name /ec2/keypair/${KeyPairId} \
  --with-decryption --query Parameter.Value --output text > ~/.ssh/${keyName}.pem

$ ls -lrt ~/.ssh/${keyName}.pem
-rw-r--r-- 1 sysmgr sysmgr 1675 Aug 15 13:33 /xxxx/.ssh/dev-20230815-KeyPair.pem

permission を切り替えます
$ chmod 400  ~/.ssh/${keyName}.pem 
$
```

つづいてアクセスするEC2インスタンスのPublic IPアドレスを確認します。

```console
$ EC2Name=dev-20230815-ec2
$ aws ec2 describe-instances \
 --query "Reservations[*].Instances[*].[(Tags[?Key=='Name'])[0].Value,PublicIpAddress]" \
 --filters "Name=tag:Name,Values=${EC2Name}" \
 --output table
 
---------------------------------------
|          DescribeInstances          |
+-------------------+-----------------+
|  dev-20230815-ec2 |  43.207.79.102  |
+-------------------+-----------------+
$
```

情報が整理できたのでようやく、sshアクセスしてみます。

```console
$ ssh -i ~/.ssh/${keyName}.pem ec2-user@43.207.79.102

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
61 package(s) needed for security, out of 144 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-192-168-0-140 ~]$ 
```

できましたー。お疲れさまでした。

最後、いらないインスタンスは削除しておきましょう。

```console
$ yarn cdk destroy BastionStack
Are you sure you want to delete: BastionStack (y/n)? y

BastionStack: destroying... [1/1]
1:37:25 PM | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack     | BastionStack
1:37:28 PM | DELETE_IN_PROGRESS   | AWS::EC2::Instance             | BastionEC2
...
 ✅  BastionStack: destroyed
Done in 66.30s.
$ 
```

おつかれさまでしたー。




### 蛇足1: すでにAWS上にあるキーペアを使いたい

先ほどはキーペアを新規作成して利用しましたが、すでにAWS上に存在する既存のキーを使う場合は

```typescript
    const keyPair = new CfnKeyPair(this, 'KeyPair', {
      keyName: `${p.name}-KeyPair`,
    })
    createEC2(this, subnet, [PublicSubnetEC2SecurityGroup.ref], keyPair.ref, true, p)
```

このキー生成のコードはコメントアウトし、そのままキー名を渡してあげればOKです。

```typescript
 createEC2(this, subnet, [PublicSubnetEC2SecurityGroup.ref], 'キー名', true, p)
```

とすればOKです。

### 蛇足2: 手元にあるキーペアを使いたい

AWS上にはないけど、キーペア自体は手元にある場合は、そのキーペアをAWSに登録するために、下記の``publicKeyMaterial``オプションを使います。

```typescript
const keyPair = new CfnKeyPair(this, 'KeyPair', {
  keyName: `${p.name}-KeyPair`,
  publicKeyMaterial:`ssh-rsa AAAAB...mdnuYU+1crQR+aWjpcFTpRvjj6DipVjPle3l8k3iWGiDfpOnYfQF0O0KTXzOP/yE/BdAA8iN`
})
```

このように手元のキーペアの「公開鍵」の情報を渡してあげればOKです。ただしこのデータは先の「[AWS Systems Manager >> パラメータストア](https://ap-northeast-1.console.aws.amazon.com/systems-manager/parameters/?region=ap-northeast-1&tab=Table)」には入らないようで[^1]、したがって、このあとコマンドからキー情報(秘密鍵情報)を取得することはできませんでした。

さらにさらに蛇足なんですが手元に秘密鍵ファイル(*.pem) しかないな、ってときに公開鍵の情報が欲しくなるときがたまにありますが、下記のコマンドで公開鍵ファイルを作成できます！

```console
$ ssh-keygen -y -f ~/.ssh/${keyName}.pem
ssh-rsa AAAAB3NzaC1yc....P3UD2X0lKYedsENogqa0RzZ
$
```


[^1]: 公開鍵しか渡していないから当然か

## まとめ

今回は既存のVPCにEC2インスタンスを構築しました。ECSなどの台頭で徐々に本番運用では使わなくなってきているかもしれないEC2ですが、インターネットに公開していないPrivate Subnetに配置したリソースにアクセスするときなど、Bastion[^2]な用途としてほしいときがあったりします。
Cfnなどがあるとそんなときにサクッと構築できて便利ですねー。

[^2]: 要塞とかいうみたいです。



## 関連リンク

- [TerraformにおけるEC2キーペアのインポートエラーの対処方法](https://dev.classmethod.jp/articles/terraform-ec2-keypair-import-error/) キーペアをインポートする際、秘密鍵をあげちゃってたって話。自分もやったw
- [AWS Cli自分用Tips](https://qiita.com/takachan/items/421928dc61c51af97fb1)
- [AWS CloudFormationでEC2キーペアを作成する](https://qiita.com/tsukamoto/items/1e0f3c8ecf4cba5cf485) パラメタストアってのを初めて知りました。
- [CloudFormationで作成したキーペアをダウンロードせずにSSHログインするスクリプトを作りました](https://blog.serverworks.co.jp/how-to-connect-ec2-with-ssm-keypair-string) ssh-agentを使ってみる件
- [AWS CDKのTIPS集](https://qiita.com/masatomix/items/7087e7813637ff5e9c36)
- [AWS CDK で Infrastructure as Code する: VPC編](https://qiita.com/masatomix/items/fc414e98d6d0091db90d)
- https://github.com/masatomix/cdk-samples/tree/vpc_base ソースコード。前提の記事をやれば取得できますが、一応リンクだけ(今回分の反映は未済)