Dockerとラズパイで迫撃砲（簡単開発支援サーバ）


# 迫撃砲（簡単開発支援サーバ）って何？

![Mortar.jpg](https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/M2-Mortar.jpg/293px-M2-Mortar.jpg)
※wikipediaからの引用


迫撃砲（簡単開発支援サーバ）は以下のような機能を持つサーバです．

* Gitレポジトリ管理
* 自動テスト

ただのCIサーバです．しかし、最大の特徴はラズパイ上で動作する点です．小型で低価格なので以下のような場面で活躍します．

1. CIサーバを購入できない貧乏チーム
2. CIサーバを試してみたいチーム
3. 出先で行った変更に対して自動テストを実行したい

所詮はラズパイなのでストレージ容量やCPU性能に物足りないところもあります．されど、小型なので迫撃砲（簡単開発支援サーバ）は鞄に忍びこませることができます．家でセットアップして職場に持っていくだけで速攻でCIサーバを展開できます．また、出先にも持っていけるので出張先から自社のサーバにアクセスするのに必要となる面倒なVPNの設定も必要ありません．稼働状況の表示やテストなどサーバの保守を支援する機能が搭載されているので少人数（というか一人？）での運用もできます．迫撃砲のように現場で迅速展開、仲間のために支援砲火をバカスカ打てます．
この記事では20人ほどの開発チームに低予算でラズパイを用いた迫撃砲（簡単開発支援サーバ）を導入することを目的に書きます．

# ハードウェア
サーバのハードウェアにはラズパイを使用します．
ラズパイはamazonで購入できます．

https://www.amazon.co.jp/-/en/Raspberry-Pre-Installed-Transparent-Instruction-Guaranteed/dp/B08D6CYBNS/ref=sr_1_4?crid=3AEHJQEFJASE4&dchild=1&keywords=raspberry+pi+4&qid=1626917635&s=computers&sprefix=raspber%2Ccomputers%2C260&sr=1-4

基盤むき出しだと不慮の事故で壊れる可能性があるのでケースも買っておきましょう．
筆者はラズパイ4で構築しています．

![IMG_20210801_212215.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77676/e4617c2a-0476-51be-8850-e09f2d1807ae.jpeg)

ケースをつけた状態でタバコサイズです．胸ポケットに入るCIサーバです．
ラズパイにはラズパイ3とラズパイ4があります．検証はしていませんが、ラズパイ3でもこの記事に書かれた方法で構築できると思います．

# ラズパイの基本設定

ラズパイをサーバとして動作させるには以下の設定をします．

1. IPアドレスの固定
2. ファイヤーウォールの設定
3. パスワード変更
4. ssh認証鍵の設定

## IPアドレスの固定

IPアドレスの割当を行うdhcpcdデーモンの設定を変更して静的なIPをラズパイに割当てます．

/etc/dhcpcd.confの以下の行を変更してください．

```:/etc/dhcpcd.conf
interface eth0
static ip_address=<割り当てるIPアドレス/ネットマスク>
static routers=<ルータのアドレス>
static domain_name_servers=<ルータのアドレス（DNSサーバのアドレス）>
```

変更後、ラズパイを再起動します．

```bash:
sudo reboot
```

## ファイヤーウォールの設定

以下のコマンドでufwをインストールします．

```bash:
sudo apt install ufw
```

ufwの設定、すなわち許可するポートの設定を行います．
許可すべきポートは以下のとおりです．

| サービス  | ポート |
| --------- | ------ |
| ssh       | 22     |
| ssh(For Gitea)| 222    |
| http      | 80     |
| glance    | 20000  |
| testinfra | 30000  |

以下のコマンドで上記のポートを有効化できます．

```bash:
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow 20000/tcp
sudo ufw allow 30000/tcp
sudo ufw allow 222/tcp
```

設定が終わったら以下のコマンドでufwを起動します．

```bash:
sudo ufw enable
```

確認のため最後に`sudo ufw status`で状態を表示しましょう．

```bash:
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
20000/tcp                  ALLOW       Anywhere
30000/tcp                  ALLOW       Anywhere
222/tcp                    ALLOW       Anywhere
22/tcp (v6)                ALLOW       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
20000/tcp (v6)             ALLOW       Anywhere (v6)
30000/tcp (v6)             ALLOW       Anywhere (v6)
222/tcp (v6)               ALLOW       Anywhere (v6)
22 (v6)                    DENY        Anywhere (v6)
```

意図した通りにファイヤーウォールが設定されているでしょうか？上記のように表示されたら完了です．

## パスワードの変更
デフォルトのパスワードは`raspberry`です．これをそのまま使って置くことはセキュリティリスクとなります．忘れずに変更しましょう．
以下のコマンドで変更できます．

```
passwd
```

変更後のパスワードは忘れないように書き留めておきましょう．


## ssh認証鍵の設定

パスワードを使ったログインは手軽ですが総当り攻撃や辞書攻撃によって簡単に破られる可能性があります．
そこで、認証鍵を用いたログインを行うようにします．
クライアント側で認証鍵を生成します．

```
ssh-keygen -t rsa
```

生成された**~/.ssh/id_rsa.pub**をラズパイにインストールします．

```
ssh-copy-id -i ~/.ssh/id_rsa.pub pi@<ラズパイのIP>
```

sshでラズパイにログインしましょう．

```
ssh pi@<ラズパイのIP>
```

ログイン後、不要となったパスワードによるログインを禁止します．
ラズパイの**/etc/ssh/sshd_config**の以下の行を変更します．

```:/etc/ssh/sshd_config
ChallengeResponseAuthentication no # ここをyes -> noにする
...
PasswordAuthentication no # ここをyes -> noにする
...
UsePAM no # ここをyes -> noにする
```

ついでにROOTでのログインも禁止しておきましょう.

```config:/etc/ssh/sshd_config
PermitRootLogin no # ここをyes -> noにする
```

変更を反映するためにsshdデーモンを再起動します．

```bash:
sudo service sshd restart
```

# システムの立ち上げ

これまででサーバとしての基本部分の設定が完了しました．
もちろんこのままでは迫撃砲（簡単開発支援サーバ）としては不十分です．
これからサーバとしての機能を構築していきます．
以下が迫撃砲（簡単支援サーバ）の全体像です．

![SDS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77676/e8c70c4f-b251-957d-3357-67818099c1ee.png)


迫撃砲のシステムは大きく2つの部分に大別されます．

1. サーバ管理システム
   サーバの稼働状況の監視、テスト、サーバの起動を行うシステムです．以下のソフトから構成されます．

   * glances（サーバ稼働状況の表示）
   * testinfra（単体テストの実行+ウェブページによる結果の表示）


2. Webアプリケーションシステム
   レポジトリ管理ソフトとしてGitTea、自動テストソフトとしてJenkinsを稼働させます．
   これらのソフトはDocker Engine上で動作します．

サーバの構築に使用したコードは以下のGithubレポジトリにアップロードしています．以降はこのレポジトリを参照しながら読み進めてください．

https://github.com/MoriokaReimen/Raspberry-Motor-Server

# サーバ管理システムのセットアップ

## glancesのセットアップ

glancesはサーバの稼働状況を表示するPythonアプリです．サーバの稼働状況を調べるのにいちいちsshでログインするのは面倒です．そこで、ウェブブラウザで使ってサーバにアクセスするだけでサーバの稼働状況を調べられるようにします．

まず、ラズパイにPython3とpipをインストールします．以下のコマンドでインストールできます．

```bash:
sudo apt-get install -y python3 python3-pip
```

次にインストールしたpipを使ってglancesをインストールします．

```bash:
sudo pip3 install glances
```

インストールが終わったらの以下のコマンドを起動してglancesウェブサーバを起動します．

```bash:
glances -w --username --password --port 20000
```

デフォルトのパスワードの入力が求められるので入力して、その後の指示に従い保存してください．

ウェブブラウザで`http://<ラズパイのIP>:20000` を開くとサーバ稼働状況が表示されます．

![glances.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77676/4611970f-c00f-f379-a969-a7e3c298b904.png)

## testinfra+Flaskのセットアップ

testinfraはPythonで実装されたサーババリデーションモジュールです．testinfraを使えばPythonスクリプトから

* ストレージ容量
* 実行しているDockerコンテナ
* コマンドの実行結果

取得することができます．この機能を使ってサーバが望ましい状態で稼働しているかをテストすることができます．
ただし、testinfraにはglancesのようにウェブページを介して表示する機能はありません．
そこで、Flaskと組み合わせることでウェブページからサーバのテスト結果を表示できるようにします．

まず以下のコマンドでtestinfraとFlaskをインストールします．

```bash:
sudo pip3 install testinfra Flask
```

以下のようなPythonスクリプトを用意します．

```python3:server.py
from flask import Flask, render_template
import testinfra
from datetime import datetime
import humanfriendly
import io

app = Flask(__name__)


@app.route('/')
def index():
    print("Test Executed!!")
    contents = "Test Result:</br>"

    return render_template('layout.html', title = 'Test', datetime =datetime.now() ,test_all = test_all)

def test_all():
    result = {}
    try:
      host = testinfra.host.get_host("local://", sudo=True)

      # test password file for root access rights
      passwd = host.file("/etc/passwd")
      result["Password Root"]  =   "OK" if passwd.contains("root") else "NG"
      result["Password User"]  =   "OK" if passwd.user == "root"   else "NG"
      result["Password Group"] =  "OK" if passwd.group == "root"  else "NG"
      result["Password Mode"]  =   "OK" if passwd.mode == 0o644    else "NG"

      # test local host resolvable
      localhost = host.addr("localhost")
      result["localhost Resovable"]  =   "OK" if localhost.is_resolvable else "NG"

      # test wan dns
      wan_dns = host.addr("8.8.8.8")
      result["WAN DNS Reachable"]  =   "OK" if wan_dns.is_reachable else "NG"

      # test sshd
      sshd = host.service("sshd")
      result["sshd running"]  =   "OK" if sshd.is_running else "NG"
      ssh_port = host.socket("tcp://0.0.0.0:22")
      result["ssh port"]  =   "OK" if ssh_port.is_listening else "NG"

      # test ufw
      ufw = host.service("ufw")
      result["ufw running"]  =   "OK" if ufw.is_running else "NG"

      # test glances
      glances_http = host.run("sudo curl -Is http://192.168.11.20:20000 | head -1")
      result["glances working"]  =   "OK" if 'HTTP/1.0 401 Unauthorized' in glances_http.stdout else "NG"

      # test docker
      docker = host.service("docker")
      result["docker running"]  =   "OK" if docker.is_running else "NG"

      nginx = host.docker("nginx")
      result["docker nginx"]  =   "OK" if nginx.is_running else "NG"

      gitea = host.docker("gitea")
      result["docker gitea"]  =   "OK" if gitea.is_running else "NG"

      giteadb = host.docker("giteadb")
      result["docker giteadb"]  =   "OK" if giteadb.is_running else "NG"

      jenkins = host.docker("jenkins")
      result["docker jenkins"]  =   "OK" if jenkins.is_running else "NG"

      docker_df = host.run("sudo docker system df --format='{{.Size}}'").stdout
      size = 0
      for line in io.StringIO(docker_df):
          size += humanfriendly.parse_size(line)
      print(size)
      result["docker size"]  =   "OK" if size < 8000000000 else "NG"

      # test gitea
      gitea_http = host.run("sudo curl -Is http://192.168.11.20/gitea/ | head -1")
      result["gitea working"]  =   "OK" if 'HTTP/1.1 200 OK' in gitea_http.stdout else "NG"

      # test jenkins
      jenkins_http = host.run("sudo curl -Is http://192.168.11.20/jenkins/login | head -1")
      result["jenkins working"]  =   "OK" if 'HTTP/1.1 200 OK' in jenkins_http.stdout else "NG"

      # test directory size
      gitea_file = host.file("/root/Server/Docker/Gitea/gitea-data/")
      result["gitea file size"]  =   "OK" if  gitea_file.size < 4000000000 else "NG"

      giteadb_file = host.file("/root/Server/Docker/GiteaDB/giteadb-data/")
      result["giteadb file size"]  =   "OK" if  giteadb_file.size < 2000000000 else "NG"

      jenkins_file = host.file("/root/Server/Docker/Jenkins/jenkins_home/")
      result["jenkins file size"]  =   "OK" if  jenkins_file.size < 4000000000 else "NG"
    except:
        err = sys.exc_info()[0]
        result["Unit test bail out!!"] =  err

    return result

if __name__ == '__main__':
    app.config['TESTING'] = False
    app.config['DEBUG'] = False
    app.run(host='0.0.0.0', port=30000)
```

server.pyはウェブページの描画にlayout.htmlを参照します．layout.htmlはテスト結果をhtmlの表として出力します．
layout.htmlの内容は割愛します．
server.pyは以下のコマンドで起動ます．

```bash:
sudo python3 server.py
```

起動したらウェブブラウザで<ラズパイのIP>:30000にアクセスしましょう.
テスト結果が表示されます．

![testinfra.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77676/acf9070c-682d-df13-6e9b-abb28676cd3e.png)


テストはserver.pyの`test_all`に実装されています．`test_all`は以下のテストを実行しています．

* WANへのアクセスのチェック
* sshサービスの稼働チェック
* ufwサービスの稼働チェック
* Giteaコンテナの稼働状態のチェック
* Jenkinsコンテナの稼働状態のチェック
* curlコマンドを用いたGiteaページへのアクセス
* curlコマンドを用いたJenkinsページへのアクセス
* Giteaのストレージの容量チェック
* Jenkinsのストレージの容量チェック


忘れていましたがserver.pyの実行にはcurlが必要です．以下のコマンドでcurlをインストールしましょう．

```bash
sudo apt-get install -y curl
```

## 起動・停止スクリプトの作成

以上でサーバ管理システムのセットアップが完了しました．

簡単にglancesとtestinfra+Flaskを起動・停止できるようにスクリプトを用意しましょう．
githubのレポジトリのstart\_server.shは起動スクリプト、stop\_server.shは停止スクリプトです．
起動スクリプトの内容は以下のとおりです．

```bash:start_server.sh
#! /bin/bash
pushd $(dirname ${BASH_SOURCE}) 1> /dev/null

# launch glances
printf "<設定したglancesのパスワード>" | glances -w --username --password --port 20000 1> /dev/null 2> /dev/null & disown

# launch testinfra
pushd ./testinfra 1> /dev/null
python3 server.py 1> /dev/null 2> /dev/null & disown
popd 1> /dev/null

popd 1> /dev/null

```

停止スクリプトは以下のとおりです．

```bash:stop_server.sh
#! /bin/bash
pushd $(dirname ${BASH_SOURCE}) 1> /dev/null

# stop glances
pkill glances

# stop testinfra
pkill -f server.py

popd 1> /dev/null
```

以下のコマンドでスクリプトを実行すればサーバの起動と停止ができます．

```bash:
# 実行権限の付与
chmod +x start_server.sh
chmod +x stop_server.sh

# 起動
./start_server.sh

＃停止
./stop_server.sh
```

## 自動起動の設定
ラズパイの電源をオフすると迫撃砲(簡単開発支援サーバ)は停止してしまいます．
電源オンオフの度に`start_server.sh`を実行するのは非常にたるいです．
そこで起動時に`start_server.sh`が自動的に実行されるようにします．
起動にはsystemdを使います．

systemdはLinuxのシステム管理デーモンです．systemdを使えば起動時や定期的にプログラムを実行できます．

設定のために新たなサービスを作成します．
以下の内容のsds.serviceファイルを/etc/systemd/system/に配置します．

```systemd:/etc/systemd/system/sds.service
# place this files in /etc/systemd/system/
[Unit]
Description = Software Development Server
Requires=docker.service

[Service]
Type=oneshot
ExecStart=<start_server.shへのパス>
ExecStop=<stop_server.shへのパス>
RemainAfterExit=true

[Install]
WantedBy=multi-user.target
```

以下のコマンドで追加されたsds.serviceファイルをsystemdに反映します．

```
sudo systemctl daemon-reload
```

以下のコマンドで起動時にsds.serviceが起動時に実行されるようにします．

```
sudo systemctl enable sds
```

これにより、ラズパイの起動時に自動的にソフトウェア開発支援サーバが起動されます．

# Webアプリケーションシステムの立ち上げ
Webアプリケーションシステムを立ち上げます．
立ち上げにはDockerを使います．Dockerはコンテナと呼ばれる仮想環境上でOSとアプリをまるごと動作させることができます．
これにより、一台のラズベリーパイの中であたかも複数のサーバが動いているかのように動作させることができます．

## docker-composeの設定
サーバを起動するときはdocker-composeを介してDockerを呼び出します．
docker-coposeはDockerの補助スクリプトです．docker-compose.ymlに記述された設定の通りに複数のサーバを起動・停止します．
なくても良いのですがDockerの複雑な引数を簡単に呼び出せるようにしてくれるので、docker-composeを使ったほうがコマンドの実行が非常に楽になります．
Docker/docker-compose.ymlの内容は以下のとおりです．

```yml:Docker/docker-compose.yml
version: "3"
networks:
  giteanet:
    external: false
  jenkinsnet:
    external: false

services:
  nginx:
    image: nginx:alpine
    depends_on:
      - gitea
      - jenkins
    container_name: nginx
    volumes:
      - ./Nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    networks:
      - giteanet
      - jenkinsnet
    ports:
      - 80:80
      - 222:222

  giteadb:
    image: arm32v7/postgres
    container_name: giteadb
    environment:
      POSTGRES_DB: giteadb
      POSTGRES_USER: gitea
      POSTGRES_PASSWORD: gitea
    logging:
      driver: "json-file"
      options:
        max-file: "2"
        max-size: "3m"
    restart: unless-stopped
    networks:
      - giteanet
    volumes:
      - ./GiteaDB/giteadb-data:/var/lib/postgresql/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro

  gitea:
    image: kunde21/gitea-arm
    container_name: gitea
    environment:
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=giteadb:5432
      - GITEA__database__NAME=giteadb
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=gitea
    logging:
      driver: "json-file"
      options:
        max-file: "2"
        max-size: "3m"
    restart: unless-stopped
    depends_on:
      - giteadb
    networks:
      - giteanet
    links:
      - "giteadb"
    volumes:
      - ./Gitea/gitea-data:/data/
      - ./Gitea/app.ini:/data/gitea/conf/app.ini
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro

  jenkins:
    build:
      context: ./Jenkins
    container_name: jenkins
    environment:
      - JENKINS_OPTS="--prefix=/jenkins"
    logging:
      driver: "json-file"
      options:
        max-file: "2"
        max-size: "3m"
    restart: unless-stopped
    networks:
      - jenkinsnet
    volumes:
      - ./Jenkins/jenkins_home:/var/jenkins_home
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro

volumes:
  giteadb-data:
    driver: local
    driver_opts:
      type: 'none'
      o: 'bind'
      device: './giteadb-data/'
  gitea-data:
    driver: local
    driver_opts:
      type: 'none'
      o: 'bind'
      device: './gitea-data/'
  jenkins_home:
    driver: local
    driver_opts:
      type: 'none'
      o: 'bind'
      device: './jenkins_home/'
```

ラズパイのポート80とポート222をNginxコンテナのポート80と222にバインドします．
JenkinsとGiteaとGiteaDBサーバはNginxを介して外部と通信行います．
さらにJenkinsとGiteaは個別のDockerネットワークに配置されます．
これにより、

* GiteaとJenkinsで同じポートが使える
* セキュリティの向上

ができます．

### Nginxの設定
ラズパイ内で複数のサーバを動かしたい場合、ポートやURLでサーバへのアクセスを切り替える必要があります．
そこで、Nginxを使います．Nginxはnginx.confに記述されたルールに基づいてサーバへのアクセスを適切なサーバに転送します．
以下がnginx.confの内容です．

```conf:nginx.conf
user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;


events {
  worker_connections  1024;
}


http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;

  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

  access_log  /var/log/nginx/access.log  main;

  sendfile        on;
  #tcp_nopush     on;

  keepalive_timeout  65;

  gzip  on;
  upstream jenkins {
    keepalive 32; # keepalive connections
    server jenkins:8080; # jenkins ip and port
  }

  # Required for Jenkins websocket agents
  map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
  }

  server {
  listen          80;       # Listen on port 80 for IPv4 requests

  server_name     127.168.11.20;  # replace 'jenkins.example.com' with your server domain name
  client_max_body_size 100M;


  # pass through headers from Jenkins that Nginx considers invalid
  ignore_invalid_headers off;

  location /gitea/ {
    proxy_pass http://gitea:3000/;
    proxy_http_version  1.1;
    proxy_set_header    Upgrade $http_upgrade;
    proxy_set_header    Connection "upgrade";
    proxy_set_header    Host $host;
    proxy_set_header    X-Real-IP $remote_addr;
    proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header    X-Forwarded-Proto $scheme;
  }

  location ~ "^/static/[0-9a-fA-F]{8}\/(.*)$" {
    # rewrite all static files into requests to the root
    # E.g /static/12345678/css/something.css will become /css/something.css
    rewrite "^/static/[0-9a-fA-F]{8}\/(.*)" /$1 last;
  }

  location /userContent {
    # have nginx handle all the static requests to userContent folder
    # note : This is the $JENKINS_HOME dir
    root /var/lib/jenkins/;
    if (!-f $request_filename){
      # this file does not exist, might be a directory or a /**view** url
      rewrite (.*) /$1 last;
      break;
    }
    sendfile on;
  }

  location / {
      sendfile off;
      proxy_pass         http://jenkins;
      proxy_redirect     default;
      proxy_http_version 1.1;

      # Required for Jenkins websocket agents
      proxy_set_header   Connection        $connection_upgrade;
      proxy_set_header   Upgrade           $http_upgrade;

      proxy_set_header   Host              $host;
      proxy_set_header   X-Real-IP         $remote_addr;
      proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
      proxy_set_header   X-Forwarded-Proto $scheme;
      proxy_max_temp_file_size 0;

      #this is the maximum upload size
      client_max_body_size       10m;
      client_body_buffer_size    128k;

      proxy_connect_timeout      90;
      proxy_send_timeout         90;
      proxy_read_timeout         90;
      proxy_buffering            off;
      proxy_request_buffering    off; # Required for HTTP CLI commands
      proxy_set_header Connection ""; # Clear for keepalive
  }
  }

}

stream {
  upstream ssh {
    server gitea:222;
  }

  server {
    listen      222;
    listen      [::]:222;
    proxy_pass ssh;
  }
}
```

nginx.confで行っている設定は以下のとおりです．

* ポート222のアクセスをGiteaコンテナに転送
* /gitea/パスへのアクセスをGiteaコンテナに転送
* それ以外はJenkinsコンテナに転送

### Giteaの設定
Docker内で複数のサーバを起動します．先述した通りDocker内のサーバへのアクセスをパスで振り分けます．
GiteaはNginxから転送されたURLに対応するためにサーバ設定を変更する必要があります．
以下が変更内容です．

```ini:app.ini
; 省略

[server]
DOMAIN           = <ラズパイのIP>
SSH_DOMAIN       = <ラズパイのIP>
; 省略
ROOT_URL         = http://<ラズパイのIP>/gitea/

; 省略
```

### Jenkinsの設定
セキュリティのためJenkinsでビルドをしたい場合はビルド環境をJenkinsノードという別のサーバにセットアップします．
しかし、これ以上ラズパイで起動するサーバを増やすと設定が複雑になるだけでなくCPUが限界を迎えそうです．
そこで、JenkinsコンテナにC/C++のビルド環境をセットアップして、Jenkinsだけでビルドを実行できるようにします．
ビルド環境のセットアップにはDocker/Jenkins/Dockerfileに記述します．

```Dockerfile:Dockerfile
FROM jenkins4eval/jenkins
USER root
RUN apt-get update && apt-get upgrade -y
RUN apt-get install -y build-essential cmake astyle
RUN apt-get install -y python3 python3-pip
RUN pip3 install virtualenv
USER jenkins
EXPOSE 8080
EXPOSE 50000
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/jenkins.sh"]
```

jenkins4evalイメージに

* python3
* pip
* build-essential(gccやmakeなど)
* cmake
* astyle(コード整形ツール)

を追加して実行できるようにしています．

## Dockerの起動
設定したDockerの起動にはdocker-compose.ymlがあるディレクトリで以下のコマンドを実行します．

```bash:
sudo docker-compose up
```

自動起動・停止できるようにこの処理をstart\_server.shとstop\_server.shに追記します．
追記したあとのstart\_server.shとstop\_server.shは以下のとおりです．

```bash:start_server.sh
#! /bin/bash
pushd $(dirname ${BASH_SOURCE}) 1> /dev/null

# launch glances
printf "youkan" | glances -w --username --password --port 20000 1> /dev/null 2> /dev/null & disown

# launch testinfra
pushd ./testinfra 1> /dev/null
python3 server.py 1> /dev/null 2> /dev/null & disown
popd 1> /dev/null

# ↓ added for Docker↓

# launch docker servers
pushd ./Docker 1> /dev/null

# set up data directories if not exist
mkdir -p "./Gitea/gitea-data/"
mkdir -p "./GiteaDB/giteadb-data/"
mkdir -p "./Docker/Jenkins/jenkins_home/"

docker-compose up -d & disown
popd 1> /dev/null

# ↑ added for Docker↑

popd 1> /dev/null

```

```bash:stop_server.sh
#! /bin/bash
pushd $(dirname ${BASH_SOURCE}) 1> /dev/null

# stop glances
pkill glances

# stop testinfra
pkill -f server.py

# ↓ added for Docker↓

# stop docker server
pushd ./Docker 1> /dev/null
docker-compose down
popd 1> /dev/null

# ↑ added for Docker↑

popd 1> /dev/null
```

# サーバの使い方

初回起動時はJenkinsのパスワードを入力する必要があります．

![UnlockJenkins.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77676/93bd6967-b9ab-f97d-2d19-f14f4075c903.png)

パスワードファイルはjenkinsコンテナの/var/jenkins\_home/secrets/initialAdminPasswordファイルに記述されています．
以下のコマンドでファイルの中身を読み出しましょう

```bash:
sudo docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
```

Giteaにアクセスしたい場合はウェブブラウザを使って以下のURLにアクセスします．

http://<ラズパイのIP>/gitea/

Jenkinsにアクセスしたいときはウェブブラウザを使って以下のアドレスにアクセスします．

http://<ラズパイのIP>/jenkins/

あとは通常のGiteaとJenkinsです．

# サーバの保守
できればサーバの稼働状況をメールで通達するような仕組みが望ましいのです．しかし、そうなるとラズパイにメールサーバの設定をしなければなりません．メールサーバの設定を行うと迫撃砲（簡単開発支援サーバ）の魅力である「持っていって設置するだけで支援砲火を打てる」ができなくなります．そこで、稼働状況の通知は諦めてかわりに簡単に確認できるようにしました．
ウェブブラウザで指定されたポートにアクセスするだけでサーバの状況を把握できるようになっています．

## サーバの稼働状況の表示
`<サーバのIP>:20000`にアクセスするとglancesのページが表示されます．

このページから

* 実行中のプロセスの状態
* CPUとメモリの消費量
* ストレージの消費量

が表示されます．

## 単体テストの実行
`<サーバのIP>:30000`にアクセスるとtestinfraを使った単体テストが実行されます．
テスト結果はウェブブラウザに表示されます．

![testinfra.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77676/43b63cea-df9e-4d91-6f6a-42d284d9a15d.png)


問題があった場合、表に**NG**が表示されます．NGとなったテストが現れたときはserver.pyの内容と照らし合わせながら原因を追求します．

## サーバの移行

今回構築した迫撃砲(簡単開発支援サーバ)は少人数のチームのためのものです．
チームの人数が増えてストレージ容量が足りなくなって来たらサーバの移行を検討しましょう．幸い単体テストにサーバのストレージ容量をチェックする項目があります．定期的に単体テストの結果をチェックすればストレージ容量不足の前兆を見逃しません．単体テストのストレージ消費量の項目はストレージの消費量が70%を超えたらNGになります．NGが出たら移行を検討すべきです．

glancesとtestinfraはPythonで実装されているので普通の(x86アーキテクチャのCPUを搭載する)サーバで普通に動きます．
問題はDockerで動作するJenkinsとGiteaです．これらはarmアーキテクチャのためのイメージを使っています．
無理矢理x86アーキテクチャのサーバを動かそうとするとCPUアーキテクチャが違うので動作しません．
JenkinsとGiteaのイメージを移行先のサーバのCPUアーキテクチャに合わせたイメージに差し替えましょう．

サーバのデータは以下のディレクトリに格納されています．

* Docker/Gitea/gitea-data/
* Docker/GiteaDB/giteadb-data/
* Docker/Jenkins/gitea-data/

移行先にそのままこれらのディレクトリの中身をコピーすれば移行完了です．
データのバックアップもこれらのディレクトリの中身をバックアップするだけで良いです．

# 注意点

「隊長！！敵（ネットワーク管理者）にわれわれの位置がバレたようです！！」
「よし．潮時だな．位置を変えるぞ！！」

（使用に際してはちゃんと許可をとってください．）

# 参考文献

- [How to set a Raspberry Pi with a static ip address?](https://www.ionos.com/digitalguide/server/configuration/provide-raspberry-pi-with-a-static-ip-address/)
- [SSH公開鍵認証で接続するまで](https://qiita.com/kazokmr/items/754169cfa996b24fcbf5)
- [How to disable ssh password login on Linux to increase security](https://www.cyberciti.biz/faq/how-to-disable-ssh-password-login-on-linux/)
- [Github:nicolargo/glances](https://github.com/nicolargo/glances/wiki/How-to-contribute-to-Glances-%3F)
- [Testinfra test your infrastructure](https://testinfra.readthedocs.io/en/latest/)
- [freedesktop.org:systemd](https://www.freedesktop.org/wiki/Software/systemd/)
- [Gitea](https://gitea.io/en-us/)
- [Nginx](https://www.nginx.com/)
- [Jenkins](https://www.jenkins.io/)
- [Docker](https://www.docker.com/)
- [Draw.io](https://app.diagrams.net/)
- [Wikipedia:迫撃砲](https://ja.wikipedia.org/wiki/%E8%BF%AB%E6%92%83%E7%A0%B2)

# 補足
この記事は

* [Qiitaエンジニアフェスタ_Dockerシステム構成](https://qiita.com/official-events/339b6440dbd578f4f66f)
* [Qiitaエンジニアフェスタ_開発環境](https://qiita.com/tags/qiita%e3%82%a8%e3%83%b3%e3%82%b8%e3%83%8b%e3%82%a2%e3%83%95%e3%82%a7%e3%82%b9%e3%82%bf_%e9%96%8b%e7%99%ba%e7%92%b0%e5%a2%83)

の応募のために作成されました．

Qiitanぬいぐるみがほしい！！