North Detail / ノースディテール

BLOG ブログ

ブログ
CATEGORY
TECH

Ansible で デプロイの自動化を目指す / 単純なファイルアップロードの場合

こんにちは。tacckです。

色々とコンテンツをデプロイする時に、「同じ手順を人力で繰り返す」ということは多々あると思います。
これは、我々エンジニア・プログラマの美徳の一つ「怠惰」に反する行為ですよね。

こういったものは、自動化してしまいましょう!

今回から、私の好きな構成管理ツール Ansible を使って、Webサイト・Webアプリケーションのデプロイを段階的に自動化していく手順を説明していきたいと思います。

今回は、Webサーバの簡単な設定と、HTMLファイルのアップロードの流れを見てみたいと思います。

準備

Ansible のインストール

インストール方法は公式の情報にあります。

macOSの場合は pip を使う方法が書かれていますが、 Hombrew を使っている方は brew install ansible でもインストールできます。

Windowsの方は、公式には書かれていないですね。。。
Docker を使うか、 VirtualBox へ Linuxディストリビューションをインストールして使うか、とすれば良いと思います。

私は macOS 10.14 + Homebrew で環境を作っているので、この環境ベースで進めていきます。

アップロード先の用意

ファイルをアップロードする先 (Webサーバ) をどこかに用意しましょう。

Webサーバは「SSHで接続できること」と「Python 2 or 3が使えること」が条件です。
このシリーズでは、手軽で安価にサーバを用意できる Amazon LightsailCentOS イメージで作成したサーバを利用する想定で進めます。

その他

ターミナルアプリで、コマンドライン操作が可能であることを前提とします。

Ansible で自動化 / HTMLファイル

必要なファイルの準備

まず、 Apache HTTP Server を用意しつつ、シンプルにHTMLファイルのみをアップロードすることをやってみましょう。

必要なファイルは、こちらのようになります。それぞれ見ていきましょう。

.
├── ansible.cfg
├── inventory
├── main.yml
├── roles
│   └── upload
│       ├── files
│       │   └── index.html
│       └── tasks
│           └── main.yml
└── ssh
    └── web_testing.pem

Inventory ファイルの作成

inventory ファイルは、サーバへの接続情報となります。

Ansible では INI形式と YAML形式 のどちらかで指定できますが、今回は歴史的に長く使われている INI形式にしています。

[web]
xx.xx.xx.xx

[web:vars]
ansible_port=22
ansible_user=centos
ansible_ssh_private_key_file=ssh/web_testing.pem

xx.xx.xx.xx はサーバのIPアドレスを指定します。(FQDNでも大丈夫です)
また1行目の [web] のようにサーバをグルーピングすることが可能です。

4行目~7行目は、[web]グループのサーバ全体への接続オプションとなります。
ここでは、ansible_port (ssh接続時のポート番号) 、 ansible_user (ssh接続時のユーザ名) 、 ansible_ssh_private_key_file (ssh接続時の秘密鍵) を指定しています。

この辺りの内容は、実際に接続するサーバに合わせて変更してください。

Playbook ファイルの作成

今回必要なのは main.ymlroles 配下のファイル群です。

まずは、main.yml から。

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

hosts は、先の inventory で指定したサーバ(のグループ)を指定します。
全てのサーバを対象にする場合には all とします。

become は、いわゆる sudo などの権限変更しての作業を行なうか、という項目です。
ここではWebサーバの設定や、HTMLファイルの配置場所に色々権限が必要なので、 yes としています。
特にユーザ名を指定しなければ root ユーザとして実行することになります。

roles は、具体的な作業が書かれたロール名を指定します。
お気づきの通り、この uploadroles/upload が対応することになります。

この書き方 (Roles) は、Ansible自身の推奨する書き方で、こうすることでタスク単位で実行したい内容、必要なファイル、といったものを集中管理できるようになります。
うまく作っていれば他への流用もやりやすくなるので、この形で進めることをお勧めします。

今回は、実際にやりたいことを roles/upload/tasks/main.yml に、アップロードしたいファイルを roles/upload/files/index.html に格納しています。

まずは roles/upload/tasks/main.yml を見てみましょう。

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

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

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

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

- name: upload
  copy:
    src: index.html
    dest: /var/www/html

name は実行するタスクの内容説明、になります。無くても実行はできますが、実行結果を見た時に何をやっているのかわからなくなることもあるので、きちんと具体的な内容で書いておきましょう。

ここでは、上から順番に "サーバのタイムゾーン変更" 、 "Apache HTTP Server のインストール(すでにあれば最新化)" 、 "Apache HTTP Server の有効化(マシン再起動時に起動されるようにする)" 、 "Apache HTTP Server の起動(すでに起動済みなら再起動)" 、 "HTMLファイルのアップロード" を行なっています。

タスク実行に使うモジュールは多くの種類があるので、自分が必要そうなものがあるか適宜検索してみてください。

その他のファイル

今回アップロードしたい roles/upload/files/index.html を用意します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Ansible1</title>
  </head>

  <body>
    This is a sample HTML.
  </body>
</html>

今回は特に凝ったことをしないので、中身はなんでも良いです。

また、 Ansible が ssh接続するために必要な秘密鍵も ssh/web_testing.pem に配置します。
このファイルは "所有者のみの読み取り専用" でないといけません。(macOS / Linux などの場合)

$ chmod 600 ssh/web_testing.pem

などを実行しておきましょう。

実行

では、実行してみます。

$ ansible-playbook -i inventory main.yml 

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

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

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

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

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

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

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

PLAY RECAP *********************************************************************************************************************************
xx.xx.xx.xx               : ok=6    changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

無事に実行できました!

ブラウザでサーバを開くと、先ほどのHTMLファイルが見えていると思います。

おまけ / テストを書く

生き残るためにテストを書く

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

https://speakerdeck.com/twada/tdd-live-and-workshop-2019-spring?slide=4 より

サバンナで生き残るために、「Webサーバがきちんと動いているか」をテストコードを使って確認してみましょう。
やり方は色々とあると思いますが、今回は Node.js のテスティングフレームワークである Jest を使って書いてみます。

Node.js と Yarn が利用可能な状態、という前提で進めます。

パッケージは Jest の他に Axios を使います。

下記のように実行して、パッケージをインストールしていきます。

$ mkdir tests
$ cd tests
$ yarn init -y
$ yarn add -D jest axios

実行できたら、 package.json というファイルが作成されているはずです。

こちらを少し書き換えて、下記のように "jest" と "scripts" を追加しましょう。

{
  "name": "tests",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "axios": "^0.19.2",
    "jest": "^25.1.0"
  },
  "jest": {
    "testEnvironment": "node"
  },
  "scripts": {
    "test": "jest index.test.js"
  }
}

テストコード作成

では、テストコードを書いてみましょう。
今回はあまり難しく考えずに、下記2点のテストにしてみたいと思います。

  • Webサーバにアクセスしたら HTTPステータスが 200であること
  • Webサーバから受け取った中身に "This is a sample HTML." が含まれていること

この辺りを工夫すれば、Webページをブラウザで開かなくてもテストできるようになっていきます。

const TARGET_URL = 'http://xx.xx.xx.xx'
const CONTENTS = 'This is a sample HTML.'

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)
  })
})

作成できたら、下記のようなファイル構成になっているはずです。

tests
├── index.test.js
├── node_modules
├── package.json
└── yarn.lock

実行

では、 tests ディレクトリの中で yarn test と実行してみましょう。

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

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

無事に実行できました!
テストも全てパスできているので、 Ansible を使って Webサーバを起動し、ファイルのアップロードもできたことが確認できました。

これで、サバンナでも生き残れますね。🦁

tacck
WRITER:tacck
元 技術推進Group Group Leader

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

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

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

一覧へ

IS 501383 / ISO 27001