BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

Git(Hub)+CircleCI+Slack で実現する静的コンテンツ配信システム

BASEでエンジニアリングマネージャーを担当している加賀谷です。普段は採用に携わったり、1on1での経験学習の促進などを通じて、個人と組織のアウトプットが大きくなるようにサポートする仕事をしています。また、サービス開発に関わる体験を良くしていくこともしています。その中で今回は、静的コンテンツのCI/CDでしていることを紹介したいと思います。

静的コンテンツのホスティング

静的コンテンツは、サーバサイドでリクエストに応じてレスポンスする内容を作成しないデータです。主に、サイト内で使う画像、CSS、JS、ランディングページなどのHTMLファイルになります。これらのファイルはよく、AWSのS3に置いてホスティングして前段にはCDNを配置し、Webブラウザの同時接続数を考慮してサービスとは別のホストに分散したりしますが、BASEでもそうしています。

f:id:yuhei_kagaya:20181030205749p:plain

静的コンテンツ用のGitリポジトリを用意

CSSやJSが成果物となる開発はBASEにおいては主にデザイナーとフロントエンドエンジニアが担っています。以前はPHPもJSもCSSも同じリポジトリで開発していたのですが、今ではサービスのメインGitリポジトリとは別のリポジトリで開発〜デプロイをするようにしています。もちろんメインGitリポジトリからいっさいのJSやCSSを無くしているわけではなく、TwitterのBootstrapのようにコンポーネントとなるCSSやJS、ランディングページなど、メインから独立できるファイル群をこのような別リポジトリに入れています。

git pushでCircleCIからaws s3 syncする

デプロイ先はS3で、git push を契機にCircleCI上から aws s3 sync しています。以前は手動で本番S3に画像をアップロードする属人的な場面も少なからずあったのですが、今ではリポジトリの中に画像を入れてCircleCI経由でS3に配置するようにしています。

f:id:yuhei_kagaya:20181030205912p:plain

開発ワークフローと環境別のURL

静的コンテンツリポジトリの開発ワークフローはメインのGitリポジトリと同じようにしています。

  1. develop ブランチから開発用ブランチfeature/xxx を切って開発
  2. developへプルリクエスト&マージ
  3. リリース時は develop から mastergit-pr-release でリリース用プルリクエストを作成
  4. masterを本番環境へデプロイ

環境ごとに静的コンテンツのURLが欲しいので、S3はそれぞれ用意してCircleCIが回るブランチでaws s3 sync 先を変えています。 develop ブランチの時にはステージング用のS3、master ブランチの時には本番、それ以外のブランチは開発用S3へ対応させています。

f:id:yuhei_kagaya:20181030205930p:plain

.circleci/config.yml

CircleCIではだいたい以下のようなことをしています。

  1. checkout
  2. awscliをインストール
  3. リポジトリ内の特定ディレクトリ配下の各ファイルにACLとcache-control、content-typeをつけてaws s3 sync
  4. syncしたファイルのパスのCloudFrontのキャッシュを削除

また、アップロードされたファイルのURLなどをSlackへ通知して気付けるようにもしています。

version: 2
jobs:
  build:
    docker:
      - image: docker:17.12.0-ce-git
    environment:
      - TZ: "/usr/share/zoneinfo/Asia/Tokyo"
      - SYNC_OPTIONS: "--cache-control \"max-age=86400\" --acl public-read --size-only --no-progress --delete"
      - S3: "static-example-net"
    steps:
      - checkout
      - run:
          name: Install dependencies
          command: |
            apk add --no-cache \
              py-pip=9.0.1-r1 \
              curl \
              curl-dev \
              openssl
            pip install \
              awscli==1.14.40
      - run:
          name: Upload files to S3
          command: |
            set -x
            tmpfile=`mktemp`
            for i in `cat .s3ignore | grep -v "^#"`
            do
                IGNORE_OPTIONS="${IGNORE_OPTIONS} --exclude \"**/${i}\""
            done
            # リポジトリ内webroot/配下の各ファイルを適切なcontent-typeをつけてsync
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.css "${IGNORE_OPTIONS}" text/css` | tee -a $tmpfile
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.js "${IGNORE_OPTIONS}" application/javascript` | tee -a $tmpfile
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.json "${IGNORE_OPTIONS}" application/json` | tee -a $tmpfile
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.html "${IGNORE_OPTIONS}" text/html` | tee -a $tmpfile
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.png "${IGNORE_OPTIONS}" image/png` | tee -a $tmpfile
            eval `printf "aws s3 sync webroot/ s3://%s/ %s %s --exclude \"*\" --include \"%s\" %s --content-type \"%s\"" "${S3}" "${AWS_PROFILE}" "${SYNC_OPTIONS}" *.jpg "${IGNORE_OPTIONS}" image/jpeg` | tee -a $tmpfile
            if [ -s $tmpfile ]; then
              tmpdir=`mktemp -d`
              split -l 30 $tmpfile $tmpdir/
              for splited in `find $tmpdir -maxdepth 1 -type f`; do
                # CloudFrontからのキャッシュを削除
                PATHS=`grep -ao "s3://${S3}/.*$" ${splited} | sed "s/^s3:\/\/${S3}//g"`
                aws cloudfront create-invalidation ${AWS_PROFILE} --distribution-id ${CDN_DISTRIBUTION_ID} --paths ${PATHS}
              done
            fi

サーバサイドのリポジトリと分けて開発、デプロイするメリット

1日に何度も本番環境へデプロイをする状況下においても、1度にデプロイするコード量が減り確認範囲が狭くなることで、よりカジュアルにデプロイできることがメリットかなと思います。サーバサイドと同じリポジトリで開発していたときには、例えばランディングページのちょっとしたスタイル変更にも、CircleCIでサーバサイドの全テストを回してBlue-Green Deploymentリリースフローをする流れを必要とするために比較的変更の反映に時間がかかっていましたが、これもなくなり手続き的にも早いデプロイができるようになりました。

Slackからgit-pr-release

リリース用プルリクエストを作るときに、git-pr-release コマンドを実行していますが、これをSlack Botでもできるようにしています。スマホのSlackアプリからも実行できるので便利です。GitHubのアカウントをSlackのアカウントに変換してメンションさせたり、リリースのサマリをSlackへ通知して変更がざっくり共有できるように工夫しています。

1. Botにメンションするとセレクトボックスを返すのでデプロイするリポジトリを選ぶ

f:id:yuhei_kagaya:20181031130502p:plain

2. 選択したリポジトリをその場で確認される

f:id:yuhei_kagaya:20181030210242p:plain

3. Yesを押すとgit-pr-releaseを実行

f:id:yuhei_kagaya:20181030210340p:plain

4. スレッドでメンションが返ってくる

f:id:yuhei_kagaya:20181030210552p:plain

f:id:yuhei_kagaya:20181030210639p:plain

5. 作成されたリリース用プルリクエストのURLが通知されるので、確認してmasterマージするとCircleCI経由で本番デプロイ

f:id:yuhei_kagaya:20181030210709p:plain

まとめ

今回は、GitHubとCircleCIでS3へデプロイするワークフローを紹介しました。まだリポジトリ内には画像のようなバイナリファイルがあまり多くないのでリポジトリが肥大化してgit pull/pushが苦になることはありませんが、多くなってきたら Git LFS も検討してみたいと思います。

BASEの開発チームでは良いサービスをつくっていくために開発体験の改善も楽しみながら活動しています。ご興味を持たれた方いらっしゃいましたら是非ご連絡いただければ幸いです。

jobs.binc.jp