「Amazon Web Services基礎からのネットワーク&サーバー構築」の環境をCDK(Typescript)で作成してみた
はじめに
本記事ではXで話題になっていたAmazon Web Services基礎からのネットワーク&サーバー構築改訂4版の復習とCDK入門を兼ねて、コンソール画面から構築するネットワークやサーバーをCDKを使って構築してみました。
前提条件
AWSのアカウントの作成、AWS configの設定、AWS CLI
とnpm
のインストールが完了していること。
AWS CDK Toolkitのインストール
AWS CDK Toolkit は npm 経由でインストールします。
npm install -g aws-cdk
npm install -g typescript ts-node
CDK プロジェクト作成
プロジェクトディレクトリの作成
空のディレクトリを作成し、カレントディレクトリを変更します。
本記事ではディレクトリ名をaws-basic-network-and-server-build-cdk
とします。
mkdir ~/aws-basic-network-and-server-build-cdk && cd ~/aws-basic-network-and-server-build-cdk
cdk init
新しい TypeScript CDK プロジェクトを作成するために cdk init
を使います。
cdk init sample-app --language typescript
プロジェクトの構造
プロジェクトのディレクトリ・ファイル構成は下記のようになります。
これ以降ではlib/aws-basic-network-and-server-build-cdk.tsを修正していきます。
cdk-workshop/
├── bin/
│ └── aws-basic-network-and-server-build-cdk.ts
├── lib/
│ └── aws-basic-network-and-server-build-cdk-stack.ts
├── node_modules/
├── test/
│ └── aws-basic-network-and-server-build-cdk.test.ts
├── .git/
├── cdk.json
├── jest.config.js
├── package.json
├── README.md
├── tsconfig.json
├── .gitignore
└── .npmignore
ネットワークの構成図
CDKで作成するネットワークの構成は下記のようになります。サブネットのCIDRブロックが異なっている点に注意してください。このようにサブネットがなっている点については、VPC・サブネットの作成で述べます。
VPC・サブネットの作成
まず初めにCHAPTER2、6、7で扱われるVPC、サブネット、NATゲートウェイを作成します。
コンソール画面から作成する場合はインターネットゲートウェイやNATゲートウェイについてルーティングテーブルを設定する必要がありますが、CDKでは下記のように少ないコード量でパブリックサブネットとプライベートサブネットが作成できることが分かります。
サブネットのCIDRブロックについてですが、SubnetConfiguration
のProperties
にはCIDRブロックがなく、cidrMask
で設定するため、各サブネットに特定のCIDRブロックを設定することはできないです。CDKではサブネットのCIDRは小さい値から付与されるため、下図のようにパブリックサブネットが10.0.0.0/24
、プライベートサブネットが10.1.0.0/24
となります。具体的に指定したい場合はこの記事を参考にしてみてください。本記事では割愛します。
import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'
import { Construct } from 'constructs';
// ec2 に関するパッケージを import
import * as ec2 from 'aws-cdk-lib/aws-ec2';
export class AwsBasicNetworkAndServerBuildCdkStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// VPC の作成
const vpc = new ec2.Vpc(this, 'VPC', {
vpcName: 'VPC',
maxAzs: 1, // アベイラビリティーゾーンは1つ
createInternetGateway: true,
natGateways: 1,
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
// サブネットの設定
subnetConfiguration: [
{
cidrMask: 24,
name: 'パブリックサブネット',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'プライベートサブネット',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
],
});
}
}
作成されたサブネットのCIDRブロック
WEBサーバーの作成
続いてWEBサーバーを作成します。
WEBサーバーを作成するコードは下記のようになります。
キーペアについてはコンソール画面から作成したものを使用しています。後ほど、キーペアをCDKで作成する場合について述べます。
WEBサーバーのセキュリティグループはHTTPのためのポート80番、SSH接続のためのポート22番についてはどのIPからも接続を許可しています。
キーペアについてはコンソール画面で作成した名前をfromKeyPairName
の3つ目の引数に入れます。
// コンソール画面から作成しているEC2のキーペア(my-key)を取得
const keyPair = ec2.KeyPair.fromKeyPairName(this, 'KeyPair', 'my-key');
// EC2 インスタンス(Web Server)の作成
// Web Serverのセキュリティグループを作成
const webServerSecurityGroup = new ec2.SecurityGroup(this, 'WebServerSecurityGroup', {
vpc: vpc,
securityGroupName: 'WEB-SG',
});
// Web Serverのセキュリティグループにインバウンドルールを追加
webServerSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP traffic from anywhere');
webServerSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow SSH traffic from anywhere');
// Web Serverのインスタンスを作成
const webServer = new ec2.Instance(this, 'WebServer', {
instanceName: 'WEBサーバー',
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023 }),
vpc: vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PUBLIC, // パブリックサブネットを指定
},
securityGroup: webServerSecurityGroup,
keyPair: keyPair,
});
DBサーバーの作成
続いてDBサーバーを作成します。
DBサーバーを作成するコードは下記のようになります。
WEBサーバーと異なる部分は2点です。
- 1点目はサブネットにプライベートを指定する。
- 2点目はセキュリティグループの設定です。DBサーバーのセキュリティグループではWEBサーバーからのSSH接続のためのポート22番、MariaDBの通信のためのポート3306番を開け、pingコマンドで疎通する際に使用するICMPプロトコルを許可するようにします。
addIngressRule
の第一引数をWEBサーバーのセキュリティグループにすることでWEBサーバーからの接続のみを許可する形になります。
// EC2 インスタンス(DB server)の作成
// DB serverのセキュリティグループを作成
const dbServerSecurityGroup = new ec2.SecurityGroup(this, 'DBServerSecurityGroup', {
vpc: vpc,
securityGroupName: 'DB-SG',
});
// DB serverのセキュリティグループにインバウンドルールを追加
// MariaDB(MySQL)のポートを開放(Web Serverからのアクセスのみを許可)
dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.tcp(3306), 'Allow MySQL traffic from Web Server');
// SSHのポートを開放(Web Serverからのアクセスのみを許可)
dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.tcp(22), 'Allow SSH traffic from Web Server');
// ICMPのポートを開放(Web Serverからのアクセスのみを許可)
dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.icmpPing(), 'Allow ICMP traffic from Web Server');
// DB serverのインスタンスを作成
const dbServer = new ec2.Instance(this, 'DBServer', {
instanceName: 'DBサーバー',
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023 }),
vpc: vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, // プライベートサブネットを指定
},
securityGroup: dbServerSecurityGroup,
keyPair: keyPair,
});
キーペアをCDKで作成
キーペアは下記のように作成することができます。
また、秘密鍵の中身はパラメータストアに保存されます。Stack出力のGetSSHKeyCommandの値はコマンドになっており、コマンドを実行することで秘密鍵の情報を取得することができます。
// キーペア作成
const cfnKeyPair = new ec2.CfnKeyPair(this, 'CfnKeyPair', {
keyName: 'key-pair-by-cdk',
})
cfnKeyPair.applyRemovalPolicy(RemovalPolicy.DESTROY)
// キーペア取得コマンドアウトプット
new CfnOutput(this, 'GetSSHKeyCommand', {
value: `aws ssm get-parameter --name /ec2/keypair/${cfnKeyPair.getAtt('KeyPairId')} --region ${this.region} --with-decryption --query Parameter.Value --output text`,
})
EC2サーバーインスタンスのコンストラクトを定義
AWSのワークショップを参考にEC2サーバーインスタンスのコンストラクトを定義し、スタックファイルから切り出します。
下記のようにlib/constructs/ec2-server-instance.ts
を作成します。
Construct props に vpc
instanceName
subnetType
securityGroup
keyName
を渡すように設計しました。
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';
// Construct props を定義
export interface EC2ServerInstanceProps {
readonly vpc: ec2.IVpc
readonly instanceName: string
readonly subnetType: ec2.SubnetType
readonly securityGroup: ec2.ISecurityGroup
readonly keyName: string
}
// EC2 インスタンスを含む Construct を定義
export class EC2ServerInstance extends Construct {
// 外部からインスタンスへアクセスできるように設定
public readonly instance: ec2.Instance;
constructor(scope: Construct, id: string, props: EC2ServerInstanceProps) {
super(scope, id);
// Construct props から vpc, instanceName, subnetType, securityGroup, keyName を取り出す
const { vpc, instanceName, subnetType, securityGroup, keyName } = props;
const instance = new ec2.Instance(this, "Instance", {
instanceName,
vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2023 }),
vpcSubnets: { subnetType },
securityGroup,
keyName,
});
// 作成した EC2 インスタンスをプロパティに設定
this.instance = instance;
}
}
まとめ
キーペアをCDKで作成、EC2サーバーのコンストラクタを定義して別ファイルにした場合のStack全体のコードは下記のようになります。
import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'
import { Construct } from 'constructs';
// ec2 に関するパッケージを import
import * as ec2 from 'aws-cdk-lib/aws-ec2';
// 自作コンストラクトを import
import { EC2ServerInstance } from './constructs/ec2-server-instance';
export class AwsBasicNetworkAndServerBuildCdkStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// VPC の作成
const vpc = new ec2.Vpc(this, 'VPC', {
vpcName: 'VPC',
maxAzs: 1,
createInternetGateway: true,
natGateways: 1,
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
// サブネットの設定
subnetConfiguration: [
{
cidrMask: 24,
name: 'パブリックサブネット',
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: 'プライベートサブネット',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
],
});
// キーペア作成
const cfnKeyPair = new ec2.CfnKeyPair(this, 'CfnKeyPair', {
keyName: 'key-pair-by-cdk',
})
cfnKeyPair.applyRemovalPolicy(RemovalPolicy.DESTROY)
// キーペア取得コマンドアウトプット
new CfnOutput(this, 'GetSSHKeyCommand', {
value: `aws ssm get-parameter --name /ec2/keypair/${cfnKeyPair.getAtt('KeyPairId')} --region ${this.region} --with-decryption --query Parameter.Value --output text`,
})
// Web Serverのセキュリティグループを作成
const webServerSecurityGroup = new ec2.SecurityGroup(this, 'WebServerSecurityGroup', {
vpc: vpc,
securityGroupName: 'WEB-SG',
});
// Web Serverのセキュリティグループにインバウンドルールを追加
webServerSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP traffic from anywhere');
webServerSecurityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow SSH traffic from anywhere');
// DB serverのセキュリティグループを作成
const dbServerSecurityGroup = new ec2.SecurityGroup(this, 'DBServerSecurityGroup', {
vpc: vpc,
securityGroupName: 'DB-SG',
});
// DB serverのセキュリティグループにインバウンドルールを追加
// MariaDB(MySQL)のポートを開放(Web Serverからのアクセスのみを許可)
dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.tcp(3306), 'Allow MySQL traffic from Web Server');
// SSHのポートを開放(Web Serverからのアクセスのみを許可)
dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.tcp(22), 'Allow SSH traffic from Web Server');
// ICMPのポートを開放(Web Serverからのアクセスのみを許可)
dbServerSecurityGroup.addIngressRule(webServerSecurityGroup, ec2.Port.icmpPing(), 'Allow ICMP traffic from Web Server');
// Web Serverのインスタンスを作成
new EC2ServerInstance(this, 'WebServer', {
vpc: vpc,
instanceName: 'WEBサーバー',
subnetType: ec2.SubnetType.PUBLIC,
securityGroup: webServerSecurityGroup,
keyName: cfnKeyPair.keyName,
});
// DB serverのインスタンスを作成
new EC2ServerInstance(this, 'DBServer', {
vpc: vpc,
instanceName: 'DBサーバー',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
securityGroup: dbServerSecurityGroup,
keyName: cfnKeyPair.keyName,
});
}
}
参考資料
コード
全体コードは以下のリポジトリにあります。
Discussion