0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS CDKでALB,EC2,RDSの検証用環境を超速構築 (TypeScript)

0
Last updated at Posted at 2026-02-18

 ALB,EC2,RDS基本構成をAWS CDKでデプロイする手順を記載します。VPC含め新規で作成することを想定し、デプロイ時にはEC2に必要なパッケージをインストールできるようにします。
 個人的な構築メモとして記載しているので、参照される場合はリソース名など修正をお願いいたします。おまけにはcdk運用時に考慮すべきと感じたことも書いてみました。

AWS CDKとは

前提

  • AWS CDK実行環境が整備されていること (下記コマンドが実行可能な環境。ちなみに私はMACローカル)
node -v
npm -v
aws --version
(構築に必要な権限を持つ、AWS認証情報を設定済みであること)
cdk --version
  • AWS CDKとの親和性を考慮しTypeScriptで実装

構成

 本記事でデプロイする構成イメージ図です。実装後はEC2にはセッションマネージャーからログインする形になります。またSecrets ManagerにAuroraのadmin用シークレットを作成したりします。

ChatGPT Image 2026年2月18日 18_08_06.png

下記要件になりますが、必要に応じて後述の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. デプロイ後の動作チェック

  1. ALB疎通
    • 出力された ArakiAlbDnsName を開く
    • もし 503/ターゲットunhealthy なら、EC2側でHTTPサーバが起動してない
  2. SSMログイン
    • EC2のコンソール → “接続” → “セッションマネージャー”
    • ログ見るなら /var/log/cloud-init-output.log とか
  3. Aurora接続(必要なら)
    • Secrets Manager に admin のシークレットができてる
    • WebEC2から mysql クライアント入れて疎通(テスト用途)

おまけ (cdk destoroyについて)

cdk destroyでは削除されないリソースもあるので運用の際は考慮が必要です。

destroyで消えにくい/残りがちな代表例

  • RDS / Aurora(削除保護・RemovalPolicy・Snapshot周り)
  • S3(中身があると消せないことが多い。autoDeleteObjects設定が必要なケース)
  • CloudWatch Logs(ロググループが残る運用も多い)
  • ECR(イメージ残ってると削除に工夫が必要)
  • IAM(ロールに依存があると残ることがある)

CDK利用を「安全側」に倒すことが多いと思うので、検証はDESTROYで割り切り、本番は RETAIN(またはスナップショット)+削除手順を別管理、が現実的かもしれません。

0
1
0

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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?