WAF+ALB+EC2+RDSの構成をテンプレート化してみた

## はじめに
本記事では、AWSで使用頻度が高いアーキテクチャーである、
WAF+ALB+EC2+RDSの構成をCDKでテンプレートしました。

テンプレート化したCDKを使用してAWSのインフラ環境を自動構築することで、
構築時間を短縮することができます。

記事の後半に、変更頻度が高いパラメータを各サービス毎にまとめましたので、
本記事で紹介したCDKを参考にして、パラメータ変更する際に役立てていただけると幸いです。

# インフラの仕事をし続けて困ったこと
　WAF+ALB+EC2+RDSの構成は、よく案件で構築することが多いのですが、案件が開始するたびに、ゼロからコンソールを使用して手作業で構築してました。手作業で実施していると、動作確認中に1回以上はミスが見つかり、調査とパラメータ修正に時間を要します。
　今回のブログを通じて、AWSのテンプレートをCDK作成することで、手作業で構築する手間を少なくし、一定の動作を保証した環境を作成します。


## 構成図とコードの紹介
WAF+ALB+EC2+RDSの構成は、下記の構成図になります。
![cdk.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3671603/b00f7676-c48a-bf39-a3ca-fd5c5500bf68.png)

こちらを、CDKでコード化したものが、下記の「waf-alb-ec2-rds-stack.ts」になります。

```typescript:waf-alb-ec2-rds-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as autoscaling from 'aws-cdk-lib/aws-autoscaling';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';


//[PARAMETER] VPC
let VPC_CIDR = '20.0.0.0/16';
let NATGW = 1; //NATGatewayの個数

//[PARAMETER] EC2
let EC2_INSTANCE_SPEC = 't3.micro';

//[PARAMETER] AutoScaling
let ASG_DESI = 1;
let ASG_MAX = 2;
let ASG_MIN = 1;

//[PARAMETER] RDS
let RDS_INSTANCE_CLASS = ec2.InstanceClass.T2;
let RDS_INSTANCE_SIZE = ec2.InstanceSize.SMALL;
let MULTI_AZ =  true;


const app = new cdk.App();
const stack = new cdk.Stack(app, 'MyStack');

export class AwsCdkEc2TamplateStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    //VPC, Subnet, NAT Gatewayの作成
    const vpc = new ec2.Vpc(this, 'MyVpc', {
      cidr: VPC_CIDR,
      natGateways: NATGW, 
      availabilityZones: ['ap-northeast-1a', 'ap-northeast-1c'], 
      subnetConfiguration: [
        {
          subnetType: ec2.SubnetType.PUBLIC,
          name: 'Public',
          cidrMask: 24,
        },
        {
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
          name: 'Private',
          cidrMask: 24,
        },
        {
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
          name: 'DB',
          cidrMask: 24,
        },
      ],
    });
  
    // ALB用のセキュリティグループを作成
    const albSecurityGroup = new ec2.SecurityGroup(this, 'ALBSecurityGroup', {
      vpc,
      allowAllOutbound: true,    
    });
  
    // ALBを作成
    const alb = new elbv2.ApplicationLoadBalancer(this, 'ALB', {
      vpc,
      internetFacing: true,
      securityGroup: albSecurityGroup,
    });
  
    // ALBアクセスログ用バケットを作成
    const logBucket = new s3.Bucket(this, "LogBucket", {});
  
    // ALBアクセスログ設定
    alb.logAccessLogs(logBucket);
  
    // EC2インスタンス用のセキュリティグループを作成
    const ec2SecurityGroup = new ec2.SecurityGroup(this, 'EC2SecurityGroup', {
      vpc,
      allowAllOutbound: true,    
    });
    ec2SecurityGroup.addIngressRule(albSecurityGroup, ec2.Port.tcp(80));

    //DBのサブネットグループを作成
    const rdsSecuriyGroup = new ec2.SecurityGroup(this, 'RDSSecuriyGroup', {
      vpc,
      allowAllOutbound: true, 
    });
    rdsSecuriyGroup.addIngressRule(ec2SecurityGroup, ec2.Port.tcp(3306));
  
    
    const userDataEC2 = ec2.UserData.forLinux({ shebang: '#!/bin/bash' })
    userDataEC2.addCommands(
      'yum update -y',
      'yum install -y httpd',
      'systemctl start httpd',
      'systemctl enable httpd',
      'touch /var/www/html/index.html',
      'echo "AWS TEST" | tee -a /var/www/html/index.html'
    )
    

    const asg = new autoscaling.AutoScalingGroup(this, "Ec2Autoscaling", {
        vpc,
        vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
        instanceType: new ec2.InstanceType(EC2_INSTANCE_SPEC),
        machineImage: ec2.MachineImage.latestAmazonLinux2023(),
        desiredCapacity: ASG_DESI,
        maxCapacity: ASG_MAX,
        minCapacity: ASG_MIN,
        userData: userDataEC2,
        securityGroup: ec2SecurityGroup,
        associatePublicIpAddress: false,
        ssmSessionPermissions: true,
        
    });
    cdk.Tags.of(asg).add('Mode', 'autoscaling');//Auto Scaling Group Tag

    
    const targetGroup = new cdk.aws_elasticloadbalancingv2.ApplicationTargetGroup(
      this,
      "TargetGroup",
      {
        vpc: vpc,
        port: 80,
        targetType: cdk.aws_elasticloadbalancingv2.TargetType.INSTANCE,
        targets: [asg],
        healthCheck: {
          path: '/',
        },
      }
    );

    //リスナー設定
    const listener = alb.addListener("Listener", {
      port: 80,
      protocol: cdk.aws_elasticloadbalancingv2.ApplicationProtocol.HTTP,
      defaultTargetGroups: [targetGroup],
    });

    listener.addTargetGroups('TargetGroup', {
      targetGroups: [targetGroup],
    });
    
  
    // RDSサブネットグループを作成
    const subnetGroup = new rds.SubnetGroup(this,'MySubnetGroup', {
      vpc,
      description: 'MySubnetGroup',
      subnetGroupName: 'MySubnetGroup',
      vpcSubnets: {
        subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
       }
    });
  
    
    // DBインスタンス(RDS MySQL)を作成
    
    const dbServer = new rds.DatabaseInstance(this, "WordPressDB", {
      vpc,
      engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_31 }),
      instanceType: ec2.InstanceType.of(RDS_INSTANCE_CLASS, RDS_INSTANCE_SIZE),
      databaseName: "wordpress",
      multiAz: MULTI_AZ,
      subnetGroup: subnetGroup,
      securityGroups:  [rdsSecuriyGroup],
    });
    


    // WAF
    const webACL = new cdk.aws_wafv2.CfnWebACL(this,"MyWebACL-main",{
      defaultAction: {
        allow: {}
      },
      scope: "REGIONAL",
      visibilityConfig: {
        cloudWatchMetricsEnabled: false,
        metricName: "MyWebACL-metrics",
        sampledRequestsEnabled: true,
      },
      name: "MyWebACL-managed-1",
      rules: [
        {
          name: 'AWS-AWSManagedRulesCommonRuleSet',
          priority: 0,
          statement: {
            managedRuleGroupStatement: {
              name:'AWSManagedRulesCommonRuleSet',
              vendorName:'AWS',
              excludedRules: [
                {name: 'SizeRestrictions_BODY'},
                {name: 'NoUserAgent_HEADER'},
                {name: 'UserAgent_BadBots_HEADER'},
                {name: 'SizeRestrictions_QUERYSTRING'},
                {name: 'SizeRestrictions_Cookie_HEADER'},
                {name: 'SizeRestrictions_BODY'},
                {name: 'SizeRestrictions_URIPATH'},
                {name: 'EC2MetaDataSSRF_BODY'},
                {name: 'EC2MetaDataSSRF_COOKIE'},
                {name: 'EC2MetaDataSSRF_URIPATH'},
                {name: 'EC2MetaDataSSRF_QUERYARGUMENTS'},
                {name: 'GenericLFI_QUERYARGUMENTS'},
                {name: 'GenericLFI_URIPATH'},
                {name: 'GenericLFI_BODY'},
                {name: 'RestrictedExtensions_URIPATH'},
                {name: 'RestrictedExtensions_QUERYARGUMENTS'},
                {name: 'GenericRFI_QUERYARGUMENTS'},
                {name: 'GenericRFI_BODY'},
                {name: 'GenericRFI_URIPATH'},
                {name: 'CrossSiteScripting_COOKIE'},
                {name: 'CrossSiteScripting_QUERYARGUMENTS'},
                {name: 'CrossSiteScripting_BODY'},
                {name: 'CrossSiteScripting_URIPATH'}
              ]
            }
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            metricName:'AWS-AWSManagedRulesCommonRuleSet',
            sampledRequestsEnabled: true,
          },
          overrideAction: {
            none: {}
          },
        }
      ]
    });

    const wafPolicy = new wafv2.CfnWebACLAssociation(this, 'MyWebACL-arn', {
      webAclArn: webACL.attrArn,
      resourceArn: alb.loadBalancerArn
    });

  }
}
```
## パラメータの解説
サービスの設定において、変更頻度が高いパラメータを、
サービス毎に表を作成してにまとめました。

### VPC
|変数名| パラメータの概要  | 値|
|---|---|---|
|VPC_CIDR  | VPCのサイダーブロック  |20.0.0.0/16 |
|NATGW  | NATGatewayの個数  |1|

### EC2
|変数名| パラメータの概要  | 値|
|---|---|---|
|EC2_INSTANCE_TYPE  | EC2のインスタンスタイプ  |t3.micro|

### EC2 AutoScaing
|変数名| パラメータの概要  | 値|
|---|---|---|
|ASG_DESIRED_CAPACITY| 起動時に必要なインスタンス数  |1|
|ASG_MAX  | インスタンスの最大数  |2|
|ASG_MIN  | インスタンスの最小数  |1|

### RDS
|変数名| パラメータの概要  | 値|
|---|---|---|
|RDS_INSTANCE_CLASS| RDSのインスタンスクラス  |ec2.InstanceClass.T2|
|RDS_INSTANCE_SIZE  | RDSのインスタンスサイズ  |ec2.InstanceSize.SMALL|
|MULTI_AZ  | マルチAZの有効  |true|


## 作成したCDKをAWS環境にデプロイ
AWSアカウントで初めてCDKを使用してデプロイする際は、下記のコマンドを使用して、
Bootstrapを作成します。（2回目以降のデプロイからは省略できます。）
```
cdk bootstrap
```

CDKからCloudFormationスタックを生成します。
```
cdk synth
```

リソースをデプロイします。
```
cdk deploy
```

## 不使用になった環境はcdk destoryで一括削除
無料アカウントで作っている場合、放置してるとお金がかかります。
CDKで作成した環境を消すときは、
```
cdk destroy
```
を実行することで、AWS上からすべて削除できます。消し忘れの心配もありません。


## 感想
使用頻度が高いAWS構成をCDKでテンプレート化しておくことで、構築工数の削減、調整頻度が高いパラメータの設計に集中することができ、品質向上を図ることができました。また、CDKによる環境構築は、自動で行われるため、構築中は他の作業を同時に進められます。これらのメリットは、オンプレでは味わえない、クラウド固有のいいところだど思いました。