Ansibleを使ってNuxt.jsのDockerコンテナを構築してみた

#背景
Dockerコンテナ内にAnsibleをインストールしてみた、Ansibleを使ってDockerをインストールしてみたと言ったブログ記事は、いくつかありましたが、AnsibleのPlaybookを使ってDockerコンテナを構築する記事がまだ少なく感じましたので、今回はAnsibleを実行して、Vue.jsのフレームワークであるNuxt.jsのDockerコンテナを構築してみた結果を記事として残します。
#実施すること
下記の内容をAnsibleのPlaybookから実行できるようにします。
- Dockerイメージ取得
- Nuxt.jsコンテナを構築
- Docker Hubへのイメージpush
 
因みに構築予定のNuxt.jsコンテナのビルド元のDockerFileはこちらの技術書を参考にしました。
[試して学ぶ Dockerコンテナ開発 ](https://www.amazon.co.jp/dp/4839967679?tag=note0e2a-22&linkCode=ogi&th=1&psc=1)
#各種ツール
- Ansible 2.9.10
- Docker 19.03.11
- Docker API 1.40
- Python 3.8.2
- Docker SDK Python 1.80

使うAnsibleのモジュールですが、[docker_image](https://docs.ansible.com/ansible/latest/modules/docker_image_module.html)と[docker_container](https://docs.ansible.com/ansible/latest/modules/docker_container_module.html?highlight=container)を使用します。
これらのモジュールは、Docker APIのバージョンが1.20以上、Docker SDK Pythonのバージョンが1.80以上という必要条件がありますので、事前にバージョンを上げておきます。また必須ではありませんが、docker_imageモジュールのいくつかのオプションはAnsibleのバージョンが2.8以上でなければ動作しないものがいくつかありますので、できればAnsibleのバージョンも上げておきます。
# 事前準備
まず初めに、Nodeイメージを取得し、簡単にNuxt.jsのプロジェクト構築ができるツール[create-nuxt-app](https://ja.nuxtjs.org/guide/installation)を事前に使用してappディレクトリをホームディレクトリとするDockerfileを作成します。

```dockerfile:Dockerfile
FROM node:latest
WORKDIR /app
```
このDockerfileをビルドして、Nodeコンテナ作成後、アタッチしNuxt.jsプロジェクトのひな型を構築します。
`docker run --rm -it <コンテナID> bash`
`yarn create nuxt-app`(コンテナ内)
実行しますと対話形式で設定方法が聞かれますので、好みに合わせてカスタマイズしていきます。
ひな型の作成が完了しましたら、新しくターミナルを開いてローカルマシン上へ作成したひな型をコピーします。
`docker cp <コンテナID>:/app .`
これで準備は整いましたので、DockerfileとPlaybookを作成していきます。
#ディレクトリ構成
ディレクトリ構成は以下の通りです。

```
$ tree
├── dockerFile
│   ├── Dockerfile
│   └── app/(Nuxt.jsのひな型)
└── build_image.yml
└── create_container.yml 
```

# DockerFile作成
Nodeコンテナから持ってきたローカル上のNuxt.jsひな型をNuxt.js用のコンテナへ配置してNuxt.jsが構築されるDockerfileを作成します。

```dockerfile:Dockerfile
FROM node:latest
ENV NUXT_HOST=0.0.0.0
ENV NUXT_TELEMETRY_DISABLED=1
WORKDIR /app

COPY ./app/package.json ./app/yarn.lock ./
RUN yarn install

COPY ./app .

CMD ["yarn", "run", "dev"]
```

# Playbook作成
次にNuxt.jsのコンテナイメージをビルドするためのPlaybookを作成します。

```yaml:build_image.yml
- name: Build Nuxt.js image
  hosts: localhost
  tasks:
  - name: build nuxt.js
    docker_image:
      build:
        path: ./dockerFile
      name: yuta28/ansible-test
      tag: Nuxtimage
      push: yes #イメージビルド後自動的にDockerHubへpushする
      source: build
    register: build_result

  - debug: var=build_result
```

docker_image内のbuildオプションでDockerfileのパスを指定しています。nameオプションは出来上がったDockerイメージの名前を付けています。yuta28は私のDocker Hubのアカウント名であり、Docker Hubへpushする場合イメージ名をDocker Hubアカウント/リポジトリ名と名付ける必要があるためこのような名前にしました。最後のsourceオプションでbuildを指定することで、DockerFileからイメージをビルドするようにしています。
最後に、イメージを基にコンテナを作成するためのPlaybook、create_container.ymlを作成致します。

```yaml:create_container.yml
- name: Create and buid container
  hosts: localhost
  tasks:
  - name: Nuxt container
    docker_container:
      name: NuxtContaier
      image: yuta28/ansible-test:Nuxtimage
      state: started
      ports:
        - "127.0.0.1:8080:3000" #コンテナ内のポート3000にNuxt.jsが構築されるのでローカルのポート8080から参照できるようにマッピング
      tty: true
      detach: true
    register: contaier_result

  - debug: var=contaier_result
```

#Ansible実行
実行するための準備が完了しましたので、Ansibleを実行します。

```yaml:build_image.yml実行
$ ansible-playbook build_image.yml 
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Build Nuxt.js image] ***************************************************************************************************************************************************************************

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

TASK [build nuxt.js] *********************************************************************************************************************************************************************************
[WARNING]: The default for build.pull is currently 'yes', but will be changed to 'no' in Ansible 2.12. Please set build.pull explicitly to the value you need.
changed: [localhost]

TASK [debug] *****************************************************************************************************************************************************************************************
ok: [localhost] => {
    "build_result": {
        "actions": [
            "Built image yuta28/ansible-test:Nuxtimage from ./dockerFile",
            "Pushed image yuta28/ansible-test to docker.io/yuta28/ansible-test:Nuxtimage"
        ],
～～～～～～～～～～～～～～～～～～～省略～～～～～～～～～～～～～～～～～～～～
        "warnings": [
            "The default for build.pull is currently 'yes', but will be changed to 'no' in Ansible 2.12. Please set build.pull explicitly to the value you need."
        ]
    }
}

PLAY RECAP *******************************************************************************************************************************************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0      
```

```yaml:create_container.yml実行
$ ansible-playbook create_container.yml
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Create and buid container] *********************************************************************************************************************************************************************

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

TASK [Nuxt container] ********************************************************************************************************************************************************************************
changed: [localhost]

TASK [debug] *****************************************************************************************************************************************************************************************
ok: [localhost] => {
    "contaier_result": {
        "ansible_facts": {
            "docker_container": {
                "AppArmorProfile": "docker-default",
                "Args": [
                    "yarn",
                    "run",
                    "dev"
                ],
～～～～～～～～～～～～～～～～～～～省略～～～～～～～～～～～～～～～～～～～～
    }
}

PLAY RECAP *******************************************************************************************************************************************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```

無事にコンテナの作成まで完了しました。
`docker ps`でコンテナが起動しているか確認して、Nuxt.jsのサンプルページにアクセスできるか試してみます。

```
$ docker ps
CONTAINER ID        IMAGE                           COMMAND                  CREATED             STATUS              PORTS                      NAMES
f5597614f5d5        yuta28/ansible-test:Nuxtimage   "docker-entrypoint.s…"   37 minutes ago      Up 37 minutes       127.0.0.1:8080->3000/tcp   NuxtContaier
```

![Screenshot 2020-08-16 16:46:16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648568/fca6b352-6939-c199-7f20-92804a8a7c39.png)
問題なくNuxt.jsのサンプルページにアクセスすることができました。
# Docker Hubへのpush
`build_image.yml`の中身に含まれているオプション`push:yes`でログイン済みのDocker Hubにイメージをpushすることができます。![Screenshot 2020-08-16 16:55:51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648568/70059238-11e9-3ceb-b7fe-0fb40e51bbac.png)
# 所感
Dockerのイメージ取得、ビルド、コンテナ作成、起動、DockerHubへのpushをAnsibleを使って一通り行うことができました。ただ、最初に事前準備のnuxt-appは対話形式での設定方法のため予めDockerfileにコマンドを書いてもその後の設定が行えず、いい方法も思いつかないため直接コンテナ内に入って手動でnuxt-appを実行しました。
実は２,３年くらい前の技術書やWebサイトには[Ansible Container](https://docs.ansible.com/ansible-container/)というコンテナの構築やビルドに便利なツールがありました。
ですが、2020年現在は開発が終了したのか非推奨になっております。(`pip install ansible-container`しても見つかりませんでした)
Ansibleには、Docker関連に特化したモジュールが豊富に用意されており、今回使用した2つのモジュール以外にも多くのモジュールがありますので、気になる方はリファレンスマニュアルを覗いてみてください。