BLOG ブログ


2020.03.03 TECH

Ansible で デプロイの自動化を目指す / Vue.js のビルド済みファイルアップロードの場合

こんにちは。tacckです。

前回 に引き続き、今回も Ansible を使った記事です。
前回は HTML ファイル1つのアップロードでしたが、今回はディレクトリをアップロードする方法を試していきたいと思います。

準備

基本的な準備事項は、前回の記事を確認お願いします。

その他の前提として、自分のPCで Node.js が実行できることが必要です。
(npmyarn も実行できるようにしてください。)
また、Vue CLI を使ってプロジェクトを作成しているので、こちらも必要です。

Vue.js プロジェクト作成

では、まず Vue CLI を使って Vue.js のプロジェクトを作成しましょう。

$ pwd
/[YOUR_HOME]/sample-ansible
$ vue create site


Vue CLI v4.2.3
? Please pick a preset: default (babel, eslint)


Vue CLI v4.2.3
✨  Creating project in /[YOUR_HOME]/sample-ansible/site.
🗃  Initializing git repository...
⚙️  Installing CLI plugins. This might take a while...

yarn install v1.21.1
info No lockfile found.
[1/4] 🔍  Resolving packages...



success Saved lockfile.
info To upgrade, run the following command:
$ curl --compressed -o- -L https://yarnpkg.com/install.sh | bash
✨  Done in 37.42s.
🚀  Invoking generators...
📦  Installing additional dependencies...

yarn install v1.21.1
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Saved lockfile.
✨  Done in 5.23s.
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project site.
👉  Get started with the following commands:

 $ cd site
 $ yarn serve

$

作成されるファイルは、下記のようになります。
(site/node_modules 以下のファイル記載は省略)

site
├── README.md
├── babel.config.js
├── node_modules
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
├── src
│   ├── App.vue
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   └── HelloWorld.vue
│   └── main.js
└── yarn.lock

プロジェクトのビルド

今回はこのままプロジェクトをビルドして、リリース用の静的ファイル群を作成します。
下記のように実行してみてください。

$ cd site/
$ yarn build
yarn run v1.21.1
$ vue-cli-service build

⠋  Building for production...

 DONE  Compiled successfully in 2814ms                                                                                              13:23:34

  File                                 Size               Gzipped

  dist/js/chunk-vendors.9804a263.js    89.13 KiB          31.92 KiB
  dist/js/app.3d469bfd.js              4.62 KiB           1.65 KiB
  dist/css/app.fb0c6e1c.css            0.33 KiB           0.23 KiB

  Images and other types of assets omitted.

 DONE  Build complete. The dist directory is ready to be deployed.
 INFO  Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
      
✨  Done in 9.00s.
$

うまくできたら、 dist というディレクトリが作られているはずです。

Ansible で自動化 / Vue.js のビルド済みファイル

Ansible Role 作成

まず、簡単に前回のおさらいです。

前回は、サーバのセッティング (Apache HTTP Server) とファイルのアップロードを一つの Role で作成していました。
しかし、これらは本来は意味が異なるものなので、分割しましょう。

サーバのセッティング Role

早速、 Role 内のタスクを整理していきます。

---
- name: set timezon Asia/Tokyo
  shell: timedatectl set-timezone Asia/Tokyo

- name: install httpd
  yum:
    name: httpd
    state: latest

- name: make directory "/var/www/html/dist"
  file:
    path: /var/www/html/dist
    state: directory

- name: upload site.conf
  copy:
    src: site.conf
    dest: /etc/httpd/conf.d

- name: enable httpd
  systemd:
    name: httpd
    enabled: yes

- name: restart httpd
  systemd:
    name: httpd
    state: restarted

前回記述していた、 htmlファイルのアップロードタスクを削除しています。

変わって、install httpd の後に二つのタスクを追加しました。(10〜18行目)

これは次のファイルアップロードに関わるのですが、 Ansible の copy モジュールはディレクトリ名を変更するようなアップロードが難しいため、事前の準備として必要なものとなります。

upload site.conf は、そのまま site.conf という Apache HTTP Server の設定ファイルのアップロードです。

先に挙げたように、ディレクトリ名を変更するようなアップロードができないため、 dist ディレクトリをアップロードすると /var/www/html/dist というディレクトリにファイル群が格納されることになります。
そのため、そのディレクトリをWebサーバの DocumentRoot として設定し直す必要があります。

下記のように記載しておくと、大丈夫です。

DocumentRoot /var/www/html/dist

ただし、指定されたディレクトリがサーバ上に存在していない場合、 Apache HTTP Server は起動に失敗してしまいます。
そのため、 make directory "/var/www/html/dist" で事前にディレクトリを作成しています。
(すでに存在している時は、何も起こらずに結果は成功となります。)

ファイルのアップロード Role

ロールを使っているんですが、アップロードしたいファイル群はロールの外にあります。
そのため、実行している場所 (実際は指定した Inventory ファイルのあるディレクトリ)を指定するために inventory_dir を使っています。
これによって、後から追加した Vue.js のプロジェクトの dist ディレクトリを指定することができます。

---
- name: upload dist directory
  copy:
    src: "{{ inventory_dir }}/site/dist"  
    dest: /var/www/html

これで、想定通りの /var/www/html/dist へ、 dist ディレクトリの中身をアップロードできます。

実行 Role の指定

上で分離した二つのロールを実行するように、設定しましょう。

---
- hosts: web
  become: yes
  roles:
    - setups
    - upload

最終的に、下記のようなファイル構成になります。

.
├── ansible.cfg
├── inventory
├── main.yml
├── roles
│   ├── setups
│   │   ├── files
│   │   │   └── site.conf
│   │   └── tasks
│   │       └── main.yml
│   └── upload
│       └── tasks
│           └── main.yml
├── site
├── ssh
│   └── web_testing.pem
└── tests
    ├── index.test.js
    ├── node_modules
    ├── package.json
    └── yarn.lock

実行

では、実行しましょう。

$ ansible-playbook -i inventory main.yml 

PLAY [web] *********************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************
ok: [xx.xx.xx.xx]

TASK [setups : set timezon Asia/Tokyo] *****************************************************************************************************
changed: [xx.xx.xx.xx]

TASK [setups : install httpd] **************************************************************************************************************
changed: [xx.xx.xx.xx]

TASK [setups : make directory "/var/www/html/dist"] ****************************************************************************************
changed: [xx.xx.xx.xx]

TASK [setups : upload site.conf] ***********************************************************************************************************
changed: [xx.xx.xx.xx]

TASK [setups : enable httpd] ***************************************************************************************************************
changed: [xx.xx.xx.xx]

TASK [setups : restart httpd] **************************************************************************************************************
changed: [xx.xx.xx.xx]

TASK [upload : upload dist directory] ******************************************************************************************************
changed: [xx.xx.xx.xx]

PLAY RECAP *********************************************************************************************************************************
xx.xx.xx.xx                : ok=8    changed=7    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

無事に成功しました!

サイト確認

実際にブラウザで確認してみましょう。
Lightsail の IPアドレスにブラウザでアクセスしてみてください。

確かに、 Vue.js の初期画面が表示されました!

ビルドも自動化

さて、せっかくなのでアップロード前のビルドも、アップロードの度に手でやるのはよくないですよね。
これも自動化してしまいましょう。

こちらについては、私の実行環境である macOS のみで動作確認をしています。他のOSではうまく行かない可能性もあるので、ご了承ください。

Vue.js のビルド Role

一度手動で実行したように、 yarn build を実行できればビルドは完了します。
そのため、そのようなタスクを作成しましょう。

---
- name: buid vue files
  shell:
    chdir: site
    cmd: yarn build

やりたいことそのままな感じですね。

実行 Role の指定

では、ビルド Role を指定しましょう。
今回は、 "自分のPC" で実行するので、少し書き方が変わります。

---
- hosts: localhost
  connection: local
  roles:
    - vue-build

- hosts: web
  become: yes
  roles:
    - setups
    - upload

2〜5行目が、今回追加した部分です。

このように、 hosts: localhostconnection: local とすることで、自分のPCで直接 Role を実行することができるようになります。

実行

では実行、、ですが、その前にきちんと更新されることを確認するために、 Vue.js 内のファイルを少し修正しておきましょう。

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Uploaded by Ansible"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

4行目、元々 "Welcome to Your Vue.js App" だった部分を "Uploaded by Ansible" へと変更しました。

これで実行してみましょう。

$ ansible-playbook -i inventory main.yml 

PLAY [localhost] ***************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************
ok: [localhost]

TASK [vue-build : buid vue files] **********************************************************************************************************
changed: [localhost]

PLAY [web] *********************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************
ok: [xx.xx.xx.xx]

TASK [setups : set timezon Asia/Tokyo] *****************************************************************************************************
changed: [xx.xx.xx.xx]

TASK [setups : install httpd] **************************************************************************************************************
ok: [xx.xx.xx.xx]

TASK [setups : make directory "/var/www/html/dist"] ****************************************************************************************
ok: [xx.xx.xx.xx]

TASK [setups : upload site.conf] ***********************************************************************************************************
ok: [xx.xx.xx.xx]

TASK [setups : enable httpd] ***************************************************************************************************************
ok: [xx.xx.xx.xx]

TASK [setups : restart httpd] **************************************************************************************************************
changed: [xx.xx.xx.xx]

TASK [upload : upload dist directory] ******************************************************************************************************
changed: [xx.xx.xx.xx]

PLAY RECAP *********************************************************************************************************************************
xx.xx.xx.xx                : ok=8    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

$

無事に成功しました!

サイト確認

では、再びブラウザで確認してみましょう。
Lightsail の IPアドレスにブラウザでアクセスしてみてください。

きちんと、 "Uploaded by Ansible" と表示されました!
これで、 Vue.js のビルド忘れも無くなります。

テストを書く

突然ですが、我々はエンジニアリングというサバンナにいます。
ここでは、テストを書かないと生き残ることができません。

というわけで、テストをアップデートしましょう。

const TARGET_URL = 'http://xx.xx.xx.xx'
const CONTENTS = 'Uploaded by Ansible'

const axiosBase = require('axios')
const axios = axiosBase.create({
  baseURL: TARGET_URL,
})

describe('check top page', () => {
  it('get top page', async () => {
    const response = await axios.get('/')
    expect(response.status).toEqual(200)
  })

  it('check contents', async () => {
    const response = await axios.get('/')
    const reg = new RegExp(CONTENTS, 'i')
    expect(reg.test(response.data)).toEqual(true)
  })
})

2行目を、今回修正した内容に置き換えてみます。
では、テストを実行してみましょう。

$ cd tests
$ yarn test
yarn run v1.21.1
$ jest index.test.js
 FAIL  ./index.test.js
  check top page
    ✓ get top page (121ms)
    ✕ check contents (106ms)

  ● check top page › check contents

    expect(received).toEqual(expected) // deep equality

    Expected: true
    Received: false

      16 |     const response = await axios.get('/')
      17 |     const reg = new RegExp(CONTENTS, 'i')
    > 18 |     expect(reg.test(response.data)).toEqual(true)
         |                                     ^
      19 |   })
      20 | })
      21 | 

      at Object.<anonymous> (index.test.js:18:37)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        1.394s
Ran all test suites matching /index.test.js/i.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
$

おっと、失敗してしまいました。
内容から、先ほどの 'Uploaded by Ansible' の部分がうまく取得できていないことがわかります。

console.log(response.data) などを差し込んでみるとわかりますが、今回の Vue.js で作ったサイトは、 JavaScript が実行可能なクライアントでアクセスしないとうまく動作しません。

そのため、 Axios の変わりとしてヘッドレスブラウザである Puppeteer を使うようにしてみましょう。
テストコードは、下記のようになります。

const TARGET_URL = 'http://xx.xx.xx.xx'
const CONTENTS = 'Uploaded by Ansible'

const puppeteer = require('puppeteer')
let browser
let page

beforeAll(async () => {
  browser = await puppeteer.launch()
  page = await browser.newPage()
})

afterAll(async done => {
  await browser.close()
  done()
})

describe('check top page', () => {
  it('get top page', async () => {
    page.on('response', response => {
      expect(response.status()).toEqual(200)
    })

    await page.goto(TARGET_URL)
  })

  it('check contents', async () => {
    await page.goto(TARGET_URL, {
      waitUntil: 'networkidle0'
    })

    const item = await page.$('h1')
    const contents = await page.evaluate(body => body.innerHTML, item)
    const reg = new RegExp(CONTENTS, 'i')
    expect(reg.test(contents)).toEqual(true)
  })
})

このテストを実行できるように、ライブラリを入れ替えましょう。

$ yarn remove axios
$ yarn add -D puppeteer

Puppeteer のライブラリもインストールできたので、実行してみます。

$ yarn test
yarn run v1.21.1
$ jest index.test.js
 PASS  ./index.test.js
  check top page
    ✓ get top page (561ms)
    ✓ check contents (1804ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        4.407s
Ran all test suites matching /index.test.js/i.
✨  Done in 6.94s.
$

無事にテストも成功しました!

これで、ディレクトリをまとめてアップロードするタイプのWebサイトでも、自動化することができるようになりました。
また、SPAのようなタイプのWebサイトでも、自動テストを(少なくとも簡単な表示チェックは)書けることもわかりました。

色々と便利なツール・ライブラリを活用し、どんどん楽をできるようにしていきましょう。


tacck
ライター名:tacck
元 技術推進Group Group Leader

現在は株式会社ノースディテールを離れて、
エバンジェリストとして技術啓蒙や勉強会の開催、各種プロジェクトに参画しています。
機会があれば、ノースディテールでのプロジェクトに参加できればと思っています。

言語は問わずに対応しますが、心はPHPer。
フロントエンド・バックエンド・インフラ・スマホアプリなどを、
「垣根を超えて」どう作るか、を考えるのが好きです。

好きなフィギュアスケートの技はスプレッド・イーグル。

主な記事一覧へ

一覧に戻る


LATEST ARTICLE 最新の記事

CATEGORY カテゴリー