【AWS CDK】5分でできるALB-EC2-RDSの構成

# はじめに
AWS CDKのキャッチアップがてら、基本的なWeb3層アーキテクチャをaws-cdkで実装してみました。
備忘がてら記録に残します！

ここでは、CDKの基本的な事項についての説明は避けます。
CDKに初めて触れる人にとっては、以下の記事がよくまとまっているのでおすすめです。

https://qiita.com/Brutus/items/6c8d9bfaab7af53d154a

# 開発環境
IDEはVSCodeで、これなしではもう生きていけない**Github Copilot**をゴリゴリ使用して実装していきました。

> - CDK : 2.72.1 (build ddbfac7)
> - TypeScript : typescript@4.9.5
> - Github copilot

# 構成図

![スクリーンショット 2023-04-03 23.39.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669969/7933b834-af53-bb6b-7bdc-2a539de121c5.png)

【ざっくり要件】
- 踏み台サーバーを設置（SSHとSGの設定練習）
- Web-EC2へはSSMを使用してログイン
- RDSのuser, passwordはsecret managerに登録
- `EC2→RDS`へのアクセスにはVPC Interface Endpointを使用
    - EC2→インターネットへの疎通確認
    - EC2→MySQL for RDSへの疎通確認
    - 上記ができればOKとする


# 手順
## cdk init
```bash
mkdir ec2-cdk && cd ec2-cdk
cdk init app --language typescript

# 使用するlibのinstallは各自必要なものを行ってください
# ↓のような形式で複数指定
npm i @aws-cdk/aws-ec2 @aws-cdk/aws-iam 
```

## 完成したStack
先に完成物を載せます。
githubのリポジトリにも上がっているのでご参照ください。

【githubリポジトリ】
https://github.com/parupappa/web-app-cdk-stack.git

```ts:lib/ec2-cdk-stack.ts
import * as cdk from '@aws-cdk/core';
import * as ec2 from "@aws-cdk/aws-ec2"; // Allows working with EC2 and VPC resources
import * as iam from "@aws-cdk/aws-iam"; // Allows working with IAM resources
import * as rds from "@aws-cdk/aws-rds"; // Allows working with RDS resources
import * as elbv2 from "@aws-cdk/aws-elasticloadbalancingv2"; // Allows working with ALB resources
import * as elbv2tg from "@aws-cdk/aws-elasticloadbalancingv2-targets"; // Allows working with ALB target groups

export class Ec2CdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    // VPC
    const vpc = new ec2.Vpc(this, "Web-VPC", {
      cidr: "10.0.0.0/16",
    });
    // VPC Endpoint
    new ec2.InterfaceVpcEndpoint(this, "ssm_endpoint", {
      vpc: vpc,
      service: new ec2.InterfaceVpcEndpointAwsService("ssm")
    });
    new ec2.InterfaceVpcEndpoint(this, "ssmmessage_endpoint", {
      vpc: vpc,
      service: new ec2.InterfaceVpcEndpointAwsService("ssmmessages")
    });

    // Create a key pair to be used with this EC2 Instance
    const key = new ec2.CfnKeyPair(this, "CfnKeyPair", {
      keyName: 'ec2-key-pair',
    });
    // Delete the key pair when the stack is deleted
    key.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
    // Output the command to get the private key
    new cdk.CfnOutput(this, 'GetSSHKeyCommand', {
      value: `aws ssm get-parameter --name /ec2/keypair/${key.getAtt('KeyPairId')} --region ${this.region} --with-decryption --query Parameter.Value --output text`,
    })
    
    // Security group for the EC2 instance
    const securityGroup = new ec2.SecurityGroup(this, "SecurityGroup", {
      vpc,
      description: "Allow SSH (TCP port 22) and HTTP (TCP port 80) in",
      allowAllOutbound: true,
    });

    // Allow SSH access on port tcp/22
    securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), "Allow SSH Access");
    // Allow HTTP access on port tcp/80
    securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80), "Allow HTTP Access");

    // IAM role to allow access to other AWS services
    const role = new iam.Role(this, "ec2Role", { assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), });
    // IAM policy attachment to allow access to 
    role.addManagedPolicy( iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"));

    // Look up the AMI Id for the Amazon Linux 2 Image with CPU Type X86_64
    const ami = new ec2.AmazonLinuxImage({
      generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
      cpuType: ec2.AmazonLinuxCpuType.X86_64,
    });

    // Create the EC2 instance using the Security Group, AMI, and KeyPair defined.
    const bation_ec2 = new ec2.Instance(this, "BationEC2", {
      vpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      machineImage: ami,
      securityGroup: securityGroup,
      keyName: key.keyName,
      role: role,
    });

    // Elasitc IP for the EC2 instance
    const eip = new ec2.CfnEIP(this, "EIP")
    // Attach the Elastic IP to the EC2 instance
    new ec2.CfnEIPAssociation(this, "EIPAssociation", {
      allocationId: eip.attrAllocationId,
      instanceId: bation_ec2.instanceId,
    });

    //////////////////////////////////////////////////////////////////
    // ALB to EC2 to RDS Connection
    //////////////////////////////////////////////////////////////////

    // set instance profile to use ssm
    const instanceProfile = new iam.Role(this, "ec2_profile", {
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
      description: "for instance profile",
      managedPolicies: [
          iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy"),
          iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"),
      ],
    });

    // create EC2 instance
    const web_ec2 = new ec2.Instance(this, "Web-EC2", {
      vpc: vpc,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_NAT },
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T2,
        ec2.InstanceSize.MICRO
      ),
      machineImage: ami,
      keyName: key.keyName,
      role: instanceProfile,
    });

    // create RDS instance
    const rdsInstance = new rds.DatabaseInstance(this, "RDS", {
      engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_28, }),
      vpc: vpc,
      // rds instance type
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
    });

    // create ALB
    const alb = new elbv2.ApplicationLoadBalancer(this, "ALB", {
      vpc: vpc,
      internetFacing: true,
    });
    // listener rule
    const listener = alb.addListener("listener", { port: 80 });
    listener.addTargets('target', {
        port: 80,
        targets: [new elbv2tg.InstanceIdTarget(web_ec2.instanceId)],
        healthCheck: { path: "/index.html" }
    });

    // Define Connections
    web_ec2.connections.allowFrom(alb, ec2.Port.tcp(80));
    rdsInstance.connections.allowFrom(web_ec2, ec2.Port.tcp(3306));
  }
}
```

基本的にはL2コンストラクタを使用しました。
L2コンストラクタで記述すると、必要なリソースをcdkが自動的に判断してくれるので、コードの記述量が大幅に削減できます。

もちろん、細かい指定も可能なので、API Referenceを参照して設定を追加していきます。

https://docs.aws.amazon.com/cdk/api/v2/

## cdk deploy
```bash
# tsをjsにビルド
cdk build 
# または
# ファイルの変更を監視し、自動的に再コンパイル。別ターミナルで常時実行。
npm run watch

# CFnテンプレートを作成
cdk synth
# デプロイ前のセットをいろいろ展開
cdk bootstrap
# deploy
cdk deploy

```


# 感想
AWS基礎コンポーネントをcdkを使用することでIaCとして管理できるだけでなく、展開の速さも感じることができました。
（Github Copilotが優秀すぎることもあり）

cdkを実運用に載せるには、チーム内で検討すべき事項がたくさんあると思います。

- CI/CDにどう載せるか
- コーディング規約どうするか
- App > Stack > Constructをどのように定義するか
- etc...

そういった部分を考えるのもSREとしての楽しさ・やりがいなのではないかと思っています。
どこかで自分の理解を深める意味でも体系的にまとめたい...

実装していて一番感じことは、**なによりcdkが楽しい！！**

# 参考

https://aws.amazon.com/jp/blogs/news/webinar-bb-aws-cloud-development-kit-cdk-2020/


https://aws.amazon.com/jp/blogs/news/best-practices-for-developing-cloud-applications-with-aws-cdk/

https://aws.amazon.com/jp/getting-started/guides/deploy-webapp-ec2/module-one/

https://blog.serverworks.co.jp/aws-cdk-alb-ec2-rds