Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

NuxtアプリをSSGでビルドしてCI/CDをお手軽に設定する【GitHub Actions × Firebase Hosting】

はじめに

今回はNuxt.jsのSSG(静的サイトジェネレーター)モードで作成したアプリのCI/CDを設定する方法を紹介します!
GitHub ActionsとFirebase Hostingを連携させるとかなり手軽に設定できたのでNuxt/Vue初心者の方も実践できる内容だと思います。

対象読者

・実務でCI/CDを使った事が無い人
・CI/CDという言葉は聞いた事があるがよく分からない人
・Vue/Nuxtを触った事はあるがレンダリングの仕方までは気にした事がない (SSGってナニ?な人)
・firebaseがなんとなく分かる人

説明しない事

・firebase各種サービスの細かな説明
・テスト(Jest)の書き方

SSG (静的サイトジェネレーター)とは?

個人的にはこの記事の説明が分かりやすいと思います
Nuxt.jsを使うときに、SPA・SSR・SSGのどれがいいか迷ったら -Qiita

NuxtをSSGモードで始めると、ビルド時(nuxt generate実行時)にvueファイル内で呼ばれているAPIからデータを取得し、それに基づいたHTMLが生成されます。

ビルド時点のDBから取得した値がHTMLへ直書きされるので、DBの値が変わったとしても再度デプロイするまではHTML内の値が変わる事はありません。

特徴

・サーバーサイドとデータのやりとりをする必要が無いので
 1) レスポンスが速い
 2) セキュリティ面のリスクが軽減される

・データの更新をする為には都度ビルドする必要があるので、頻繁に更新するサイトには向かない (LPやポートフォリオには向いている)

CI/CDとは

ニフクラさんによると

「CI」とは「Continuous Integration(継続的インテグレーション)」の略で、ソフトウェア開発におけるビルドやテストを自動化し、継続的に行うアプローチのことです。
「CD」とは「Continuous Delivery(継続的デリバリー)」の略で、CIによってテストされたコードのマージや、本番環境向けのビルドの作成を自動的に行い、本番環境にデプロイが可能な状態を整えるプロセスのことです。

CI/CDとは | ニフクラ

簡単に言えば、CI/CDとは継続的にビルド/テスト/デプロイを行う事で、いつでも本番環境を更新できるような状態に保つ為の仕組みの事です。
近年の開発手法の主流である「アジャイル開発」の中で、リリース回数が多くなっても品質を下げない様にする為の施策であるとも言えます。

今回実装するCI/CDの動き

今回作成するシステムは以下の様になります
Untitled Diagram (1).png
mainブランチはリリース用のブランチなので、そこで作業することはありません。

手順
1. 機能を実装し、実装箇所のテストを書いたら、mainブランチへコードをpushします。
2. GitHub Actionsはmainブランチへのpushを検知して、ビルド、テストを実行します。
3. 2で問題がなければそのままFirebase Hostingへコードをデプロイします。
4. 問題があった場合(テスト/ビルドに失敗)は通知がされ、本番への反映はされません。

本編

※ Node.jsがインストールされていること、GitHubが使える事が前提

環境

Node v12.20.0
Nuxt v2.14.6
firebase v8.1.1


それでは実際に環境構築していこうと思います!
まず最初にGitHubでリポジトリを作成して、cloneしておきます。

$ git clone[作成したリポジトリ名]
$ cd[作成したリポジトリ名]


Nuxtアプリの作成

次にnuxtアプリを作成します。

npx create-nuxt-app [アプリ名]

いくつか質問に答えます。

create-nuxt-app v3.4.0
✨  Generating Nuxt.js project in '[アプリ名]'
? Project name: '[アプリ名]'
? Programming language: JavaScript
? Package manager: Npm
? UI framework: None
? Nuxt.js modules: Axios
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: jsconfig.json
? Continuous integration: None
? Version control system: None

create-nuxt-appが完了したら、生成したファイルをルートディレクトリに移動します。

# ファイルをルートに移動
mv [アプリ名]/{*,.*} .
# 空になったディレクトリを削除
rm -rf [アプリ名]

最後にfirebaseのパッケージをインストールします。
npm i firebase

firebaseのセットアップ

プロジェクトの作成

https://console.firebase.google.com

firebaseコンソールにてプロジェクトを作成してください。
初めての方はGmailのアカウントが必要になります。
無事作成できると、コンソールページに入れるようになります。

スクリーンショット 2020-12-11 20.20.40.png

Cloud Firestoreの設定

次にCloud Firestoreのページへ移動し、データベースを作成します。
今回はテストモードで開始しますが、運用の際にはルールを設定してください。
image.png

データベースが作成されたら、適当なレコードを登録しておきます。
自分はusersコレクションにnameを持ったドキュメントをいくつか作成しました。

スクリーンショット 2020-12-11 20.25.43.png

これが今回扱うデータになります。

アプリにFirebaseを追加

自分のアプリ(今回はNuxt)でfirebaseを使うための設定をしていきます。
「プロジェクトを設定」から「ウェブアプリに Firebase を追加」を選択します。

手順が表示されるので、この通りに進めていきます。

image.png

 1.「このアプリのFirebase Hostingも設定します」にチェックを入れてください。
 2. モジュールバンドラを使用するので「FirebaseSDKの追加」は飛ばしてください。
 4. firebase initでいくつかの質問を答える事になります。以下の様に回答してください。

? Which Firebase CLI features do you want to set up for this folder?:
 > firestore/hosting
? Please select an option: 
 > Use an existing project
Select a default Firebase project for this directory:
 > [先ほど作成したプロジェクト]
? What file should be used for Firestore Rules?
 > firestore.rules
? What file should be used for Firestore indexes? 
 > firestore.indexes.json
What do you want to use as your public directory? 
 > dist
? Configure as a single-page app (rewrite all urls to /index.html)? 
 > No
? Set up automatic builds and deploys with GitHub? 
 > Yes # GitHubの認証が必要になります
? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) 
 > [今回使っているリポジトリ]
? Set up the workflow to run a build script before every deploy? 
 > Yes
? What script should be run before every deploy?
 > npm ci & npm run build
? Set up automatic deployment to your sites live channel when a PR is merged?
 > Yes
? What is the name of the GitHub branch associated with your sites live channel?
  > main

後半はCI/CDの設定についてです (後述)
最後にfirebase deploy --only hosting:[アプリ名]を実行とありますが、これはデプロイ用のコマンドであり、今のままではうまくいきません。後ほど設定をします。

テストの確認

まずデプロイの前に、実装したコードに不備が無いかテストを実行します。
デフォルトでtestディレクトリに Logo.spec.js があると思います。
今回は説明の為にこのファイルのみテストします。

Logo.vueがvue instanceであるかのチェックです。
npm run test を実行すればテストが実行されます。

image.png

jestを使ってテストをしています。
1 passed, 1 total とあるように、合計1つのテストケースのうち、1つのテストがpass(成功)しました。
コードorテストに不備があるとエラーが起きます。

このようにしてデプロイ(本番反映)前にテストを実行する事でバグを未然に防げます。ただし、テストを実行し忘れてデプロイしてしまう可能性もあるので、CI/CDを使ってデプロイの度に自動テストをする仕組みを作成します。


本番環境へデプロイ

テストが成功した事を確認したら、本番環境へデプロイしていきます。

まずビルド用のコマンド、

npm run generate

を実行します。

するとdist配下に静的ファイルが作成されます。
このdistの中身が、今回Firebase Hostingによってホスティングされる静的ファイル群です。

ビルドが成功したら、デプロイ用のコマンド、

firebase deploy --only hosting

を実行してみてください。

完了するとHosting URL:[URL]と最後に表示されるので、アクセスしてみます。
image.png

無事アクセスできました!

Nuxt × firestoreでSSGを試す

それではNuxt側でfirestoreにアクセスし、データを取得/表示してみます。

firebaseSDKを使う

まずはfirebaseSDKをNuxtで使うために、firebaseを初期化するためのpluginファイルを作成します。

plugins配下にfirebase.jsを作成します。
firebaseConfigの部分は自分のものに変えてください。


firebaseConfigの取得方法
コンソールにログインして、
「プロジェクトを設定」→ 「マイアプリ」 → 「Firebase SDK snippet」
と進み、「CDN」を選択するとコードが表示されます。
firebaseConfigの部分だけコピーします。
スクリーンショット 2020-12-11 21.42.23.png


plugins/firebase.js
import firebase from 'firebase'

const firebaseConfig = {
  apiKey: 'Your apiKey',
  authDomain: 'Your authDomain',
  projectId: 'Your projectId',
  storageBucket: 'Your storageBucket',
  messagingSenderId: 'Your messagingSenderId',
  appId: 'Your appId',
}

if (!firebase.apps.length) {
  firebase.initializeApp(firebaseConfig)
}

export default firebase



これ以降はNuxt側で

import firebase from '~/plugins/firebase'

と書く事で、先ほど作成したfirebaseプロジェクトを利用できるようになります!

asyncDataとmountedを比較

SSGでレンダリングする場合には、asyncData内でAPIへリクエストを送り、レスポンスのPromiseを{key:value}形式で返します。
keyにはtemplate内で使いたい名前を、valueにはPromiseを入れます。

実際にindex.vueを書き換えて、SSGとSPAでの挙動の違いを確認してみます。

index.vue
<template>
  <div class="container">
    <div>
      <h2>Users with asyncData (SSG)</h2>
      <p v-for="(user, i) in asyncUsers" :key="'user-async' + i">
        {{ user.name }}
      </p>

      <h2>Users with mounted</h2>
      <p v-for="(user, i) in mountedUsers" :key="'user-mounted' + i">
        {{ user.name }}
      </p>
    </div>
  </div>
</template>

<script>
import firebase from '~/plugins/firebase'
export default {
  data() {
    return {
      mountedUsers: [],
    }
  },
  async asyncData() {
    const res = await firebase
      .firestore()
      .collection('users')
      .get()
      .then((querySnapshot) => {
        const array = []
        querySnapshot.forEach((doc) => {
          array.push(doc.data())
        })
        return array
      })
      .catch((e) => console.log(e))
    return { asyncUsers: res }
  },
  async mounted() {
    await firebase
      .firestore()
      .collection('users')
      .get()
      .then((querySnapshot) => {
        querySnapshot.forEach((doc) => {
          this.mountedUsers.push(doc.data())
        })
      })
      .catch((e) => console.log(e))
  },
}
</script>

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

h2 {
  margin: 16px 5px;
}
p {
  margin: 5px 0px;
}
</style>



npm run devで確認してみます。

asyncDataはビルド時に実行されていて、DBから取得した値がHTMLに直書きされている状態なので、その都度取得するmountedより表示速度が速い事が分かります。

test666.gif

SSGを利用している部分をデプロイ

今回新しい実装をしたので、再度本番環境へ反映させます。

手順としてはざっくり、

テストの実行 (本来なら変更箇所のテストを追加作成) → npm run generate
ビルドの実行npm run generate
デプロイの実行firebase deploy --only hosting

となると思います。

ただし、
毎回これらの手順を人の手で実行するのは面倒である事はもちろん、テストを実行し忘れたままデプロイしてしまう等、本番環境での不具合にも繋がります。

本番環境への反映をシステム化する為にもCI/CDを導入します。

以上の点を踏まえて、CI/CDを設定して変更内容をデプロイします!

CI/CDで自動テスト&自動デプロイをする

デプロイの設定は既に完了しています。

firebase init

を実行した時にいくつか質問に答えたと思います。
この質問の後半での回答がCI/CDの設定になります。

? Set up automatic builds and deploys with GitHub? 
 > Yes
? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) 
 > [今回使っているリポジトリ] #GitHub Actionsを設定するリポジトリ
? Set up the workflow to run a build script before every deploy? 
 > Yes
? What script should be run before every deploy?
 > npm ci & npm run build #デプロイ時に実行するコマンドを指定
? Set up automatic deployment to your sites live channel when a PR is merged?
 > Yes # マージされた時も自動デプロイを有効にするかどうか
? What is the name of the GitHub branch associated with your sites live channel?
  > main #ここで指定したブランチへpushした時にGitHub Actionsが走ります

mainブランチへのpushをGitHub Actionsが検知し、指定したJobを実行してくれるようになります。

ビルド、テストの設定についてはもう少し手を加える必要があります。

では、実行するJobを編集していきます。
CI/CDの定義はymlファイルに記述します。

既存のnpm ci & npm run buildを削除して、以下のように編集します。

.github/workflows/firebase-hosting-merge.yml
-      - run: npm ci & npm run build
+      - name: Cache dependencies
+        uses: actions/cache@v1
+        with:
+          path: ~/.npm
+          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+          restore-keys: |
+            ${{ runner.os }}-node-
+
+      - name: Install dependencies
+        run: npm ci
+        
+      - name: Run test
+        run: npm test
+        
+      - run: npm run generate

それでは、変更内容をmainブラントへpushしてみましょう!

pushしたらGitHub Actionsをみてみます。
リポジトリのActionsタブにワークフローが表示されていると思います。

image.png


詳細をみてみると、先ほど定義したjobが実行されている事が分かります。

image.png



ステータスが緑のチェックになっていれば成功です!
image.png
これ以降はmainブランチに変更内容をpushするだけで、本番環境へ反映されるようになります。



ここでテストが通らなければデプロイに失敗します。

例えば以下のように変更すればテストが通らず、デプロイにも失敗します。

- expect(wrapper.vm).toBeTruthy()
+ expect(wrapper.vm).toBeFalsy()

まとめ

今回扱った技術については理解が曖昧な部分があったので、自分としてもまとめられて良かったです!
何よりSSGが使いやすくて好きになりました!
ポートフォリオサイトに最適だと思ったので、今回の構成で作り直してみようと思います。
SSR/ISRも試してみよう。


22卒の学生エンジニアです。
Twitterもやってるのでフォローお願いします!
https://twitter.com/1keiuu

ikkei12
都内のスタートアップでインターンをしている学生です。 Twitterのフォローもお願いします!
https://1k-cove.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away