あるSEのつぶやき・改

ITやシステム開発などの技術に関する話題を、取り上げたりしています。

AngularのSPAをS3+CloudFront環境にデプロイしてキャッシュクリアする方法

はじめに

Angular な SPA を Amazon S3 + CloudFront にホストするのはいいですが、じゃあデプロイはどうするのとなって行き詰まっていました。

www.aruse.net

いろいろ調べてみたところ、Angular の SPA を自動でデプロイして CloudFront のキャッシュもクリアする方法が分かったので、メモ的な位置づけでその方法を残しておきます。

なお、環境は以下の通りで、AWS SDK はすでにインストール済みであることを前提にしています。

  • Windows 10
  • Angular 7.0.0
  • Node 10.12.0
  • Visual Studio Code

Angular プロジェクトの作成

適当な場所で、以下のコマンドを実行して Angular プロジェクトを作成します。

$ ng new SampleProject
$ cd SampleProject

必要なパッケージのインストール

S3 へのアップロード、および CloudFront のキャッシュをクリアするのに必要なパッケージをインストールします。

$ npm install aws-sdk --save-dev
$ npm install mime-types --save-dev
$ npm install node-uuid --save-dev

デプロイスクリプトの作成

プロジェクトのルートパスにscriptフォルダを作成し、そのフォルダの下にdeploy.jsを新規作成します。

そして、以下のコードを貼り付け、config の設定を書き換えます。

基本的に config を書き換えればうまくいくと思うのでコピペでもいいと思いますが、詳細を確認したい場合は参考サイトを参照してください。

なお、アクセスキー ID とシークレットアクセスキーは、外部に漏洩しないよう厳重に管理してください。

const AWS = require("aws-sdk"); // imports AWS SDK
const mime = require('mime-types') // mime type resolver
const fs = require("fs"); // utility from node.js to interact with the file system
const path = require("path"); // utility from node.js to manage file/folder paths
var uuid = require('node-uuid');

// configuration necessary for this script to run
const config = {
    s3BucketName: '{S3バケット名}',
    folderPath: '../dist/{プロジェクト名}',        // path relative script's location
    accessKeyId: '{アクセスキーID}',               // IAM user's AccessKeyID
    secretAccessKey: '{シークレットアクセスキー}',  // IAM user's SecretAccessKey
    DistributionId: '{CloudFrontのID}'            // DistributionId of CloudFront
};

/* S3 のファイルアップロード設定 */
// initialise S3 client
const s3 = new AWS.S3({
    signatureVersion: 'v4'
});

// resolve full folder path
const distFolderPath = path.join(__dirname, config.folderPath);

// Normalize \\ paths to / paths.
function unixifyPath(filepath) {
    return process.platform === 'win32' ? filepath.replace(/\\/g, '/') : filepath;
};

// Recurse into a directory, executing callback for each file.
function walk(rootdir, callback, subdir) {
    // is sub-directory
    const isSubdir = subdir ? true : false;
    // absolute path
    const abspath = subdir ? path.join(rootdir, subdir) : rootdir;

    // read all files in the current directory
    fs.readdirSync(abspath).forEach((filename) => {
        // full file path
        const filepath = path.join(abspath, filename);
        // check if current path is a directory
        if (fs.statSync(filepath).isDirectory()) {
            walk(rootdir, callback, unixifyPath(path.join(subdir || '', filename || '')))
        } else {
            fs.readFile(filepath, (error, fileContent) => {
                // if unable to read file contents, throw exception
                if (error) {
                    throw error;
                }

                // map the current file with the respective MIME type
                const mimeType = mime.lookup(filepath)

                // build S3 PUT object request
                const s3Obj = {
                    // set appropriate S3 Bucket path
                    Bucket: isSubdir ? `${config.s3BucketName}/${subdir}` : config.s3BucketName,
                    Key: filename,
                    Body: fileContent,
                    ContentType: mimeType
                }

                // upload file to S3
                s3.putObject(s3Obj, (res) => {
                    console.log(`Successfully uploaded '${filepath}' with MIME type '${mimeType}'`)
                })
            })
        }
    })
}


/* CloudFrond のキャッシュクリア設定*/
var cloudfront = new AWS.CloudFront({
  apiVersion: '2018-06-18',
  accessKeyId: config.accessKeyId,
  secretAccessKey: config.secretAccessKey,
});

var timestamp = new Date();
var string_timestamp = String(timestamp.getTime());

var invalidate_items = ['/*'];  // 全キャッシュクリア
var item_count = invalidate_items.length

var params = {
  DistributionId: config.DistributionId,
  InvalidationBatch: {
    CallerReference: string_timestamp,
    Paths: {
      Quantity: item_count,
      Items: invalidate_items
    }
  }
};

// S3にファイルアップロード実行
walk(distFolderPath, (filepath, rootdir, subdir, filename) => {
    console.log('Filepath', filepath);
});

// CloudFront キャッシュクリア実行
cloudfront.createInvalidation(params, function(err, data) {
  if (err) console.log(err, err.stack);
  else     console.log(data);
});

デプロイの設定

pakage.jsonscripts配下に、以下の2行を追加します(追加時に、一行前の末尾にカンマを付けるのを忘れないこと)。

    "predeploy": "ng build --prod --aot",
    "deploy": "node ./scripts/deploy.js"

デプロイの実行

以下のコマンドを実行することで、ビルド&デプロイ、キャッシュのクリアまで行うことができます。

$ npm run deploy

おわりに

ざっくりとした感じですが、このスクリプトで実際に動作させて問題ないことは確認済みです。

本番では CodeCommit などの Code シリーズと組み合わせた方がよいのでしょうが、開発段階ではとりあえずこれでも問題ないのではないかと思います。

なお、CloudFront の無効化(キャッシュクリア)は、1000回/月 を超えると課金されるのでスクリプト利用時は気をつけてください。

参考サイト