AWSCDKを使ってVuejsをデプロイしてみた

AWSCDKを使ってVuejsをデプロイしてみた

こんにちは。 今春の応用情報試験の勉強で一ヶ月ぐらいブログほったらかしでしたが、最近延期されることが決まったため勉強は一旦止めていろいろ触りたかった技術を触ることにしました。

今日はAWS クラウド開発キット、別名CDKをワークショップから一歩進んだ内容で触ってみました。

CDKは2週間前にワークショップを触ッタレベルです。ちなみにワークショップはとても丁寧でCDKの特徴がわかって入門にはぴったりです。

https://cdkworkshop.com/

今回は、仕事でVue.jsで作成された静的ファイルをS3バケット・CloudFrontを用意する作業を新しいPJが立つたびに行う必要があって面倒なので、それを簡略化するためにCDKを導入しようと思います。

通常の静的ファイル配信とは異なり、どのパスに対するアクセスでも、index.htmlに内部でリダイレクトさせる必要があります。というのも、Vue.jsでの各ページ遷移には、Vue Routerのhistoryモードを使っているためです。SPAによくあることだと思います。その点に注意する必要がありました。

aws公式のGitHubリポジトリが静的ファイルのデプロイをCDKで行っているサンプルをあげていたのでそちらを全面的に参考にしています。他のIaSと比較したCDKの弱点として、 ドキュメントが少ない点 が挙げられている通り、あまり他に参考になりそうなドキュメントはありませんでした。

https://blog.codecentric.de/en/2019/09/aws-cdk-versus-terraform-and-serverless-framework/

サンプルを準備

git cloneしてサンプルのディレクトリまで移動

$ git clone https://github.com/aws-samples/aws-cdk-examples.git
$ cd aws-cdk-examples/typescript/static-site
$ yarn

パッケージをインストールしないと、vscodeで補完が効かなくなるのでこのタイミングでしたほうが良いです

修正

エントリーポイントを修正しました。修正したものが以下の通りです。

// index.ts

#!/usr/bin/env node
import cdk = require('@aws-cdk/core');
import { StaticSite } from './static-site';

class MyStaticSiteStack extends cdk.Stack {
    constructor(parent: cdk.App, name: string, props: cdk.StackProps) {
        super(parent, name, props);
        new StaticSite(this, name);
   }
}

const app = new cdk.App();

const appName = app.node.tryGetContext('app_name');
const mode = app.node.tryGetContext('mode');
const name = `${appName}-${mode}`

new MyStaticSiteStack(app, `${name}`, { env: {
    region: 'us-east-1'
}});

app.synth();

各リソースの同一性を担保するものは、 コンストラクタの name なので、その部分は cdk.jsonのcotextから読み込めるようにしました。 ちなみにname_が使えないので区切り文字には -を使いましょう

cdkには以下のような感じでcotextを設定しています。

{
    "app": "node index",
    "context": {
        "app_name": "myapp",  // アプリ名
        "mode": "staging"    // 環境種類(staging or production)
    }
}

また、リージョンは、 cdk.Stack クラスのコンストラクタの propsに envとして設定するみたいです。ACMはバージニアリージョンで設定しないと有効にならないのでバージニアになっています。

このファイルから読み込んでいる static-site.ts も修正していきます。今回の範囲では、Route53とACMは一旦不要なのでコメントアウトします。

s3の設定箇所がこちら。

        const siteBucket = new s3.Bucket(this, `SiteBucket-${name}`, {
            bucketName: name,
            websiteIndexDocument: 'index.html',
            websiteErrorDocument: 'index.html',
            publicReadAccess: true,
            removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code

            // websiteRoutingRules: [
            //     {
            //         condition: {
            //             httpErrorCodeReturnedEquals: "404",
            //         },
            //         httpRedirectCode: "301",
            //         replaceKey: s3.ReplaceKey.with("redirect.html")
            //     }
            // ]
        });
        new cdk.CfnOutput(this, 'Bucket', { value: siteBucket.bucketName });

Vuejsなのでエラーページもindex.htmlで良いです。内部でリダイレクトさせるさいに、S3のリダイレクト設定を使おうとしている人をみたことありますが、CloudFrontに噛ませる場合だと無効状態になってしまいます。なのでs3ではリダイレクトの設定をする必要がありません。僕はそれを忘れていて設定を書いてしまいました💦(コメントアウトしてる部分が該当の設定です)

また、new cdk.CfnOutput部分で、デプロイ時にデータを出力することができるようです。これも一つのリソースのようです。

CloudFrontの設定箇所が以下の通りです

const distribution = new cloudfront.CloudFrontWebDistribution(this, `SiteDistribution-${name}`, {
            // aliasConfiguration: {
            //     acmCertRef: certificateArn,
            //     names: [ siteDomain ],
            //     sslMethod: cloudfront.SSLMethod.SNI,
            //     securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_1_2016,
            // },
            errorConfigurations: [
                {
                    errorCode: 403,
                    responseCode: 200,
                    responsePagePath: "/index.html"
                },
                {
                    errorCode: 404,
                    responseCode: 200,
                    responsePagePath: "/index.html"
                }
            ],
            originConfigs: [
                {
                    s3OriginSource: {
                        s3BucketSource: siteBucket
                    },
                    behaviors : [ {isDefaultBehavior: true}],
                }
            ]
        });
        new cdk.CfnOutput(this, 'DistributionId', { value: distribution.distributionId });

        // サイトURL表示
        new cdk.CfnOutput(this, 'SiteUrl', { value: 'https://' + distribution.domainName });

errorConfigurationsがリダイレクトの設定です。型定義が厳密なtypescriptとvscodeの相性が良いおかげで、気分良く実装することができました。

プロパティがstringやnumberではなく独自インタフェースでも、VSCODE上で定義元に飛ぶことで簡単に独自インタフェースを把握することができます。

残るは 静的ファイルのデプロイ部分です。


    new s3deploy.BucketDeployment(this, `DeployWithInvalidation-${name}`, {
            sources: [ s3deploy.Source.asset('../dist') ],
            destinationBucket: siteBucket,
            distribution,
            distributionPaths: ['/*'],
          });

s3へのデプロイ部分は、各案件のCIで行えば良いので目的外ですが、念のため触ってみました。

デプロイしたいディレクトリは s3deploy.Source.asset('../dist') にて設定してください。

s3のデプロイだけではなく、CloudFrontのinvalidationもやってくれているみたいですね。とても便利です。

適用

新しいリージョンで作成するときは、bootstrapコマンドが必要です

  $ cdk bootstrap
 ⏳  Bootstrapping environment aws://294573228326/us-east-1...
CDKToolkit: creating CloudFormation changeset...
 0/2 | 14:18:43 | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket 
 0/2 | 14:18:44 | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket Resource creation Initiated
 1/2 | 14:19:05 | CREATE_COMPLETE      | AWS::S3::Bucket | StagingBucket 
 2/2 | 14:19:07 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit 
 ✅  Environment aws://294573228326/us-east-1 bootstrapped.

あとは適用するだけ

$ cdk deploy
myapp-staging: deploying...
myapp-staging: creating CloudFormation changeset...
 0/6 | 15:21:08 | UPDATE_IN_PROGRESS   | AWS::S3::Bucket               | myapp-staging/SiteBucket-myapp-staging (myappstagingSiteBucketmyappstaging2423F624) 

...

 ✅  myapp-staging

Outputs:
myapp-staging.myappstagingBucket9532DE13 = myapp-staging
myapp-staging.myappstagingDistributionIdC319011 = E2FWKCKSZGCSM
myapp-staging.myappstagingSiteUrl0C438ED = https://d314dtcht4avx.cloudfront.net

このような表示になれば完了しています。

new cdk.CfnOutputで定義したものがデプロイ時に出力されていることがわかります。

最後に出力されているURLにアクセスしてデプロイされているか試してみましょう。どんなパスでアクセスしてもindex.htmlが出力されていると思います。

感想

  • 思ったより簡単にできた。
    • 手作業時の2,3倍ほどの時間で済んだので、十分ペイできそう
  • ドキュメントが少ないかもしれないが、開発上は気にならない
    • AWSが公式で大量のサンプルを公開している・tsとvscodeでの作業がスムーズなので
  • package.jsonにバージョン指定せずに記述することができることを知った
    • ワークショップを途中で1週間くらい間空けたさいに、バージョン違いでエラーが出て死ぬことがあったが、このしていないらバージョンのズレを防げる
"dependencies": {
  "@aws-cdk/aws-certificatemanager": "*",
  "@aws-cdk/aws-cloudfront": "*",
  "@aws-cdk/aws-iam": "*",
  "@aws-cdk/aws-route53": "*",
  "@aws-cdk/aws-route53-targets": "*",
  "@aws-cdk/aws-s3": "*",
  "@aws-cdk/aws-s3-deployment": "*",
  "@aws-cdk/core": "*",
  "aws-cdk": "*"
}

コード

こちら

cdk.json

{
    "app": "node index",
    "context": {
        "app_name": "myapp",
        "mode": "staging"
    }
}

index.ts

#!/usr/bin/env node
import cdk = require('@aws-cdk/core');
import { StaticSite } from './static-site';

class MyStaticSiteStack extends cdk.Stack {
    constructor(parent: cdk.App, name: string, props: cdk.StackProps) {
        super(parent, name, props);
        new StaticSite(this, name);
   }
}

const app = new cdk.App();


const appName = app.node.tryGetContext('app_name');
const mode = app.node.tryGetContext('mode');
const name = `${appName}-${mode}`

new MyStaticSiteStack(app, `${name}`, { env: {
    region: 'us-east-1'
}});

app.synth();

static-site.ts

#!/usr/bin/env node
import cloudfront = require('@aws-cdk/aws-cloudfront');
import s3 = require('@aws-cdk/aws-s3');
import s3deploy = require('@aws-cdk/aws-s3-deployment');
import cdk = require('@aws-cdk/core');
import { Construct } from '@aws-cdk/core';

/**
 * Static site infrastructure, which deploys site content to an S3 bucket.
 *
 * The site redirects from HTTP to HTTPS, using a CloudFront distribution,
 * Route53 alias record, and ACM certificate.
 */
export class StaticSite extends Construct {
    constructor(parent: Construct, name: string) {
        super(parent, name);

        // const zone = route53.HostedZone.fromLookup(this, 'Zone', { domainName: props.domainName });
        // const siteDomain = props.siteSubDomain + '.' + props.domainName;

        // Content bucket
        const siteBucket = new s3.Bucket(this, `SiteBucket-${name}`, {
            bucketName: name,
            websiteIndexDocument: 'index.html',
            websiteErrorDocument: 'index.html',
            publicReadAccess: true,
            removalPolicy: cdk.RemovalPolicy.DESTROY, // NOT recommended for production code
            // websiteRoutingRules: [
            //     {
            //         condition: {
            //             httpErrorCodeReturnedEquals: "404",
            //         },
            //         httpRedirectCode: "301",
            //         replaceKey: s3.ReplaceKey.with("indx.html")
            //     }
            // ]
        });
        new cdk.CfnOutput(this, 'Bucket', { value: siteBucket.bucketName });

        // // TLS certificate
        // const certificateArn = new acm.DnsValidatedCertificate(this, 'SiteCertificate', {
        //     domainName: siteDomain,
        //     hostedZone: zone
        // }).certificateArn;
        // new cdk.CfnOutput(this, 'Certificate', { value: certificateArn });

        // CloudFront distribution that provides HTTPS
        const distribution = new cloudfront.CloudFrontWebDistribution(this, `SiteDistribution-${name}`, {
            // aliasConfiguration: {
            //     acmCertRef: certificateArn,
            //     names: [ siteDomain ],
            //     sslMethod: cloudfront.SSLMethod.SNI,
            //     securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_1_2016,
            // },
            errorConfigurations: [
                {
                    errorCode: 403,
                    responseCode: 200,
                    responsePagePath: "/index.html"
                },
                {
                    errorCode: 404,
                    responseCode: 200,
                    responsePagePath: "/index.html"
                }
            ],
            originConfigs: [
                {
                    s3OriginSource: {
                        s3BucketSource: siteBucket
                    },
                    behaviors : [ {isDefaultBehavior: true}],
                }
            ]
        });
        new cdk.CfnOutput(this, 'DistributionId', { value: distribution.distributionId });

        // サイトURL表示
        new cdk.CfnOutput(this, 'SiteUrl', { value: 'https://' + distribution.domainName });


        // // Route53 alias record for the CloudFront distribution
        // new route53.ARecord(this, 'SiteAliasRecord', {
        //     recordName: siteDomain,
        //     target: route53.AddressRecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),
        //     zone
        // });

        // Deploy site contents to S3 bucket
        new s3deploy.BucketDeployment(this, `DeployWithInvalidation-${name}`, {
            sources: [ s3deploy.Source.asset('./site-contents') ],
            destinationBucket: siteBucket,
            distribution,
            distributionPaths: ['/*'],
          });
    }
}
tech  AWS  Vue.js 

See also