CircleCIでSassをCSSに変換してS3へデプロイする

# これは何？
CircleCIのジョブでSassをCSSにトランスパイルし、ミニファイしてS3へデプロイするために必要な設定をまとめたものです。

# ゴール
my_repositoyリポジトリの`assets/css/style.scss`を`style.min.css`にトランスパイル＆ミニファイし、S3のproduction-bucketに`assets/css/style.min.css`としてデプロイします。

# デプロイパイプライン
<img width="1149" alt="CircleCI_-_CircleCI.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/249168/f6f14ee1-b3f6-9dce-4ec5-b2260869a92e.png">

- リポジトリへプルリクエスト
  - この時点で実行されるジョブでSass→CSSトランスパイルが実行される
  - その結果をステージング用のS3バケットにデプロイする
- マージされる
  - masterブランチなら、本番用とステージング用S3バケットにそれぞれデプロイ
  - それ以外のブランチなら、ステージング用のバケットにデプロイ

# 必要な設定
## CSSトランスパイル
トランスパイル用のタスクランナーとしてgulpを利用します。

```js:gulpfile.js
'use strict';

const gulp = require('gulp');
const sass = require('gulp-sass');
const workDir = './assets/css';
sass.compiler = require('node-sass');

const compileSass = (cb) => {
  const options = {
    outputStyle: "expanded",
  };

  gulp
    .src(`${workDir}/style.scss`)
    .pipe(sass(options))
    .pipe(gulp.dest(workDir));

  cb();
}

const compileSassCompressed = (cb) => {
  const rename = require('gulp-rename'); // ファイル名にminを付けたいのでそれ用のモジュールを使います
  const options = {
    outputStyle: "compressed", // ここで圧縮指定
  };

  gulp
    .src(`${workDir}/style.scss`)
    .pipe(sass(options))
    .pipe(rename({
      suffix: ".min",
    }))
    .pipe(gulp.dest(workDir));

  cb();
}

exports.compileSass = compileSass; // こっちは使っていません
exports.default = compileSassCompressed;
```

実行は以下をビルドの過程に入れておきます。

```shell-session
$ npx gulp
# npx便利ですねー
```

### 参考
- [公式 Quick Start](https://gulpjs.com/docs/en/getting-started/quick-start/)
- [公式 Creating Tasks](https://gulpjs.com/docs/en/getting-started/creating-tasks/)
- [絶対つまずかないGulp 4入門(2019年版)
インストールとSassを使うまでの手順](https://ics.media/entry/3290/)

## CircleCI
- S3へのデプロイは[circleci/aws-s3 Orb](https://circleci.com/orbs/registry/orb/circleci/aws-s3)を利用して楽してます
  - デプロイ時に、パブリックアクセスを許可させるためには以降に記載のバケット設定をする必要がありました

```yaml:CircleCI用コンフィグ
version: 2.1
orbs:
  aws-s3: circleci/aws-s3@3.0

# 本番用デプロイとステージング用デプロイジョブそれぞれで同じ処理をするので、デプロイ処理をコマンド化しています
commands:
  deploy-to-s3:
    description: "Deploy Assets to S3"
    parameters:
      bucket_name:
        type: string
    steps:
      - checkout

      - restore_cache:
          keys:
            - my_repository-css-v0-{{ .BuildNum }}
            - my_repository-css-v0-

      # Deploy CSS assets to S3
      - aws-s3/copy:
          arguments: |
            --acl public-read \
            --cache-control "max-age=604800"
          from: assets/css/style.min.css
          to: "s3://<< parameters.bucket_name >>/assets/css/style.min.css"

# 本番用デプロイとステージング用デプロイジョブで同じDockerイメージを使うのでこっちもエグゼキューターとして定義しています
executors:
  deploy-executor:
    docker:
      # aws cliを使うので、pythonがないとだめです
      - image: cimg/python:latest

jobs:
  build:
    parallelism: 1
    working_directory: ~/my_repository
    docker:
      - image: circleci/node:9.11.2
    steps:
      - checkout

      - restore_cache:
          keys:
            - my_repository-npm-v0-{{ checksum "package-lock.json" }}
            - my_repository-npm-v0-

      - run:
          name: npm Install
          command: npm install

      - save_cache:
          key: my_repository-npm-v0-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules

      - run:
          name: Run Sass Lint
          command: ./node_modules/.bin/sass-lint -c config/.sass-lint.yml --verbose --no-exit

      - run:
          name: Compile Scss
          command: npx gulp

      - save_cache:
          key: my_repository-css-v0-{{ .BuildNum }}
          paths:
            - assets

      - store_test_results:
          path: test_results

  deploy-as-staging:
    working_directory: ~/my_repository
    executor: deploy-executor
    steps:
      - deploy-to-s3:
          bucket_name: "staging-bucket"

  deploy-as-production:
    working_directory: ~/my_repository
    executor: deploy-executor
    steps:
      - deploy-to-s3:
          bucket_name: "production-bucket"

workflows:
  build_and_deploy:
    jobs:
      - build
      - deploy-as-staging:
          requires:
            - build
      - deploy-as-production:
          requires:
            - build
          filters:
            branches:
              only: master
```

### 参考
- [circleci/aws-s3](https://circleci.com/orbs/registry/orb/circleci/aws-s3)

## AWS側の設定
- IAM
  - deployグループを作成し、S3フルアクセスを付与
  - circleci-userユーザを作成し、deployグループに所属
- S3
  - デプロイ先のバケットを作成
  - ブロックパブリックアクセス (バケット設定)を「（新しい/任意の）アクセスコントロールリスト (ACL) を介して許可されたバケットとオブジェクトへのパブリックアクセスをブロックする」を**オフ**
  - バケットポリシーを以下にする


```json:バケットポリシー
{
    "Version": "2012-10-17",
    "Id": "Policy1583424049389",
    "Statement": [
        {
            "Sid": "Stmt1583424044685",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::アカウント名:user/circleci-user"
            },
            "Action": [
                "s3:DeleteObject",
                "s3:PutObject",
                "s3:PutObjectAcl"
            ],
            "Resource": "arn:aws:s3:::production-bucket/*"
        }
    ]
}
```

  - CORS設定を以下にする
     - アプリのドメインとS3のドメインが異なる場合は必要

```xml:CORSの設定
<CORSConfiguration>
 <CORSRule>
   <AllowedOrigin>*</AllowedOrigin>
   <AllowedMethod>GET</AllowedMethod>
 </CORSRule>
</CORSConfiguration>
```

### 参考
- [公式 Amazon S3 パブリックアクセスブロックの使用](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/access-control-block-public-access.html)
- [公式 S3 Cross-Origin Resource Sharing (CORS)](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/cors.html)