リノシス開発者ブログ

株式会社リノシスのエンジニアブログです。

Ansistranoでgitリポジトリからラズパイにデプロイ

f:id:otabe01:20200218150947p:plain:w200

はじめに

こんにちは, インターン生の田部と申します. IoT化がますます進展していく中, ラズパイを利用したプロダクトを開発したいという方もいらっしゃると思います. そこで, Ansistranoを使ってgitリポジトリからラズパイにデプロイをする方法をご紹介したいと思います. Ansistranoは, Capistranoライクに使えるツールで, Railsエンジニアでもキャッチアップしやすいので, ぜひ利用してみてください.
今回は, 主にAnsistranoで以下を行っていきます.

  • ラズパイの初期設定
  • FastAPIを使ったAPIサーバを起動時に実行するための設定

前提

raspbian OS(buster 2019-09-26)がインストールされている

ssh, wifiの設定

microSDにOSイメージを書き込んだ後にルート(/boot)ディレクトリに, ファイル名sshの空ファイル, wpa_supplicant.conf (参考) を作成する

ラズパイへ公開鍵を配布

以下のコマンドで公開鍵を配布します
ssh-copy-id -i [path_to_pubkey] pi@[raspi_ip_ddress]

IPアドレスの代わりに, [host_name].localでsshすることも可能です
ssh-copy-id -i [path_to_pubkey] pi@raspberrypi.local
※ 複数のラズパイが同一ネットワークにいる場合は, ラズパイのホスト名をユニークに設定しましょう

デプロイするリポジトリディレクトリ構造

$ tree
.
├── deploy
│   ├── hosts
│   ├── roles
│   │   ├── reboot.yml
│   │   ├── setup.sh
│   │   └── setup_sub.yml
│   ├── rollback.yml
│   ├── deploy.yml
│   └── templates
│       └── sample.conf
├── main.py
├── README.md
└── requirements.txt

FastAPIを利用したサンプルAPI

APIサーバを簡単に構築できるFastAPIを利用して簡単なAPIを作成します
FastAPIはドキュメントも自動生成してくれます
※ 必要条件: python3.6以降

# main.py
from fastapi import FastAPI, Path
from pydantic import BaseModel, Field

app = FastAPI()


class User(BaseModel):
    name: str
    email: str
    age: int
    sex: str = Field(..., regex="^male$|^female$")

users = {
    1: {"name": "taro", "email": "example1@test.com", "age": 20, "sex": "male"},
    2: {"name": "hana", "email": "example2@test.com", "age": 30, "sex": "female"},
    3: {"name": "hori", "email": "example3@test.com", "age": 40, "sex": "male"},
}

def not_resource_error(user_id: int):
    return {'error': f'resource user{user_id} does not exist'}

@app.get("/")
async def hello():
    return {"result": "Hello!"}

@app.get("/users/")
async def read_all_user():
    return users

@app.get("/users/{user_id}")
async def read_user(*, user_id: int = Path(..., title="The ID of the user to get", ge=1)):
    if not user_id in users.keys():
        return not_resource_error(user_id)
    user = users[user_id]
    return user

必要なモジュールのリスト

# requirements.txt
fastapi == 0.48.0
pydantic
uvicorn

Ansistranoのインストール

動作環境のAnsibleのバージョン

ansible --version
ansible 2.5.1

以下のコマンドを実行 (参考)

ansible-galaxy install ansistrano.deploy ansistrano.rollback

Ansistranoの設定

playbookの作成

以下のようにplaybookを作成します
Capistranoでいう全体の設定ファイル(config/deploy.rb)に対応しています

#deploy.yml
- hosts: all
  vars:
    ansistrano_keep_releases: 5
    ansistrano_deploy_to: "/home/pi"
    ansistrano_deploy_via: git
    ansistrano_git_branch: develop
    ansistrano_git_repo: [user]@[host]:[project].git # gitリポジトリにはsshでアクセス
    ansistrano_git_identity_key_path: [path_to_git_secret_key] # gitにアクセスするための秘密鍵へのpathを設定
    ansistrano_after_symlink_tasks_file: "{{ playbook_dir }}/roles/setup_sub.yml"
    ansistrano_after_cleanup_tasks_file: "{{ playbook_dir}}/roles/reboot.yml"

  roles:
    - ansistrano.deploy
  • デプロイ先の環境で固有の設定ファイルやディレクトリなどは, 上記に以下を追加することで, sharedディレクトリにコピーされ, 対象のファイルへのリンクが作成されます
    ansistrano_shared_files: []
    ansistrano_shared_paths: []
    この設定を追加すれば, 各バージョン間で設定ファイルなどを保持することができます
# rollback.yml
- hosts: all
  vars:
    ansistrano_deploy_to: "/home/pi"
    ansistrano_rollback_after_cleanup_tasks_file: "{{ playbook_dir }}/roles/reboot.yml"
  roles:
    - ansistrano.rollback

templatesディレクトリに設定ファイルを作成

  • supervisorの設定ファイル
# sample.conf
[program:sample]
directory=/home/pi/current
command=/home/pi/current/venv/bin/uvicorn main:app --host 0.0.0.0
process_name=%(program_name)s_%(process_num)02d
numprocs=1
autostart=true
autorestart=true
redirect_stderr=true
user=pi
stdout_logfile=/var/log/sample.log

hookファイルの作成

今回は, 基本的にシェルスクリプトに書き出した処理を機能毎に実行していきます
Capistranoでいうtaskファイル(lib/tasks/*)に対応した処理です

# setup_sub.yml
- name: setup language
  shell: bash setup.sh lang
  args:
    chdir: "{{ ansistrano_release_path.stdout }}/deploy/roles"

- name: setup pubkey
  shell: bash setup.sh pubkey
  args:
    chdir: "{{ ansistrano_release_path.stdout }}/deploy/roles"

- name: setup supervisor
  shell: bash setup.sh supv
  args:
    chdir: "{{ ansistrano_release_path.stdout }}/deploy/roles"

- name: setup raspberry
  shell: bash setup.sh rasp
  args:
    chdir: "{{ ansistrano_release_path.stdout }}/deploy/roles"

- name: setup python
  shell: bash deploy/roles/setup.sh python
  args:
    chdir: "{{ ansistrano_release_path.stdout }}"

- name: pip install from requirements.txt
  pip:
    requirements: "{{ ansistrano_release_path.stdout }}/requirements.txt"
    executable: "{{ ansistrano_release_path.stdout }}/venv/bin/pip"

- name: set supervisord sample.conf
  become: yes
  copy:
    src: "{{ ansistrano_release_path.stdout }}/deploy/templates/sample.conf"
    dest: /etc/supervisor/conf.d/sample.conf
    remote_src: yes
    mode: '0644'
# reboot.yml
# 設定を反映させるためにラズパイを再起動
- name: raspi reboot
  shell: sleep 2 && shutdown -r now
  async: 1
  poll: 0
  become: yes
  ignore_errors: true

- name: wait reboot
  wait_for_connection:
  delegate_to: localhost
  # setup.sh
  
  # pythonのバージョンを指定
  PYTHON_VERSION=3.7.4
  # 日本語に対応
  language_setup(){
      sudo apt-get update
      sudo apt-get install -y ttf-kochi-gothic xfonts-intl-japanese xfonts-intl-japanese-big xfonts-kaname;
      sudo apt-get install -y fcitx-mozc
  }
  # パスワード認証を無効化
  pubkey_setup(){
      sudo sh -c "cat /etc/ssh/sshd_config | sed s/#PasswordAuthentication' '/PasswordAuthentication' 'no' '#/ | sed s/#PubkeyAuthentication/PubkeyAuthentication/ > ./sshd_config"
      sudo cp ./sshd_config /etc/ssh/sshd_config
      rm -f ./sshd_config
  }
  # pythonのプログラムをデーモン実行するために使用
  install_supervisord(){
      sudo apt-get install -y supervisor
  }
  # locale, timezoneの設定
  rasp_setup(){
      sudo raspi-config nonint do_change_timezone "Asia/Tokyo"
      sudo raspi-config nonint do_change_locale ja_JP.UTF-8
  }
  
  python_setup(){
      # 補足を参照
      sudo apt-get install -y --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
      git clone https://github.com/pyenv/pyenv.git ~/.pyenv
      if [ -z "`cat ~/.bashrc | grep pyenv`" ]; then
          echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
          echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
          echo 'eval "$(pyenv init -)"' >> ~/.bashrc
      fi
      if [ -z "`/home/pi/.pyenv/bin/pyenv versions | grep $PYTHON_VERSION`" ]; then
          /home/pi/.pyenv/bin/pyenv install -f $PYTHON_VERSION
          /home/pi/.pyenv/bin/pyenv local $PYTHON_VERSION
      fi
      if [ ! -e some_directory ]; then
          /home/pi/.pyenv/versions/${PYTHON_VERSION}/bin/python -m venv venv
      fi
      if [ -z "`cat ~/.bashrc | grep venv`" ]; then
          echo source ~/current/venv/bin/activate >> ~/.bashrc
      fi
  }
  
  case $1 in
      lang ) language_setup ;;
      pubkey ) pubkey_setup ;;
      supv ) install_supervisord ;;
      rasp ) rasp_setup ;;
      python ) python_setup ;;
  esac

デプロイ先の情報を記載したファイルを作成

Capistranoでいう, 環境ごとの設定ファイル(config/deploy/[stage_name].rb)に対応しています.

# hosts
[web]
raspberrypi.local

[all:vars]
ansible_ssh_user=pi

デプロイ

  • deployディレクトリに移動し, 以下のコマンドを実行します
    ansible-playbook -i hosts deploy.yml --private-key=[path_to_private_key]
    Capistranoでは以下のコマンドに相当します
    bundle exec cap [stage_name] deploy
  • 予め, ssh-agentを利用して, 秘密鍵を登録しておくと秘密鍵を毎回指定せずに済みます
    ssh-add [path_to_private_key]

ラズパイで立ち上がったAPIサーバーにアクセス

http://[raspi_ip_address]:8000 にアクセスして, 以下が返ってくるはずです

{"result":"Hello!"}

http://[raspi_ip_address]:8000/docs にアクセスすると, でAPIドキュメントを参照できます

宣伝

RenoSysでは, Railsエンジニアを募集中です.
ラズパイを活用したプロダクト開発などにも携われます.
en-gage.net