AWS CDKを使ってCloudFrontとALBの環境構築をしてみた

# はじめに
この記事は[Playground Advent Calender 2019](https://qiita.com/advent-calendar/2019/playground)の12/14の記事です。
初めまして。今年の春からPlaygroundでバックエンドをメインで勉強しているkikilsです。ここでは、ここ最近勉強しているAWS CDKというサービスについての知見を書いていきたいと思います。

# 行ったこと
CDKでユーザーがECSにアクセスするときにCloudFrontを経由する仕組みを作りました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547791/5d41cf64-bc0e-b887-f580-603817757e16.png)


# AWS CDK(Cloud Development Kit)とは
今年の7月にGAされたばかりのサービスで、AWSのインフラを TypeScript などの言語を使って定義します。プログラムはAppとStackとConstructという概念で構成します。CDKプログラムを実行することで CloudFormation テンプレートを生成してそのテンプレートを使ってデプロイします。

# 仕組み
CloudFrontを経由しないALBへのアクセスを拒否するには、ALBのリクエストルーティング機能を使います。ALBのlistenerのルールに「Cloud Frontからのアクセスであるかどうかの判定をしたらECSに転送する」というルールを追加したうえで「それ以外のアクセスは403を返す」というルールもデフォルトアクションにすることで実現します。CloudFrontからのアクセスかどうかの判定は、httpヘッダを読み取り、User-agentがCloudFrontのものであるかとCloudFront側でカスタムヘッダを追加してそれが一致するかどうかを判定します。



# コードを書いてみる
まず、FargateとCloudFontを構築するStackを定義します。

```typescript

const app = new cdk.App();

const fargateStack = new FargateStack(app, `fargate`);
new CloudFrontStack(app, `cloudfront`, {
    loadBalancer: fargateStack.loadBalancer,
});

```


次に、CloudFrontのStackを作成していきます。AWS CDKの[公式APIレファレンス](https://docs.aws.amazon.com/cdk/api/latest/)を参照すると`CloudFrontWebDistribution`クラスがあるのでこれを用います。カスタムオリジンにはfargateStackで渡してあげたALBのDNSを指定しています。また、`originProtocolPolicy`をhttpにしていますが本番環境ではhttpsに変更したほうがよいです。カスタムヘッダには、任意の文字列を設定します。

```typescript:cloudFront-stack.ts

interface CloudFrontProps extends cdk.StackProps {
    loadBalancer: ApplicationLoadBalancer;
}

export class CloudFrontStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props: CloudFrontProps) {
        super(scope, id, props);

     new CloudFrontWebDistribution(this, `distribution`, {
            defaultRootObject: "",
            originConfigs: [
                {
                    customOriginSource: {
                        domainName: props.loadBalancer.loadBalancerDnsName,
                        originProtocolPolicy: OriginProtocolPolicy.HTTP_ONLY
                    },
                    behaviors: [
                        {
                            isDefaultBehavior: true
                        }
                    ],
                    originHeaders: {
                        ["x-pre-shared-key"]: "PRESHAREDKEY"
                    }
                }
            ]
        });
    }
}
```

次に、fargateを構築します。clusterとtaskDefinitionは各自で当ててください。

```typescript:fargate-stack.ts

export class FargateStack extends cdk.Stack {
    public readonly loadBalancer: ApplicationLoadBalancer;
    const fargate = new ApplicationLoadBalancedFargateService(
            this,
            "apps",
            {
                cluster: cluster,
                taskDefinition: taskDefinition
            }
        );

        const listener = fargate.listener.node.defaultChild as CfnListener;
        listener.defaultActions = [
            {
                type: "fixed-response",
                fixedResponseConfig: {
                    statusCode: "403",
                    contentType: "text/html",
                    messageBody: "<h1>403 Forbidden</h1>"
                }
            }
        ];

        new CfnListenerRule(this, `listener`, {
            actions: [
                {
                    type: `forward`,
                    targetGroupArn: fargate.targetGroup.targetGroupArn
                }
            ],
            conditions: [
                {
                    field: "http-header",
                    httpHeaderConfig: {
                        httpHeaderName: "User-agent",
                        values: ["Amazon CloudFront"]
                    }
                },
                {
                    field: "http-header",
                    httpHeaderConfig: {
                        httpHeaderName: "x-pre-shared-key",
                        values: ["PRESHAREDKEY"]
                    }
                }
            ],
            listenerArn: listener.ref,
            priority: 1
        });
```

### 説明
`ApplicationLoadBalancedFargateService`クラスはクラスターを作りALBとターゲットグループの接続をよしなにしてくれます。

- listenerのデフォルトアクションを変更する

CDKにはlistenerのデフォルトアクションを設定するパラメータがまだないので、CloudFormationに直接上書きできる`CfnListener`クラスを使います。実際に出力されるcloudformationのテンプレ―トは次のようになります。

```json
～～～～～～～～～～
"appsLBPublicListenerEC0E93DB": {
      "Type": "AWS::ElasticLoadBalancingV2::Listener",
      "Properties": {
        "DefaultActions": [
          {
            "FixedResponseConfig": {
              "ContentType": "text/html",
              "MessageBody": "<h1>403 Forbidden</h1>",
              "StatusCode": "403"
            },
            "Type": "fixed-response"
          }
        ],
        "LoadBalancerArn": {
          "Ref": "appsLB2B3DA679"
        },
        "Port": 80,
        "Protocol": "HTTP"
      },
      "Metadata": {
        "aws:cdk:path": "fargate/apps/LB/PublicListener/Resource"
      }
    }
～～～～～～～～～～
```
このようにDefaultActionsが上書きされていることがわかります。

- Cloud Frontからのアクセスであるかどうかの判定をしたらECSに転送する

listenerと同じくCDKには`ApplicationListenerRule`というクラスが用意されていますがルールの条件にhttpヘッダがサポートされていないのでCloudformationを直に追加できる`CfnListerRule`を使います。この時に出力されるテンプレートは次のようになります。

```json

～～～～～～～～～～
"listener": {
      "Type": "AWS::ElasticLoadBalancingV2::ListenerRule",
      "Properties": {
        "Actions": [
          {
            "TargetGroupArn": {
              "Ref": "appsLBPublicListenerECSGroup7B961058"
            },
            "Type": "forward"
          }
        ],
        "Conditions": [
          {
            "Field": "http-header",
            "HttpHeaderConfig": {
              "HttpHeaderName": "User-agent",
              "Values": [
                "Amazon CloudFront"
              ]
            }
          },
          {
            "Field": "http-header",
            "HttpHeaderConfig": {
              "HttpHeaderName": "x-pre-shared-key",
              "Values": [
                "PRESHAREDKEY"
              ]
            }
          }
        ],
        "ListenerArn": {
          "Ref": "appsLBPublicListenerEC0E93DB"
        },
        "Priority": 1
      },
～～～～～～～～～～
```

このようにCloudFormationのテンプレートに追加されていることがわかります。

# まとめ
このようにCDKはAWSサービスが抽象化されて非常にわかりやすくなっており、それだけではなく今回のようにパラメータにない場合でもAWSサービスを構築することができます。