ようへいの日々精進XP

よかろうもん

俺の AWS CDK コードを恥ずかしげもなく晒す (2) 〜 よくありそうな S3 + CloudFront + Route53 構成 (2) 〜

tl;dr

俺の AWS CDK コードを恥ずかしげもなく晒すシリーズ第一弾の続き. そして, この記事は YAMAP エンジニア Advent Calendar 2019 の五日目の記事になる予定です.

qiita.com

実現したいこと

昨日, 恥ずかしげもなく公開したコードを少し進化させてみました. 進化の内容は以下の通り.

  • 出来るだけ, 肝となるコード内にドメイン名とか ID とか汎用性の低い情報を書かないようにする
  • WAF の WebACL を紐付ける

俺の AWS CDK

リポジトリ

引き続き, 以下にアップしています.

github.com

肝となる lib/tutorial01-stack.ts は以下の通り.

import cdk = require('@aws-cdk/core');
import s3 = require('@aws-cdk/aws-s3');
import cf = require('@aws-cdk/aws-cloudfront');
import iam = require('@aws-cdk/aws-iam');
import route53 = require('@aws-cdk/aws-route53');
import route53_targets = require('@aws-cdk/aws-route53-targets/lib');

interface Tutorial01InfraStackProps extends cdk.StackProps {
  domain: string;
  project: string;
  issue: string;
  owner: string;
  certificate_arn: {[key: string]: string};
  logbucket_name: string;
  webacl_id: string;
}

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

    // const envName = this.node.tryGetContext('env');
    const envName = process.env.DEPLOY_ENV ? process.env.DEPLOY_ENV : 'dev';
    const domainName = this.node.tryGetContext('fqdn');

    // Create Bucket for contents
    const websiteBucket = new s3.Bucket(this, `Tutorial01Infra-s3bucket-${this.stackName}`, {
      bucketName: domainName,
    });

    // Define Bucket for logging
    const loggingBucket = s3.Bucket.fromBucketName(
      this,
      `Tutorial01Infra-loggingbucket-${this.stackName}`,
      props.logbucket_name
    );

    // Create CloudFront Origin Access Identity
    const OAI = new cf.CfnCloudFrontOriginAccessIdentity(this, `Tutorial01Infra-identity-${this.stackName}`,{
      cloudFrontOriginAccessIdentityConfig:{
        comment: `Tutorial01Infra-identity-${this.stackName}`
      }
    });

    // Create Access Policy for S3 Bucket
    const webSiteBucketPolicyStatement = new iam.PolicyStatement({effect: iam.Effect.ALLOW});
    webSiteBucketPolicyStatement.addCanonicalUserPrincipal(OAI.attrS3CanonicalUserId);
    webSiteBucketPolicyStatement.addActions("s3:GetObject");
    webSiteBucketPolicyStatement.addResources(`${websiteBucket.bucketArn}/*`);
    websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement);

    // Create CloudFront Distribution
    const distribution = new cf.CloudFrontWebDistribution(this, `Tutorial01Infra-cloudfront-${this.stackName}`, {
      originConfigs:[
        {
          s3OriginSource: {
            s3BucketSource: websiteBucket,
            originAccessIdentityId: OAI.ref
          },
          behaviors: [{ isDefaultBehavior: true}]
        }
      ],
      aliasConfiguration: {
        acmCertRef: props.certificate_arn[envName],
        names: [domainName],
        sslMethod: cf.SSLMethod.SNI,
        securityPolicy: cf.SecurityPolicyProtocol.TLS_V1_1_2016,
      },
      viewerProtocolPolicy: cf.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      priceClass: cf.PriceClass.PRICE_CLASS_ALL,
      loggingConfig: {
        bucket: loggingBucket,
        prefix: domainName + '/' 
      },
      webACLId: props.webacl_id
    });

    const zone = route53.HostedZone.fromLookup(this, 'Zone', { domainName: props.domain });
    new route53.ARecord(this, `Distribution-route53-record-${this.stackName}`, {
      recordName: domainName,
      target: route53.AddressRecordTarget.fromAlias(new route53_targets.CloudFrontTarget(distribution)),
      zone
    });

    for (let cons of [websiteBucket, distribution]) {
      cdk.Tag.add(cons, 'Project', props.project);
      cdk.Tag.add(cons, 'Environment', envName);
      cdk.Tag.add(cons, 'Owner', props.owner);
      cdk.Tag.add(cons, 'Issue', props.issue);
      cdk.Tag.add(cons, 'Name', domainName);
    }

    // Output CloudFront URL
    new cdk.CfnOutput(this, 'CloudFrontURL', {value: `https://${distribution.domainName}/`})
    // Output Distribution ID
    new cdk.CfnOutput(this, 'DistributionId', { value: distribution.distributionId });
  }
}

更に, bin/tutorial01-stack.ts は以下のように書きました. こちらに出来るだけユニークな情報 (汎用性の低い情報) を寄せるようにした感じです.

#!/usr/bin/env node
import 'source-map-support/register';
import cdk = require('@aws-cdk/core');
import { Tutorial01Stack } from '../lib/tutorial01-stack';

const deployEnv = process.env.DEPLOY_ENV ? process.env.DEPLOY_ENV : 'dev';

const app = new cdk.App();
new Tutorial01Stack(app, `Tutorial01Stack-${deployEnv}`, {
    env: {
        account: process.env.CDK_DEFAULT_ACCOUNT,
        region: process.env.CDK_DEFAULT_REGION
    },
    domain: 'example.com',
    project: 'my-project',
    issue: 'my-issue',
    owner: 'My Team',
    certificate_arn: { dev: 'arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx',
                       production: 'arn:aws:acm:us-east-1:123456789012:certificate/xxxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx' },
    logbucket_name: 'my-log-bucket-name',
    webacl_id: 'xxxxxxxxx-xxxx-xxxx-xxxxx-xxxxxxxx'
});

従来は --context オプションを使って渡していた dev とか production を表現する文字列については, 環境変数から渡すようにして, 環境毎に CloudFormation スタックを作るようにしました.

以上

引き続き, 頑張って AWS CDK を習得していきたいと思います.