LoginSignup
3
2

AWS CDK で Infrastructure as Code する: EC2編

Last updated at Posted at 2023-08-15

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

前提

やってみる

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

v7I2tyC

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

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

CDKのソースコード

コードです。

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の情報などを渡してもらっています。
参考: 別のスタックの値を参照したい

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で作成した情報を利用
cdk.json(主要な部分だけ)
{
  "app": "npx ts-node --prefer-ts-exts bin/cdk-samples.ts",
  ...
  "context": {
    ...
    "dev": {
      "name": "dev-20230815"
    }
  }
}

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

コードの説明つづき

キーペアを

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インスタンスを立ててみましょう。

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

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

そのためにはキー情報を取得したいのですが、まず上記のCDKとかCloudFormationで作成したキーペアは「AWS Systems Manager >> パラメータストア」ってところで管理されています。

image-20230815130316373

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

$ 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へアクセスできるはずですね。やってみましょう。

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

$ 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アドレスを確認します。

$ 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アクセスしてみます。

$ 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 ~]$ 

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

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

$ 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上に存在する既存のキーを使う場合は

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

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

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

とすればOKです。

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

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

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

このように手元のキーペアの「公開鍵」の情報を渡してあげればOKです。ただしこのデータは先の「AWS Systems Manager >> パラメータストア」には入らないようで1、したがって、このあとコマンドからキー情報(秘密鍵情報)を取得することはできませんでした。

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

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

まとめ

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

関連リンク

  1. 公開鍵しか渡していないから当然か

  2. 要塞とかいうみたいです。

3
2
1

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
3
2