GiteaとJenkinsによるCI/CDの実例サンプル（Python / PHP / Perl でテストとデプロイを自動化する）

# はじめに
先日、**[GiteaとJenkinsを利用したCI/CD環境を構築](https://qiita.com/bashaway/items/4368b2fd46003d674223)**しました。
この環境を利用して、テストとデプロイを自動化するサンプルを動かして、いわゆるCI/CDに入門してみようと思います。

対象はPHPとPythonとPerlで作成したHTMLを生成するだけのアプリです。
できるだけ簡素なサンプルとしたかったので、フレームワークは利用していません。

簡単なサンプルで一連の流れが自動化できた（と思ってる）ので、これを「ひな型」として拡張していけたらいいかな、と思います。


# これをやります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/4c8570a6-3602-bc65-8c97-eb9b4b979896.png)

`user01`というひとが、nginxが動作している開発サーバ(web-dev)上で`sample`というアプリを作成していると仮定します。

① user01 が作成したアプリをリポジトリにgit pushすると、、、
② GiteaからSlackおよびJenkinsのwebhookをたたく（この時点でSlackにはuser01/sampleにpushされたよ、という通知が飛ぶ）
③ Jenkinsではuser01/sampleにpushがあったという通知をGiteaプラグインが受ける
④ pushされたuser01/sampleにJenkinsfileがあるかチェックする
⑤ Jenkinsfileがあればそれをもとにpipeline処理を開始する
⑥ test stageでは、開発サーバにremoterというユーザでSSHログインして各テストを実施
⑦ test stageをパスした場合のみ、開発サーバでAnsibleのデプロイ用Playbookを実行
⑦' Playbookでは、公開サーバ(web-pub)にSSHログインしてpushされたリポジトリをcloneする
⑧ pipelineの結果をJenkins内部で通知して
⑨ Slackのwebhookをたたいてpipelineのサマリを通知する


# 対象機器および環境

## 検証環境
* CentOS8 (8.2.2004)
* Gitea (1.13.0+dev)
* Jenkins (2.251)
* PHP / PHP-FPM ( 7.2.24 )
* Python ( 3.8.0 )
* Perl ( 5.26.3 )


## サーバ

今回の検証で利用するサーバは以下の２種類とします。

* Gitサーバ：Gitea＋Jenkins（→**[この環境](https://qiita.com/bashaway/items/4368b2fd46003d674223)**）
* webサーバ：Nginx＋CGIが動く環境（→**[この環境](https://qiita.com/bashaway/items/16c2aba74a8328ed9618)**）

Gitea+Jenkinsは前回作成したものをそのまま利用します。
webサーバは手抜きしてしまったので、１台を開発/公開で兼用し、別FQDNで別ディレクトリを参照させるようにします。
別サーバにする場合は、後述する`remoter`ユーザを双方に準備しておく必要があります。
また、各スクリプトをCGIとして動作するようにしておきます。


## アプリケーションのディレクトリ構造

今回は `sample` というアプリを作成します。これがそのままリポジトリ名です。
ディレクトリ構造は以下の通りです。
http://[公開サーバ]/sample/ へのアクセスを `sample/src` とすることで、公開コンテンツ以外にはアクセスさせないようにします。

```:
./sample/
|-- Jenkinsfile <----------------- Jenkinsの自動ビルド処理
|-- deploy <---------------------- webサーバにデプロイするためのAnsible
|   |-- ansible.cfg
|   |-- hosts
|   `-- playbook_deploy-web.yml
|-- src <------------------------- アプリケーションディレクトリ（ここをwebで公開する）
|   |-- index.html
|   |-- index.php
|   |-- index.pl
|   |-- index.py
|   `-- lib <--------------------- アプリケーションで利用するライブラリ群
|       |-- math_modules.php
|       |-- math_modules.pm
|       `-- math_modules.py
`-- test <------------------------ 単体テスト用のテストコード
    |-- perltest.t
    |-- phptest.php
    `-- pythontest.py
```

# 作業内容

## webサーバでスクリプトをCGIとして動作させる

nginxでスクリプトをCGIとして動作させるための設定をしておきます。
**[CentOS8の初期状態から nginx で php python perl ruby を動かすまで](https://qiita.com/bashaway/items/16c2aba74a8328ed9618)**


コンテンツ置き場のディレクトリを作成し、権限を付与しておきます。

```shell:ディレクトリ作成
mkdir /usr/share/nginx/html-{dev,pub}
chmod 777 /usr/share/nginx/html-{dev,pub}
```


`web-dev.prosper2.net` へのアクセスを開発サーバとし、ドキュメントルートを `/usr/share/nginx/html-dev/` にしておきます。
また `sample` へのアクセスは `sample/src` へ向けるようにしておきます。


```nginx:/etc/nginx/conf.d/web-dev.conf
server {
    listen 80;
    server_name web-dev.prosper2.net;

    root   /usr/share/nginx/html-dev/;
    index  index.html;

    location  ~ ^/sample/ {

        location ~ ^/sample/src/(.*) {
            internal;

            location ~ \.(php|phar)$ {
                include        fastcgi_params;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                fastcgi_pass   unix:/run/php-fpm/www.sock;
            }

            location ~ \.(py|pl|rb)$ {
                include        fastcgi_params;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                fastcgi_pass   unix:/var/run/spawn-fcgi.socket;
            }

        }

        rewrite  ^/sample/(.*) /sample/src/$1 last;

    }
}
```

`web-pub.prosper2.net` へのアクセスを公開サーバとし、ドキュメントルートを `/usr/share/nginx/html-pub/` にしておきます。
先ほどと同様に `sample` へのアクセスは `sample/src` へ向けるようにしておきます。

```nginx:/etc/nginx/conf.d/web-pub.conf
server {
    listen 80;
    server_name web-pub.prosper2.net;

    root   /usr/share/nginx/html-pub/;
    index  index.html;

    location  ~ ^/sample/ {

        location ~ ^/sample/src/(.*) {
            internal;

            location ~ \.(php|phar)$ {
                include        fastcgi_params;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                fastcgi_pass   unix:/run/php-fpm/www.sock;
            }

            location ~ \.(py|pl|rb)$ {
                include        fastcgi_params;
                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
                fastcgi_pass   unix:/var/run/spawn-fcgi.socket;
            }

        }

        rewrite  ^/sample/(.*) /sample/src/$1 last;

    }
}
```


```:nginxの設定を再読み込み
systemctl reload nginx
```

## PHPUnit（PHPのテストツール）を導入する。

PHPのバージョンで利用できるPHPUnitのバージョンが変わるため、以下から適切なものをダウンロードします。
対応表：https://phpunit.de/supported-versions.html
入手先：https://phar.phpunit.de/

何も考えずにCentOS8にPHPをインストールすると、7.2が入るので、PHPUnitは8系を導入します。

```shell:PHPUnitの導入
curl -L https://phar.phpunit.de/phpunit-8.5.8.phar -o phpunit
chmod +x phpunit
cp phpunit /usr/local/bin
```


## Test::More（Perlのテストモジュール）を導入する。

Perlでテストを行うために必要なものは `prove` コマンドと `Test::More` モジュールです。

```shell
dnf install -y perl-Test-Simple perl-Test-Harness
```


## リポジトリの準備

アプリケーションのためのリポジトリを準備します。

### Giteaでリポジトリの作成と設定

今回は新たなユーザ `user01` を作成し、その中に `sample` というリポジトリを作成します。
リポジトリを作成したら、このリポジトリに対してJenkinsとSlackのwebhookを設定しておきます。
これで、リポジトリへのpushのタイミングで、Slackへの通知と、Jenkinsでパイプライン処理の実行ができるようになります。


**ユーザの作成**
メニューの「サイト管理」→「ユーザーアカウント」から「ユーザーアカウントを作成」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/3785201d-5277-365c-2dcf-0578bf92757b.png)

適当に情報をいれてユーザを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/307d7a86-78ff-a6cb-94eb-3c1ae5ed2b74.png)

user01でログインしなおして公開鍵を登録しておく
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/776f35ba-355c-2543-fcf9-5c4c2e50b409.png)


**リポジトリの作成**
上部メニューから「新しいリポジトリ」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/8850852b-a49c-af55-7f30-95cae278de2b.png)

オーナーは自分、リポジトリ名を `sample` としておきます。
（公開・非公開はどちらでもOKです）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/1baf2ef0-266d-046e-013c-5183011c4ca3.png)


**リポジトリにSlackとJenkinsのwebhookを設定**
リポジトリへのpushを検知し、Slackへの通知と、Jenkinsのビルドトリガを行うため、webhookを設定します。

リポジトリの設定メニュからWebhookのSlackを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/273c4b0a-f0a5-7cf9-952f-1e3d4e5bf958.png)

必要な情報を入力します。
（Slackアプリが未作成の場合は **[ここ](https://qiita.com/bashaway/items/4368b2fd46003d674223#gitea%E3%81%ABslack%E3%81%AEwebhook%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B%E7%B5%84%E7%B9%94%E5%8D%98%E4%BD%8D)** のとおり設定しておく）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/7a167b9e-f9c5-8a51-ea85-4ac702aefceb.png)

ターゲットURLはSlackアプリ設定のページの以下の場所のやつです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/e573b97a-757b-6a10-1062-dec5a34449f4.png)

同様にリポジトリの設定メニュからWebhookのGiteaを選択し、JenkinsのWebhook情報を入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/7dba05b7-3d49-6c49-f448-d90dacbe574f.png)


**リポジトリへのpush**
このサーバで初めてgitを利用する場合は、自身の設定をしておきます。

```
git config --global user.name "user01"
git config --global user.email user01@example.com
```

ためにし、開発用のディレクトリに移動して、このリポジトリへpushしてみます。

```
mkdir /usr/share/nginx/html-dev/sample
cd /usr/share/nginx/html-dev/sample
echo '# Sample Project' > README.md
git init

git add README.md
git commit -m "first commit"
git remote add origin gitea@gitea.prosper2.net:user01/sample.git
git push -u origin master
```

Giteaにpushされました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/7f5e0c8a-0647-501e-290c-684a4f9b2281.png)

Slackにも通知がきました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/fd003fcc-f414-8699-7b65-b637f9def9ef.png)


**リポジトリにJenkinsからアクセスできるようにトークンを発行**
Jenkinsへpushを通知した際に、JenkinsのGiteaプラグインからアクセスできるようにするため、アクセストークンを発行します。
「設定」→「アプリケーション」からアクセストークンを発行しておきます。
（トークンは作成直後に１度だけ表示されるので、控えておきます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/8a249c51-ac68-4cd9-cd16-3ecfc900dbde.png)


### Jenkinsでsampleアプリ用のプロジェクトを作成

前回構築手順では、組織に対してプロジェクトを作成しましたが、今回は個別ユーザのリポジトリですので、個別ユーザのプロジェクトを作成します。

サイドメニュの「新規ジョブ作成」を選択し、「Gitea Organization」でジョブを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/9ce6f65a-240c-3fd8-7bd7-99ee0d9d4629.png)

「Credentials」→「追加」として、作成したジョブ名称（今回は `user01 - sample` としました）を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/773cd1a2-aad4-ee5d-fc5c-5506a7376177.png)

「種類」を `Gitea Personal Access Token` とし、さきほど作成しておいたGiteaのアクセストークンを入力して「追加」します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/fc8ee611-bf09-47a2-cc8a-b9ffa19743d8.png)

もどった画面で「Credentials」にいま作成したアクセストークンを選択、「Owner」にはGiteaのユーザ名（今回は `user01` ）を入力します。
そのほかはデフォルトのままでOKなので、「保存」します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/c35bc2fd-17db-482b-7b89-fcabbb003f08.png)

ジョブ作成直後に設定に従って、 `user01` が所有するリポジトリを走査します。
画面では `sample` というリポジトリが検出されましたが、Jenkinsfileがない、と言われています。（現時点では作成していないのでこれでOKです）
今後の手順でJenkinsfileにpipelineの記述をして、自動的にテストやデプロイを行わせるようにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/ae10ee67-6cc5-6fbc-1702-9800046b0e12.png)


## アプリケーションの作成

ここでは、 `sample` という名称のアプリケーションを作成していきます。
同時にテストコードも作成しておきます。

まず、今後必要となるディレクトリを作成しておきます。

```
mkdir /usr/share/nginx/html-dev/sample/{src,test,deploy}
mkdir /usr/share/nginx/html-dev/sample/src/lib
```

### 各スクリプトへのインデックスファイル

動作確認のため、各スクリプトへのインデックスを準備します。

```html:/usr/share/nginx/html-dev/sample/src/index.html
<html>
<body>

<h2>PHP script</h2>
<a href=/sample/index.php>index.php</a><BR>

<h2>Perl script</h2>
<a href=/sample/index.pl>index.pl</a><BR>

<h2>Python script</h2>
<a href=/sample/index.py>index.py</a><BR>

</body>
</html>
```


### PHPアプリケーション

アプリケーション本体

```php:/usr/share/nginx/html-dev/sample/src/index.php
<?php

include('./lib/math_modules.php');

print "<html><body>\n";
print "Hello PHP Script!<BR>\n";

for ($xx=1; $xx<10; $xx++) {
    for ($yy=1; $yy<10; $yy++) {
        printf("%4d",$xx*$yy);
    }
    print "<BR>\n";
}

print "NEW FUNCTIONS<BR>\n";
print "factorial(4) = ".math_modules::factorial(4)."<BR>\n";
print "</body></html>\n";
```

モジュール

```php:/usr/share/nginx/html-dev/sample/src/lib/math_modules.php
<?php

class math_modules {

  public function factorial($n) {
    if ($n > 0) {
      return $n * math_modules::factorial($n - 1);
    } else {
      return 1;
    }
  }

}
```

テストコード

```php:/usr/share/nginx/html-dev/sample/test/phptest.php
<?php

include('../src/lib/math_modules.php');

class math_modules_test extends \PHPUnit\Framework\TestCase {
    protected $object;

    protected function setUp(): void {
        $this->object = new math_modules();
    }

    public function testFactorial() {
        $this->assertEquals(24, $this->object->factorial(4));
        $this->assertEquals(1, $this->object->factorial(0));
    }

}
```

`test` ディレクトリで `phpunit` を実施

```shell:PHPテストを実施
~test$ phpunit --cache-result-file=. ./phptest.php
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 130 ms, Memory: 10.00 MB

OK (1 test, 2 assertions)
~test$ 
```

### Pythonアプリケーション

アプリケーション本体


```python:/usr/share/nginx/html-dev/sample/src/index.py
#!/usr/bin/python3

from lib import math_modules

if __name__ == "__main__":
    print("HTTP/1.0 200 OK")
    print("Content-type: text/html\n")
    print("<html><body>");
    print("Hello Python Script!<BR>")

    for xx in range(1,10):
        for yy in range(1,10):
            print('%4d' % (xx*yy) , end="")
        print("<BR>")

    print("NEW FUNCTIONS<BR>")
    print("factorial(4) = " + str(math_modules.factorial(4)) + "<BR>")
    print("</body></html>");
```

アプリケーション本体には実行権限を与えておきます

```
chmod 755 /usr/share/nginx/html-dev/sample/src/index.py
```


モジュール

```python:/usr/share/nginx/html-dev/sample/lib/math_modules.py
def factorial(n):
    if n > 0:
        return n * factorial(n - 1)

    else:
        return 1
```

テストコード

```python:/usr/share/nginx/html-dev/sample/test/pythontest.py
import unittest
import sys
sys.path.append('../')
from src.lib import math_modules

class factorialTest(unittest.TestCase):
    def setUp(self):
        pass

    def tearDown(self):
        pass

    def test_first(self):
        self.assertEqual(24, math_modules.factorial(4))

    def test_second(self):
        self.assertEqual(1, math_modules.factorial(0))


if __name__ == "__main__":
    unittest.main()
```

`test` ディレクトリで テストコードを実行

```shell:Pythonテストを実施
~test$ python3 ./pythontest.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
~test$ 
```


### Perlアプリケーション

アプリケーション本体

```perl:/usr/share/nginx/html-dev/sample/src/index.pl
#!/usr/bin/perl

use strict;
use warnings;

use lib qw(./lib/);
use math_modules;

my ($xx,$yy);

print "Content-type: text/html \n\n";
print "<html><body>\n";

print "Hello Perl Script!<BR>\n";

for ($xx=1; $xx<10; $xx++) {
    for ($yy=1; $yy<10; $yy++) {
        printf("%4d",$xx*$yy);
    }
    print "<BR>\n";
}

print "NEW FUNCTIONS<BR>\n";
print "factorial(4) = ".math_modules::factorial(4)."<BR>\n";
print "</body></html>\n";

exit;
```

アプリケーション本体には実行権限を与えておきます

```
chmod 755 /usr/share/nginx/html-dev/sample/src/index.pl
```


モジュール

```perl:/usr/share/nginx/html-dev/sample/lib/math_modules.pm
package math_modules;

use strict;
use warnings;

sub factorial {
  my $n = shift;
  if ($n > 0) {
    return $n * factorial($n - 1);
  } else {
    return 1;
  }
}

1;
```

テストコード

```perl:/usr/share/nginx/html-dev/sample/test/perltest.t
use strict;
use warnings;

use lib qw(../src/lib);

use Test::More;
use math_modules;

{
  ok(math_modules::factorial(4) == 24, 'factorial');
  ok(math_modules::factorial(0) == 1, 'factorial');
}

done_testing;
```

`test` ディレクトリで `prove` を実施

```shell:Perlテストを実施
~test$ prove ./
./perltest.t .. ok
All tests successful.
Files=1, Tests=2,  0 wallclock secs ( 0.03 usr  0.00 sys +  0.06 cusr  0.02 csys =  0.11 CPU)
Result: PASS
~test$ 
```


### ブラウザから動作確認

http://web-dev.prosper2.net/sample/index.html へアクセス後、各スクリプトが動作するか確認する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/58a8ad40-88ea-2b5b-be3d-a13e7471672c.png)


## Jenkinsによるテストの自動化

Jenkinsfileを準備し、一連のテストを連続で実施するパイプラインを作成ます。
リポジトリへのpushをGiteaが検知し、Jenkinsへのwebhookを叩きパイプラインを実行させるようにしてみます。


### ノードの準備

Jenkinsのパイプライン処理を行うサーバのことを「ノード」といい、デフォルトでJenkinsサーバそのものが `master` として存在しています。
今回は、アプリ開発をしているwebサーバをノードとして定義し、リポジトリへのpushを検知してテスト実施用のユーザが自動的にログインしてパイプライン処理を実施するように設定します。

webサーバ上でテストを自動的に実施するためのユーザを作成し、公開鍵認証でログインできるようにしておきます。
今回はこのユーザを `remoter` として作業対象のノードに作成しておきます。
鍵ペア作成時には秘密鍵のパスフレーズも設定しておきます。
秘密鍵は一度表示させて、どこかに控えておいたら、サーバ上からは削除しておきます。

```shell:webサーバ上でアカウント作成する
useradd remoter
su remoter
ssh-keygen -t rsa -b 4096
mv ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
cat ~/.ssh/id_rsa
rm ~/.ssh/id_rsa
```

また、Jenkinsノードとするために、ノード側にもjavaの導入が必要です。

```shell:webサーバにもjavaをいれておく
dnf install -y java-11-openjdk-devel java-11-openjdk
```

「Jenkinsの管理」→「ノードの管理」→「新規ノード作成」をクリックし、作成するノード名を入力して「OK」します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/c6618381-1299-7d4c-8a80-8bc5572c617b.png)


リモートFSルート：このノードのワークスペースディレクトリを選択します。（今回はホームディレクトリそのまま）
用途：今回はアプリのテストで利用するので、特定ジョブ専用にします。
起動方法：JenkinsからSSHでwebサーバにログインするので、これにします。
ホスト：対象ホストのFQDNを入力します。
Host Key Verification Strategy：known_hostsのチェックですが、今回は無視するため、 `Non verifying～` とします。

認証情報に先ほど作成した `remoter` の秘密鍵を登録するため、「追加」→「Jenkins」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/f2b2ecdc-b349-eddf-e3b4-ebec487c8cb0.png)

先ほど作成した秘密鍵とパスフレーズを登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/8bac0ee7-9aa2-76d6-1ac1-9d8e35001b64.png)

もとの画面に戻ったら、認証情報に登録した情報（今回は `remoter` ）を選択し、「保存」します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/bf6ba86b-8511-ad5a-cd79-f21822e78d84.png)

作成したノードのチェックが行われ、問題なければ、下記のように取得した情報で更新されます。
更新されない場合は、画面上の「ステータス更新」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/120012c5-479c-8911-b1c4-132e9e04796c.png)


remoterの認証情報は groovy スクリプトでも作成できます。

```:JENKINS_HOME/init.groovy.d/credentials.groovy
import jenkins.model.*
import com.cloudbees.plugins.credentials.*
import com.cloudbees.plugins.credentials.common.*
import com.cloudbees.plugins.credentials.domains.*
import com.cloudbees.jenkins.plugins.sshcredentials.impl.*
import hudson.plugins.sshslaves.*;

def domain = Domain.global()
def store = Jenkins.instance.getExtensionList('com.cloudbees.plugins.credentials.SystemCredentialsProvider')[0].getStore()
def keyFileContents = new File("/path/to/privkey.pem").text

def privateKey = new BasicSSHUserPrivateKey(
  CredentialsScope.GLOBAL,
  'remoter-privkey',
  'remoter',
  new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(keyFileContents),
  '',
  'remoter ssh private key'
)

store.addCredentials(domain, privateKey)
```

### Jenkinsfileの準備

パイプライン処理でテストを実施させるために、Jenkinsfileを準備します。
PHP、Python、Perlの各テストコードの戻り値は成功すると０、失敗すると０以外、となるので、これでステージの成否を判断します。

各ステージはテストのみなので、テストのステージを並列処理するか、各ステージで失敗しても次の処理に遷移できるようにしておきます。
今回は各ステージを直列につないで、エラー発生しても中断しないように `catchError` しています。

```groovy:/usr/share/nginx/html-dev/sample/Jenkinsfile
pipeline {

  // 作成したエージェントを選択する
  agent { node { label 'node-web-dev' } }

  stages {

    // 省略可：ホントにノードにログインできてるか確認しておく
    stage('Check environment') {
      steps {
        sh "cat /etc/redhat-release ; id ; hostname ; pwd ; ls"
      }
    }

    // PHPアプリのテストを行うステージ
    stage('Test PHP applications : PHPunit') {
      steps {

        // 変数に代入するのでscriptで処理させる必要がある
        script {

          // shをreturnStatus:trueにしてテスト結果を取得してRESULTに代入する
          RESULT = sh(returnStatus: true, script: 'cd test && phpunit --cache-result-file=. ./phptest.php')

          // 戻り値が０以外なら失敗とみなして強制的にエラーにさせるが、
          // 処理を中断させない（＝次のステージのテストは継続実施させる）ようにcatchErrorしておく
          if ( RESULT ){
            catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
              error message: "TEST FAIL"
            }
          }
        }
      }
    }


    // Pythonアプリのテストを行うステージ
    stage('Test Python applications : unittest') {
      steps {
        script {
          RESULT = sh(returnStatus: true, script: 'cd test && python3 ./pythontest.py')
          if ( RESULT ){
            catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
              error message: "TEST FAIL"
            }
          }
        }
      }
    }


    // Perlアプリのテストを行うステージ
    stage('Test Perl applications : Test::More') {
      steps {
        script {
          RESULT = sh(returnStatus: true, script: 'cd test && prove ./perltest.t')
          if ( RESULT ){
            catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
              error message: "TEST FAIL"
            }
          }
        }
      }
    }


  }

  // パイプライン処理終了後に行う処理
  post {
    // ビルドの成否にかかわらず、cleanWsでワークスペースを掃除する
    always {
      cleanWs()
    }
  }

}
```

### リポジトリへpushする

Jenkinsファイルが作成できたら、これまでに作成したアプリケーションも含めてリポジトリへpushします。

```shell:
cd /usr/share/nginx/html-dev/sample
git add -A
git commit -m "pipeline job : Jenkinsfile" 
git push origin master
```

これで、Giteaでpushを検知してJenkinsのwebhookを叩いて、JenkinsのGiteaプラグインがsampleにあるリポジトリを走査します。
検出されたリポジトリにJenkinsfileが存在すれば、ビルドが走ります。

Jenkinsでのビルド結果はこんな感じで、すべてPassしているのがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/69e854ad-cc22-e036-040e-8d164c5231d8.png)

Slack通知は、GiteaにPushされたものと、Jenkinsビルド結果、の２通が通知されてきています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/b4340df3-e882-ab14-7035-dcbc24c832d4.png)


### わざとエラーになるようにモジュールを修正してリポジトリへpushする

PHPアプリのモジュールの一部を変更してテストが失敗するようにして、再度pushしてみます。

```shell:
sed -i 's/return 1/return 2/'  ./src/lib/math_modules.php
git add ./src/lib/math_modules.php
git commit -m "create bug : math_modules.php"
git push origin master
```

ステージ途中でエラーになっても中断しないようにJenkinsfileを作成しているので、PHPの部分はfailedですが、後続のテストが行われています。
ビルド全体はfailとなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/2c3ac2aa-5b1c-3852-339d-421a30f7f044.png)

同じくSlackでも通知がきています。こちらもビルド結果はfailとなっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/d124360d-b3ab-0814-8253-978a6f955879.png)


## Jenkinsによるデプロイの自動化

さきほどのJenkinsfileを修正し、テストが全部Passしたら、公開サーバにこのリポジトリのmasterブランチをcloneしてみます。
cloneはAnsibleで準備し、Jenkinsから自動でデプロイでも、手動でデプロイでも実施できるようにしておきます。
Jenkinsのpipeline処理で開発サーバにSSHログインした状態でPlaybookを実行するようにします。


### Ansibleのインストール

開発サーバ上でPlaybookを実行するため、Ansibleが導入されていない場合は、pipでインストールしておきます。

```shell:webサーバにansibleをインストールする
pip3 install pip -U
pip3 install ansible
```


### AnsibleのPlaybookを作成

開発サーバ上でPlaybookを作成します。

```ini:/usr/share/nginx/html-dev/sample/deploy/ansible.cfg
[defaults]
inventory = ./hosts

[ssh_connection]
ssh_args = -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ForwardAgent=yes
```


```:/usr/share/nginx/html-dev/sample/deploy/hosts
[web_pub]
web-pub.prosper2.net
```

```yaml:/usr/share/nginx/html-dev/sample/deploy/playbook_deploy-web.yml
- hosts: web_pub
  tasks:

    - name: Git clone sample application
      git:
        repo: "gitea@gitea.prosper2.net:user01/sample.git"
        dest: "/usr/share/nginx/html-pub/sample"
        version: "master"
        accept_hostkey: yes
```


自分の公開鍵がwebサーバ上にない場合は作成しておきます。
これは開発サーバ→公開サーバへSSHログイン（今回の場合は同一ホストですが。。。）するため、公開サーバに公開鍵を置く、ということです。

```shell:公開サーバに公開鍵を置く
vi ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
```

手動で実行して本番サーバへログインしてリポジトリのクローンができるか確認しておきます。

```shell:
$ ansible-playbook playbook_deploy-web.yml

PLAY [web_pub] ***************************************************************************

TASK [Gathering Facts] *******************************************************************
ok: [web-pub.prosper2.net]

TASK [Git clone sample application] ******************************************************
changed: [web-pub.prosper2.net]

PLAY RECAP *******************************************************************************
web-pub.prosper2.net       : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

```

無事にクローンできていることが確認できたら、削除しておきます。

```shell
$ ls /usr/share/nginx/html-pub/sample/
Jenkinsfile  README.md  src  test

$ rm -rf /usr/share/nginx/html-pub/sample/
```

### Giteaにremoterのユーザを追加して権限付与する

Jenkinsからpipelineを実行するユーザの `remoter` がGiteaのリポジトリにアクセスしてcloneしてくるので、Giteaにユーザ登録しておきます。

gitadmin（他に管理者権限のアカウントがあればそれでもOK）でログインし、ユーザを作成します。

「サイト管理」→「ユーザーアカウント」からユーザアカウントを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/544de703-5db0-4792-f94a-5912eee9d4c7.png)

ユーザ名を `remoter` として作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/df18c200-c0b3-d513-d65f-992b8ae4d7dd.png)

公開鍵登録のため、ログアウトして`remoter`でログインします。
「設定」→「SSHキーの追加」で先ほど作成した公開鍵（/home/remoter/.ssh/authorized_keys）を登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/7be4c00b-6cce-f784-0c9b-f8b7aadf7b4b.png)

こんな感じで登録されたら、ログアウトして`gitadmin`でログインしなおす。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/039b192f-d002-ae15-8e46-f269b4228f0b.png)


「サイト管理」→「ユーザーアカウント」から`remoter`を選択し、「サインイン無効」「管理者」にチェックをいれて更新する。
これでWebUIは利用できないが、管理権限を持つremoterがGitea上に作成された。
（「管理者」でない場合、リポジトリをプライベートで作成した場合に `remoter` からアクセスできず clone が失敗します）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/c737955a-964c-d52a-a195-d918a562b05d.png)


ユーザ作成と公開鍵登録は以下のようにコマンドでも実施できます。

```:ユーザ作成
sudo -u gitea ./gitea admin create-user \
 --username remoter \
 --password PASSWORD \
 --email remoter@localhost \
 --admin \
 --must-change-password=false \
 --access-token
```
こうすると、APIトークンが出力される。
これを利用してGiteaAPIで鍵を登録する。

```
curl -X POST "https://www.prosper2.net/gitea/api/v1/user/keys" \
    -H "accept: application/json" \
    -H "Authorization: token ユーザ作成で表示されたAPIトークンを入れる" \
    -H "Content-Type: application/json" -d \
       "{\"key\": \"`ssh-keygen -y -f /path/to/privkey.pem|tr -d '\n'`\", \"read_only\": true, \"title\": \"remoter pubkey\"}" -i
```



### JenkinsにSSH Agentプラグインを導入

テストを行うステージでは、`remoter`が開発サーバにログインしてから、ローカルのテストコマンドを発行しました。
デプロイのステージでは、`remoter`が開発サーバにログインしてから、Ansibleを叩きますが、その際に対象サーバにSSHログインが行われます。
このとき、 `remoter` の秘密鍵は開発サーバ上にないため、Jenkinsから開発サーバへログインしたときの鍵をそのまま流用する必要があります。
そのために、SSH Agentプラグインの導入が必要になります。

「Jenkinsの管理」→「プラグインの管理」からSSH Agentをインストールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/e6cb0953-6f1d-9d09-1270-63115428097b.png)


### Jenkinsfileの準備

テストを実行するJenkinsfileに、「それまでのステージのビルド結果がSUCCESSであれば、AnsibleのPlaybookを実行する」という処理をくわえます。

加えたあとのjenkinsfileは以下のとおり。

```groovy:/usr/share/nginx/html-dev/sample/Jenkinsfile
pipeline {

  agent { node { label 'node-web-dev' } }

  stages {

    // ホントにノードにログインできてるか確認しておく
    stage('Check environment') {
      steps {
        sh "cat /etc/redhat-release ; id ; hostname ; pwd ; ls"
      }
    }

    // PHPアプリのテストを行うステージ
    stage('Test PHP applications : PHPunit') {
      steps {

        // 変数に代入するのでscriptで処理させる必要がある
        script {

          // shをreturnStatus:trueにしてテスト結果を取得してRESULTに代入する
          RESULT = sh(returnStatus: true, script: 'cd test && phpunit --cache-result-file=. ./phptest.php')

          // 戻り値が０以外なら失敗とみなして強制的にエラーにさせるが、
          // ビルドは成功したとみなして次のステージへ進めるようにする。
          if ( RESULT ){
            catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
              error message: "TEST FAIL"
            }
          }
        }
      }
    }


    // Pythonアプリのテストを行うステージ
    stage('Test Python applications : unittest') {
      steps {
        script {
          RESULT = sh(returnStatus: true, script: 'cd test && python3 ./pythontest.py')
          if ( RESULT ){
            catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
              error message: "TEST FAIL"
            }
          }
        }
      }
    }


    // Perlアプリのテストを行うステージ
    stage('Test Perl applications : Test::More') {
      steps {
        script {
          RESULT = sh(returnStatus: true, script: 'cd test && prove ./perltest.t')
          if ( RESULT ){
            catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
              error message: "TEST FAIL"
            }
          }
        }
      }
    }


    // デプロイ用のPlaybookを実行
    stage('Deploy Application') {
      // ここまでのビルド結果がSUCCESS（＝テストをすべてPASS）になっていたら、このステージを実行します。
      when {
        expression {currentBuild.currentResult == 'SUCCESS'}
      }
      steps {
        // Jenkinsから開発サーバにログインしたときの鍵を流用するため
        // sshagent で remoter の クレデンシャルを指定しておきます。
        sshagent(['remoter']) {
          sh "cd deploy && ansible-playbook ./playbook_deploy-web.yml"
        }
      }
    }


  }

  // パイプライン処理終了後に行う処理
  post {
    // ビルドの成否にかかわらず、cleanWsでワークスペースを掃除する
    always {
      cleanWs()
    }
  }

}

```

開発しているディレクトリに戻って、先ほど作成したAnsibleのPlaybookと、修正したJenkinsfileをリポジトリへpushします。

```shell
cd /usr/share/nginx/html-dev/sample
git add -A
git commit -m "Ansible deploy : Jenkinsfile , deploy/"
git push origin master
```

さきほどの手順で、PHPアプリケーションをわざと間違えるようにしていたため、デプロイ部分がスキップされていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/21c6d951-84b4-e9a2-9c24-dc1dc84932e4.png)

通知もfailedになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/ea8c9732-ec70-1556-a52a-90fb56ed14e2.png)


PHPアプリケーションの間違い部分をもとにもどします。

```shell
sed -i 's/return 2/return 1/'  ./src/lib/math_modules.php
git add ./src/lib/math_modules.php
git commit -m "bug fix : math_modules.php"
git push origin master
```

テストがすべてPassし、デプロイされていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/df610cf4-156c-b7e5-3d3e-4eb029b3a92f.png)

通知も正常になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/334782/6b614fec-9775-d4cc-b37e-161a7600ad88.png)


公開サーバのディレクトリを確認すると、Jenkinsから実行したユーザである `remoter` で git clone できたように見えます。

```shell
$ ll /usr/share/nginx/html-pub/sample/
合計 8
-rw-rw-r-- 1 remoter remoter 2411  8月 15 18:59 Jenkinsfile
-rw-rw-r-- 1 remoter remoter   17  8月 15 18:59 README.md
drwxrwxr-x 2 remoter remoter   69  8月 15 18:59 deploy
drwxrwxr-x 3 remoter remoter   84  8月 15 18:59 src
drwxrwxr-x 2 remoter remoter   93  8月 15 18:59 test
```

ブラウザから http://web-pub.prosper2.net/sample/index.html を閲覧し、各スクリプトが動作することを確認します。

これでリポジトリへのpushを契機に、テストを実施、テストをパスしたらデプロイ、まで自動化できました。


# さいごに

GiteaのwebhookとJenkinsのGiteaプラグインの設定をチューニングしていけば、「公開サーバにデプロイするのは、masterブランチへのpushだけ」とか「stagingブランチにマージされた場合は、テスト後にステージングwebサーバにデプロイ」とか「stagingで一定期間が経過したら勝手にmasterへマージ＆デプロイ」とかできそうなのですが、私はお仕事でプログラムを書くひとではなく、趣味レベルの開発しか経験がないので、どんな使われ方をするのか、よくわかってないのが正直なところです。「テストコード」というものを書いたのも今回が初めての経験でして。。。

あと、`remoter` が個人に紐づかない無人ユーザなので、秘密鍵の保管ってどうすればいいんでしょか。権限とかシェルの制限とか。共有アカウントみたいな扱いだから、自由にシェルつかえる状態だと、不正に利用された場合に証跡がわからん。とか。なんかめんどくさそう。
`remoter` じゃなくて個人ユーザにしたほうがよかったのかな。でもJenkins上に秘密鍵置きたくないな。やっぱデプロイはトリガーで無人じゃなくて、有人でボタンぽちー、なのかな。じゃあテストのステージもおんなじじゃねーか。アプリケーションのトークンとかも似た話なんだよな。自動化と権限の話はやっぱめんどくさいや。とかとか。

いろいろ考えることがありそうですが、個人利用でローカル環境だし全然おっけーっしょ。でいろいろとすっ飛ばしてしまいました。すみません。


# 参考＆出典

http://nginx.org/en/docs/http/ngx_http_core_module.html#location
https://phpunit.de/supported-versions.html
https://phpunit.readthedocs.io/ja/latest/installation.html
https://www.jenkins.io/doc/book/pipeline/jenkinsfile/
https://www.jenkins.io/doc/pipeline/steps/ssh-agent/
https://plugins.jenkins.io/ssh-agent/
https://qiita.com/phorizon20/items/acb929772aaae4f52101
https://qiita.com/stoneBK7/items/fcc898f38ee161b38ef4