AWS CDK構築備忘録①(WAF+CloudFlont+S3)

# 内容
下記AWS WAF、CloudFront、S3の箇所をCDKで構築します。
* 証明書はACMで払い出し済みのものを使用
* ホストゾーンはRoute53でドメイン取得時に作成されたものを使用
* 環境作成後にCloudFrontのAレコード（Alias）をホストゾーンに追加

![cloudfront.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523652/7aeb11b4-715b-2fcb-8b69-78529d794dd5.png)

# WAFスタック

## 準備
```powershell
mkdir waf
cd waf
cdk init --language typescript
cdk bootstrap aws://ACCOUNT-NUMBER/us-east-1
npm install @aws-cdk/aws-wafv2
npx tsc -w
```

## 作成
* デフォルトブロック、日本からのアクセスを許可するルールを追加
```typescript:bin/waf.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { WafStack } from '../lib/waf-stack';

const app = new cdk.App();
new WafStack(app, 'WafStack', {
 env: {
    region: "us-east-1",
  },
});
```
```typescript:lib/waf-stack.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';

export class WafStack extends Stack {

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const waf_scope = 'CLOUDFRONT';
    const cfnWebACL = new wafv2.CfnWebACL(this, 'WebACL', {
      defaultAction: {
        block: {
        }
      },
      name: 'waf-web-acl',
      scope: waf_scope,
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: 'waf-web-acl',
        sampledRequestsEnabled: true,
      },
      rules: [
        {
          priority: 0,
          name: 'country-rule-set',
          action: { allow: {}},
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: 'country-rule-set',
          },
          statement: {
            geoMatchStatement: {
              countryCodes: ['JP'],
            },
          },
        },
      ]
    })
  };
};

```

# CloudFrontスタック

## 準備
```powershell
mkdir cloudfront
cd cloudfront
cdk init --language typescript
cdk bootstrap aws://ACCOUNT-NUMBER/ap-northeast-1
npm install @aws-cdk/aws-s3
npm install @aws-cdk/aws-cloudfront
npm install @aws-cdk/aws-cloudfront-origins
npm install @aws-cdk/aws-iam
npm install @aws-cdk/aws-route53
npm install @aws-cdk/aws-certificatemanager
npx tsc -w
```
## 作成

* コンテンツ配置用のS3バケット作成
* CloudFrontのログ保存用のS3バケット作成
* OAIアカウント作成とバケットポリシーに追加
* CloudFrontディストリビューション作成（WAFと紐づけ）

```typescript:bin/cloudfront.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CloudfrontStack } from '../lib/cloudfront-stack';

const app = new cdk.App();

new CloudfrontStack(app, 'CloudfrontStack', {
  env: {
    region: 'ap-northeast-1',
  },
});
```

```typescript:lib/cloudfront-stack.ts
import { aws_route53_targets, RemovalPolicy, Stack, StackProps, } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as acm from 'aws-cdk-lib/aws-certificatemanager'
import * as route53 from 'aws-cdk-lib/aws-route53'

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

    //Variable declaration
    const app_name:string = 'app-cloudfront';
    const create_year:string = String(new Date().getFullYear());
    const create_month:string =  ('0' + String(new Date().getMonth() + 1)).slice(-2);
    const app_bucket_name:string = `${app_name}-backet-${create_year}${create_month}`;
    const domain_name:string = 'DOMAIN_NAME';
    const cert_arn:string = 'CERT_ARN';
    const cfn_webacl_arn:string = 'WAFACL_ARN'

    // Create S3 bucket for logging
    const log_bucket = new s3.Bucket(this, 'LogBucket',{
      bucketName: `${app_bucket_name}-log`,
      enforceSSL: true,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    //Create S3 bucket for App 
    const app_bucket = new s3.Bucket(this, 'AppBacket',{
      bucketName: app_bucket_name,
      enforceSSL: true,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      });
    
    //Create OAI account & bucket policy
    const identity = new cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {
      comment: `cloudfront OAI access for bucket: ${app_bucket_name}`, 
    });

    const canonicalUserPrincipal = new iam.CanonicalUserPrincipal(identity.cloudFrontOriginAccessIdentityS3CanonicalUserId);

    const oai_bucket_policy = new iam.PolicyStatement({
      actions:['S3:GetObject'],
      effect: iam.Effect.ALLOW,
      principals: [canonicalUserPrincipal],
      resources: [`${app_bucket.bucketArn}/*`],
    });

    app_bucket.addToResourcePolicy(oai_bucket_policy);

    //Create CloudFront Distribution

    const distribution = new cloudfront.Distribution(this, 'distribution', {
      defaultBehavior: {
        allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD,
        cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD,
        cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        origin: new origins.S3Origin(app_bucket, {
          originAccessIdentity: identity,
        }),
      },

      enableLogging: true,
      logBucket: log_bucket,
      logFilePrefix: 'cloudfront_log',
      priceClass: cloudfront.PriceClass.PRICE_CLASS_200,
      domainNames: [domain_name],
      certificate: acm.Certificate.fromCertificateArn(
        this,
        'Certificate',
        cert_arn,
      ),
      comment: `${app_name}-distribution`,
      defaultRootObject: 'index.html',
      webAclId: cfn_webacl_arn,

    });

    // Create host zone and A record to Route53
    // const host_zone = new route53.PublicHostedZone(this, 'HostedZone', {
    //   zoneName: 'example.com',
    // });

    // new route53.ARecord(this, 'CloudFrontARecord', {
    //   zone: host_zone,
    //   recordName: 'www',
    //   target: route53.RecordTarget.fromAlias(new aws_route53_targets.CloudFrontTarget(distribution)),

    // }) 
  }
}

```