ALB,EC2,RDS基本構成をAWS CDKでデプロイする手順を記載します。VPC含め新規で作成することを想定し、デプロイ時にはEC2に必要なパッケージをインストールできるようにします。
個人的な構築メモとして記載しているので、参照される場合はリソース名など修正をお願いいたします。おまけにはcdk運用時に考慮すべきと感じたことも書いてみました。
前提
- AWS CDK実行環境が整備されていること (下記コマンドが実行可能な環境。ちなみに私はMACローカル)
node -v
npm -v
aws --version
(構築に必要な権限を持つ、AWS認証情報を設定済みであること)
cdk --version
- AWS CDKとの親和性を考慮しTypeScriptで実装
構成
本記事でデプロイする構成イメージ図です。実装後はEC2にはセッションマネージャーからログインする形になります。またSecrets ManagerにAuroraのadmin用シークレットを作成したりします。
下記要件になりますが、必要に応じて後述のtsファイル内のパラメーターを書き換えてください。
【前提】
・リージョン:ap-northeast-1(東京)
・RemovalPolicy は検証用のため基本 DESTROY (cdk destroyでAurora含め全部消えます)
* すべてのリソース名の先頭に "Araki" と付いているので、もし参照される方は置換して変えてください
【VPC】
・CIDR: 172.16.0.0/16
・マルチAZ (2AZ)
・Public Subnet ×2 (/24)
・Private Subnet ×2 (/24)
・NAT Gateway ×2(各AZ)
・Internet Gatewayあり
【EC2(Webサーバ)】
・Private Subnetに配置
・インスタンスタイプ: t3.small
・Amazon Linux 最新
・SSMセッションマネージャーで接続(踏み台なし)
・IAMロールに AmazonSSMManagedInstanceCore を付与
・起動時に user-data.sh を実行
- lib/resources/user-data.sh を読み込む
- yum update -y
- httpd インストール&起動
・ポート80で待受
【ALB】
・Public Subnetに配置
・HTTP(80) リスナー
・ターゲット: WebEC2
・ヘルスチェックパス: ```
【Aurora MySQL】
・Engine: aurora-mysql
・EngineVersion: 3.11.1 系
・インスタンスタイプ: db.t4g.medium
・Writer ×1
・Reader ×1(Multi-AZ)
・Private Subnetに配置
・DeletionProtection: false
・RemovalPolicy: DESTROY
・資格情報は自動生成
SecurityGroup】
・ALB → 80 を許可
・WebEC2 → 3306 でAuroraへ接続許可
・EC2はアウトバウンド許可
【VPC Endpoint】
・SSM
・EC2 Messages
・SSM Messages
・Private Subnetに作成
【Output】
・ALB DNS名
・EC2 InstanceId
・Aurora Cluster Endpoint
・Aurora Secret名
実装
1. 前準備
ターミナル等で下記コマンドを実行します
※ cdkコマンドリファレンス
aws sts get-caller-identity
aws configure get region
# 上記コマンドの情報をもとにブートストラップ実行(AWS CDKを利用するための初期化)
cdk bootstrap aws://<アカウントNoを記載>/<リージョンを記載>
アカウントとリージョンが変更になる場合は都度ブートストラップします
# プロジェクト用ディレクトリ作成
mkdir araki-cdk-app && cd araki-cdk-app
# アプリを作成 (cdk実行に必要なファイルが配下に作成される)
cdk init app --language typescript
2. ファイル作成
この時点で配下にlibフォルダが作成されています。lib/araki-cdk-app-stack.ts を下記に修正します
import * as path from "path";
import * as fs from "fs";
import { Stack, StackProps, RemovalPolicy, CfnOutput } 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";
import * as rds from "aws-cdk-lib/aws-rds";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as targets from "aws-cdk-lib/aws-elasticloadbalancingv2-targets";
export class ArakiCdkAppStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// ====================================================
// VPC: 172.16.0.0/16 / Public&Private x2AZ /24 / NAT x2
// ====================================================
const vpc = new ec2.Vpc(this, "ArakiVpc", {
ipAddresses: ec2.IpAddresses.cidr("172.16.0.0/16"),
maxAzs: 2,
natGateways: 2,
subnetConfiguration: [
{
name: "ArakiPublic",
subnetType: ec2.SubnetType.PUBLIC,
cidrMask: 24,
},
{
name: "ArakiPrivate",
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
cidrMask: 24,
},
],
});
// ====================================================
// SSM VPC Endpoints
// ====================================================
vpc.addInterfaceEndpoint("ArakiSsmEndpoint", {
service: ec2.InterfaceVpcEndpointAwsService.SSM,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
privateDnsEnabled: true,
});
vpc.addInterfaceEndpoint("ArakiEc2MessagesEndpoint", {
service: ec2.InterfaceVpcEndpointAwsService.EC2_MESSAGES,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
privateDnsEnabled: true,
});
vpc.addInterfaceEndpoint("ArakiSsmMessagesEndpoint", {
service: ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
privateDnsEnabled: true,
});
// ====================================================
// Security Groups
// ====================================================
const albSg = new ec2.SecurityGroup(this, "ArakiAlbSg", {
vpc,
description: "ALB SG",
allowAllOutbound: true,
});
albSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), "HTTP from Internet");
const webSg = new ec2.SecurityGroup(this, "ArakiWebSg", {
vpc,
description: "Web EC2 SG",
allowAllOutbound: true,
});
webSg.addIngressRule(albSg, ec2.Port.tcp(80), "HTTP from ALB");
const dbSg = new ec2.SecurityGroup(this, "ArakiDbSg", {
vpc,
description: "Aurora SG",
allowAllOutbound: true,
});
dbSg.addIngressRule(webSg, ec2.Port.tcp(3306), "MySQL from Web EC2");
// ====================================================
// IAM Role for EC2 (SSM)
// ====================================================
const webRole = new iam.Role(this, "ArakiWebEc2Role", {
assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
});
webRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")
);
// ====================================================
// UserData from file: lib/resources/user-data.sh
// ====================================================
const userDataPath = path.join(__dirname, "resources", "user-data.sh");
const userDataScript = fs.readFileSync(userDataPath, "utf8");
// ====================================================
// EC2 (Private subnet)
// ====================================================
const webInstance = new ec2.Instance(this, "ArakiWebEc2", {
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
instanceType: new ec2.InstanceType("t3.small"),
machineImage: ec2.MachineImage.latestAmazonLinux2023(),
securityGroup: webSg,
role: webRole,
});
webInstance.addUserData(userDataScript);
// ====================================================
// ALB (HTTP:80)
// ====================================================
const alb = new elbv2.ApplicationLoadBalancer(this, "ArakiAlb", {
vpc,
internetFacing: true,
securityGroup: albSg,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
});
const listener = alb.addListener("ArakiHttpListener", {
port: 80,
open: false,
});
listener.addTargets("ArakiWebTargets", {
port: 80,
targets: [new targets.InstanceTarget(webInstance, 80)],
healthCheck: { path: "/" },
});
// ====================================================
// Aurora MySQL(Writer/Reader: db.t4g.medium)
// ====================================================
const auroraInstanceType = ec2.InstanceType.of(
ec2.InstanceClass.T4G,
ec2.InstanceSize.MEDIUM
);
const cluster = new rds.DatabaseCluster(this, "ArakiAuroraCluster", {
engine: rds.DatabaseClusterEngine.auroraMysql({
version: rds.AuroraMysqlEngineVersion.VER_3_11_1,
}),
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
securityGroups: [dbSg],
credentials: rds.Credentials.fromGeneratedSecret("admin"),
writer: rds.ClusterInstance.provisioned("ArakiWriter", {
instanceType: auroraInstanceType,
}),
readers: [
rds.ClusterInstance.provisioned("ArakiReader1", {
instanceType: auroraInstanceType,
}),
],
removalPolicy: RemovalPolicy.DESTROY,
deletionProtection: false,
});
// ====================================================
// Outputs
// ====================================================
new CfnOutput(this, "ArakiAlbDnsName", { value: alb.loadBalancerDnsName });
new CfnOutput(this, "ArakiWebEc2InstanceId", { value: webInstance.instanceId });
new CfnOutput(this, "ArakiAuroraClusterEndpoint", {
value: cluster.clusterEndpoint.hostname,
});
new CfnOutput(this, "ArakiAuroraSecretName", {
value: cluster.secret?.secretName ?? "N/A",
});
}
}
lib配下にresourcesという名称でディレクトリを作成し、lib/resources/user-data.sh というファイルを作成します。そのファイル内に以下記載します。
EC2起動時にユーザデータを利用して、EC2内で下記コマンド実行しパッケージインストール等をします。
#!/bin/bash
set -euxo pipefail
yum update -y
yum install -y httpd
systemctl enable httpd
echo "hello from ArakiWebEc2" > /var/www/html/index.html
systemctl start httpd
3. デプロイ
下記コマンドを実行します。すみませんタイトルに超速構築とありますが、RDSもあるので全体で15分くらいはかかります。
# CFnテンプレートを生成
cdk synth
# diffを確認 (terrafrom planみたいなもの)
cdk diff
# スタックを作成
cdk deploy
不要になったらcdk destroyで削除します。
4. デプロイ後の動作チェック
- ALB疎通
- 出力された ArakiAlbDnsName を開く
- もし 503/ターゲットunhealthy なら、EC2側でHTTPサーバが起動してない
- SSMログイン
- EC2のコンソール → “接続” → “セッションマネージャー”
- ログ見るなら /var/log/cloud-init-output.log とか
- Aurora接続(必要なら)
- Secrets Manager に admin のシークレットができてる
- WebEC2から mysql クライアント入れて疎通(テスト用途)
おまけ (cdk destoroyについて)
cdk destroyでは削除されないリソースもあるので運用の際は考慮が必要です。
destroyで消えにくい/残りがちな代表例
- RDS / Aurora(削除保護・RemovalPolicy・Snapshot周り)
- S3(中身があると消せないことが多い。autoDeleteObjects設定が必要なケース)
- CloudWatch Logs(ロググループが残る運用も多い)
- ECR(イメージ残ってると削除に工夫が必要)
- IAM(ロールに依存があると残ることがある)
CDK利用を「安全側」に倒すことが多いと思うので、検証はDESTROYで割り切り、本番は RETAIN(またはスナップショット)+削除手順を別管理、が現実的かもしれません。
