AWS CloudFormationでWAFを設定したELBを構築しよう

# はじめに
AWS CloudFormationを利用してVPC構築のテンプレートのサンプルです。

テンプレートの概要が分からない場合は、[はじめてのAWS CloudFormationテンプレートを理解する](https://qiita.com/miyabiz/items/eb7b0fa1675b5199b9b7)を参考にしてください。

コードは[GitHub](https://github.com/miyabiz-room/CloudFormation-Template/tree/waf-01)にもあります。

今回は、akane というシステムの dev 環境を想定しています。
同じ構成で違う環境を作成する場合は、{環境名}-parameters.jsonを別途作成します。

```text:ディレクトリ構成
    akane (システム)
      ├── network (スタック)
      │   ├── dev-parameters.json (dev 環境のパラメータ)
      │   └── network.yml (CFnテンプレート)
      ├── sg (スタック)
      │   ├── dev-parameters.json (dev 環境のパラメータ)
      │   └── sg.yml (CFnテンプレート)
      ├── elb (スタック)
      │   ├── dev-parameters.json (dev 環境のパラメータ)
      │   └── elb.yml (CFnテンプレート)
      └── waf (スタック)
          ├── dev-parameters.json (dev 環境のパラメータ)
          └── waf.yml (CFnテンプレート)
```

# AWS リソース構築内容
  1. networkスタック
    - VPC (10.0.0.0/16)
    - Publicサブネット1 (10.0.1.0/24)
    - Publicサブネット2 (10.0.2.0/24)
    - インターネットゲートウェイ
    - Publicルートテーブル
 
  2. sgスタック
    - セキュリティグループ(80ポートインバウンド許可)

  3. elbスタック
    - ELB
    - リスナー (80ポート)
    - ターゲットグループ (HealthCheckPathは/healthcheckとしています)
 
  4. wafスタック
    - ルールグループ(SQLインジェクション、XSS、ヘッダートークン認証[x-akane-id])
    - ウェブACL

# 実行環境の準備
[AWS CloudFormationを動かすためのAWS CLIの設定](https://qiita.com/miyabiz/items/fed11796f0ea2b7608f4)を参考にしてください。

# AWS リソース構築手順
1. 下記を実行してスタックを作成

    ```
    ./create_stacks.sh
    ```

2. 下記を実行してスタックを削除

    ```
    ./delete_stacks.sh
    ```

# 構築テンプレート
**1. networkスタック**
 
```yaml:network.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Network For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [all, dev, stg, prod]
    ConstraintDescription: must specify all, dev, stg, or prod.
  VPCCidrBlock:
    Type: String
    AllowedPattern: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/16
  PublicSubnet1:
    Type: String
    AllowedPattern: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/24
  PublicSubnet2:
    Type: String
    AllowedPattern: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/24

Mappings: 
  AzMap: 
    ap-northeast-1:
      1st: ap-northeast-1a
      2nd: ap-northeast-1c
      3rd: ap-northeast-1d

# Conditions

# Transform

Resources:
  # VPC作成
  akaneVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidrBlock
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-vpc
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType

###########################################################################
# Publicサブネット関連
###########################################################################
  # Publicサブネット作成
  akanePublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref akaneVPC
      CidrBlock: !Ref PublicSubnet1
      AvailabilityZone: !FindInMap [AzMap, !Ref AWS::Region, 1st]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-public-subnet1
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  akanePublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref akaneVPC
      CidrBlock: !Ref PublicSubnet2
      AvailabilityZone: !FindInMap [AzMap, !Ref AWS::Region, 2nd]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-public-subnet2
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # インターネットゲートウェイ作成
  akaneInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-igw
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # インターネットゲートウェイをVPCにアタッチ
  akaneVPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref akaneVPC
      InternetGatewayId: !Ref akaneInternetGateway
  # Publicルートテーブル作成
  akanePublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref akaneVPC
      Tags:
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-public-route
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
  # Publicルートテーブル サブネット関連付け
  akanePublicSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref akanePublicSubnet1
      RouteTableId: !Ref akanePublicRouteTable
  akanePublicSubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref akanePublicSubnet2
      RouteTableId: !Ref akanePublicRouteTable
  # Publicルートテーブル ルートにインターネットゲートウェイを関連付ける
  akanePublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref akanePublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref akaneInternetGateway

Outputs:
  akaneVPC:
    Value: !Ref akaneVPC
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-vpc
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
  akanePublicSubnet1:
    Value: !Ref akanePublicSubnet1
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-public-subnet1
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
  akanePublicSubnet2:
    Value: !Ref akanePublicSubnet2
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-public-subnet2
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
  akanePublicRouteTable:
    Value: !Ref akanePublicRouteTable
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-public-route-table
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
```

```json:dev-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "dev"
        },
        {
            "ParameterKey": "VPCCidrBlock",
            "ParameterValue": "10.0.0.0/16"
        },
        {
            "ParameterKey": "PublicSubnet1",
            "ParameterValue": "10.0.0.0/24"
        },
        {
            "ParameterKey": "PublicSubnet2",
            "ParameterValue": "10.0.1.0/24"
        }
    ]
}
```

**２. セキュリティグループスタック**

```yaml:sg.yml
AWSTemplateFormatVersion: 2010-09-09
Description: SG For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [dev, stg, prod]
    ConstraintDescription: must specify dev, stg or prod.

Mappings: 
  AzMap: 
    ap-northeast-1:
      1st: ap-northeast-1a
      2nd: ap-northeast-1d
      3rd: ap-northeast-1c

# Conditions

# Transform

Resources:
  # ELBのSG(セキュリティグループ)作成
  akaneSecurityGroupELB:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: !Sub
        - ${SystemName}-${EnvType}-sg-elb
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      GroupName: !Sub
        - ${SystemName}-${EnvType}-sg-elb
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      SecurityGroupIngress: 
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
      Tags: 
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-sg-elb
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
      VpcId:
        Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-vpc
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}

Outputs:
  akaneSecurityGroupELB:
    Value: !Ref akaneSecurityGroupELB
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-sg-elb
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
```

```json:dev-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "dev"
        }
    ]
}
```

**3. ELBスタック**

```yaml:elb.yml
AWSTemplateFormatVersion: 2010-09-09
Description: ELB For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [dev, stg, prod]
    ConstraintDescription: must specify dev stg or prod.
  TargetGroupHealthCheckPath:
    Type: String

Mappings: 
  AzMap: 
    ap-northeast-1:
      1st: ap-northeast-1a
      2nd: ap-northeast-1c
      3nd: ap-northeast-1d

# Conditions

# Transform

Resources:
  # ELB作成
  akaneElasticLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties: 
      IpAddressType: ipv4
      Name: !Sub
        - ${SystemName}-${EnvType}-elb
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Scheme: internet-facing
      SecurityGroups: 
        - Fn::ImportValue: !Sub
            - ${SystemName}-${EnvType}-sg-elb
            - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Subnets: 
        - Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-public-subnet1
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-public-subnet2
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Tags: 
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-elb
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
      Type: application

  # TargetGroup作成
  akaneTargetGroup80:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 15
      HealthCheckPath: !Ref TargetGroupHealthCheckPath
      HealthCheckPort: traffic-port
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      Name: !Sub
        - ${SystemName}-${EnvType}-tg-80
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Port: 80
      Protocol: HTTP
      Tags: 
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-tg-80
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
      TargetType: instance
      UnhealthyThresholdCount: 2
      VpcId:
        Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-vpc
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}

  # ELB リスナー80作成
  akaneELBListener80:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions: 
        - Type: forward
          TargetGroupArn: !Ref akaneTargetGroup80
      LoadBalancerArn: !Ref akaneElasticLoadBalancer
      Port: 80
      Protocol: HTTP

Outputs:
  akaneElasticLoadBalancer:
    Value: !Ref akaneElasticLoadBalancer
    Export:
      Name: !Sub
        - ${SystemName}-${EnvType}-elb
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
```

```json:dev-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "dev"
        },
        {
            "ParameterKey": "TargetGroupHealthCheckPath",
            "ParameterValue": "/healthcheck"
        }
    ]
}
```

**4. WAFスタック**

```yaml:waf.yml
AWSTemplateFormatVersion: 2010-09-09
Description: WAF For Akane

# Metadata:

Parameters:
  SystemName:
    Type: String
    AllowedPattern: '[a-zA-Z0-9-]*'
  EnvType:
    Description: Environment type.
    Type: String
    AllowedValues: [dev, stg, prod]
    ConstraintDescription: must specify dev, stg or prod.
  WAFHeaderName:
    Type: String
  WAFHeaderToken:
    Type: String

Mappings: 
  AzMap: 
    ap-northeast-1:
      1st: ap-northeast-1a
      2nd: ap-northeast-1d
      3rd: ap-northeast-1c

# Conditions

# Transform

Resources:
  # WAF RuleGroupを作成
  akaneWAFRuleGroup:
    Type: AWS::WAFv2::RuleGroup
    Properties: 
      Capacity: 1500
      Description: !Sub
        - ${SystemName}-${EnvType}-waf-rule-group
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Name: !Sub
        - ${SystemName}-${EnvType}-waf-rule-group
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Rules:
        - Name: !Sub
            - ${SystemName}-${EnvType}-waf-rule-sqli
            - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
          Priority: 0
          Action:
            Block: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: !Sub
              - ${SystemName}-${EnvType}-waf-rule-sqli
              - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
          Statement:
            OrStatement:
              Statements:
                - SqliMatchStatement:
                    FieldToMatch:
                      UriPath: {}
                    TextTransformations:
                      - Priority: 0
                        Type: NONE
                - SqliMatchStatement:
                    FieldToMatch:
                      QueryString: {}
                    TextTransformations:
                      - Priority: 0
                        Type: NONE
                - SqliMatchStatement:
                    FieldToMatch:
                      Body: {}
                    TextTransformations:
                      - Priority: 0
                        Type: NONE
        - Name: !Sub
            - ${SystemName}-${EnvType}-waf-rule-xss
            - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
          Priority: 1
          Action:
            Block: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: !Sub
              - ${SystemName}-${EnvType}-waf-rule-xss
              - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
          Statement:
            OrStatement:
              Statements:
                - XssMatchStatement:
                    FieldToMatch:
                      UriPath: {}
                    TextTransformations:
                      - Priority: 0
                        Type: NONE
                - XssMatchStatement:
                    FieldToMatch:
                      QueryString: {}
                    TextTransformations:
                      - Priority: 0
                        Type: NONE
                - XssMatchStatement:
                    FieldToMatch:
                      Body: {}
                    TextTransformations:
                      - Priority: 0
                        Type: NONE
        - Name: !Sub
            - ${SystemName}-${EnvType}-waf-rule-header
            - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
          Priority: 2
          Action:
            Allow: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: !Sub
              - ${SystemName}-${EnvType}-waf-rule-header
              - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
          Statement:
            ByteMatchStatement:
              FieldToMatch:
                SingleHeader:
                  Name: !Ref WAFHeaderName
              PositionalConstraint: EXACTLY
              SearchString: !Ref WAFHeaderToken
              TextTransformations:
                - Priority: 0
                  Type: NONE        
      Scope: REGIONAL
      Tags: 
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-waf-rule-group
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
      VisibilityConfig: 
        SampledRequestsEnabled: true
        CloudWatchMetricsEnabled: true
        MetricName: !Sub
          - ${SystemName}-${EnvType}-waf-rule-group
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}

  # WAF WebACLを作成
  akaneWAFWebACL:
    Type: AWS::WAFv2::WebACL
    Properties:
      DefaultAction:
        Allow: {}
      Description: !Sub
        - ${SystemName}-${EnvType}-waf-web-acl
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Name: !Sub
        - ${SystemName}-${EnvType}-waf-web-acl
        - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Rules:
        - Name: !Sub
            - ${SystemName}-${EnvType}-waf-web-acl-rule
            - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
          OverrideAction:
            None: {}
          Statement:
            RuleGroupReferenceStatement: 
              Arn: !GetAtt akaneWAFRuleGroup.Arn
          Priority: 0
          VisibilityConfig: 
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: !Sub
              - ${SystemName}-${EnvType}-waf-web-acl-rule
              - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      Scope: REGIONAL
      Tags: 
        - Key: Name
          Value: !Sub
          - ${SystemName}-${EnvType}-waf-web-acl
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
        - Key: SystemName
          Value: !Ref SystemName
        - Key: EnvType
          Value: !Ref EnvType
      VisibilityConfig: 
        SampledRequestsEnabled: true
        CloudWatchMetricsEnabled: true
        MetricName: !Sub
          - ${SystemName}-${EnvType}-waf-web-acl
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}

  # WAF WebACLをELBにを関連付ける
  akaneWebACLAssociation:
    Type: AWS::WAFv2::WebACLAssociation
    Properties: 
      ResourceArn:
        Fn::ImportValue: !Sub
          - ${SystemName}-${EnvType}-elb
          - {SystemName: !Ref SystemName, EnvType: !Ref EnvType}
      WebACLArn: !GetAtt akaneWAFWebACL.Arn         

# Outputs:
```

```json:dev-parameters.json
{
    "Parameters": [
        {
            "ParameterKey": "SystemName",
            "ParameterValue": "akane"
        },
        {
            "ParameterKey": "EnvType",
            "ParameterValue": "dev"
        },
        {
            "ParameterKey": "WAFHeaderName",
            "ParameterValue": "x-akane-id"
        },
        {
            "ParameterKey": "WAFHeaderToken",
            "ParameterValue": "4HGVKikFK4EBHHDiBFmW-mKVxHZAt9hTsJyCCXz3M"
        }
    ]
}
```

**5. 実行ファイル**

```sh:create_stacks.sh
#!/bin/sh
SYSTEM_NAME=akane
ENV_TYPE=dev

function create_stack() {
STACK_NAME=$1
aws cloudformation create-stack \
--stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME} \
--template-body file://./${SYSTEM_NAME}/${STACK_NAME}/${STACK_NAME}.yml \
--cli-input-json file://./${SYSTEM_NAME}/${STACK_NAME}/${ENV_TYPE}-parameters.json

aws cloudformation wait stack-create-complete \
--stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

create_stack network
create_stack sg
create_stack elb
create_stack waf
```

```sh:delete_stacks.sh
#!/bin/sh
SYSTEM_NAME=akane
ENV_TYPE=dev

function delete_stack() {
STACK_NAME=$1
aws cloudformation delete-stack \
--stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}

aws cloudformation wait stack-delete-complete \
--stack-name ${SYSTEM_NAME}-${ENV_TYPE}-${STACK_NAME}
}

delete_stack waf
delete_stack elb
delete_stack sg
delete_stack network
```