PCOVでコードカバレッジ取得を高速化

この記事はCakePHP Advent Calendar 2019の21日目の記事です

つい先日、ついにCakePHP 4.0がリリースされましたが、CakePHP 4.0で利用しているテスティングフレームワークはもちろんPHPUnitです。CakePHP3では、PHPUnit 6.0系を使っていましたが8.5.0に更新されています。

PHPUnitで、コードカバレッジを取得するにはXdebugを使うのが定番ですが、PHPUnit8系ではXdebug以外にPCOVを利用することができます。

PCOVは、今年(2019年)リリースされたばかりのコードカバレッジドライバーで、高速かつ省メモリで動作することが特徴です。

ということで、今回は実際にどれくらい高速化できるのかを簡単に調べてみました。

計測した環境

当初、CakePHP3で作ったサンプルアプリをCakePHP4化して試そうと思っていたのですが、いろいろな問題があり断念しCakePHP4本体のテストの一部(CakePHPのUnitTestすべてを実行するとかなり時間がかかるので、ORMのテストのみ)を利用して計測しました。

計測した環境は、macOS上のDocker Containerで、PHP公式で公開されているこちらをベースに若干拡張などを追加したものを利用しています。

それぞれのバージョンは以下のとおりです。

# php -v
PHP 7.3.10 (cli) (built: Oct 10 2019 21:12:52) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.10, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.3.10, Copyright (c) 1999-2018, by Zend Technologies
# pecl list 
Installed packages, channel pecl.php.net:
=========================================
Package Version State
apcu    5.1.17  stable
pcov    1.0.6   stable
redis   5.0.2   stable
xdebug  2.9.0   stable    

コードカバレッジなし

まずは、Xdebugを無効にしてコードカバレッジなしでの計測結果がこちら。

# ./vendor/bin/phpunit tests/TestCase/ORM/
PHPUnit 8.5.0 by Sebastian Bergmann and contributors.

.............................................................   61 / 1331 (  4%)
.............................................................  122 / 1331 (  9%)
...
..................................................            1331 / 1331 (100%)

Time: 7.04 seconds, Memory: 36.00 MB

OK, but incomplete, skipped, or risky tests!
Tests: 1331, Assertions: 3917, Skipped: 3, Incomplete: 2.

Xdebug

Xdebugでコードカバレッジを取得した場合がこちら。

# ./vendor/bin/phpunit tests/TestCase/ORM/
PHPUnit 8.5.0 by Sebastian Bergmann and contributors.

.............................................................   61 / 1331 (  4%)
.............................................................  122 / 1331 (  9%)
...
..................................................            1331 / 1331 (100%)

Time: 3.68 minutes, Memory: 126.00 MB

OK, but incomplete, skipped, or risky tests!
Tests: 1331, Assertions: 3917, Skipped: 3, Incomplete: 2.

Generating code coverage report in Clover XML format ... done [1.68 minutes]

Generating code coverage report in HTML format ... done [1.8 minutes]

やはりかなり遅いですね。約31倍です。

PCOV

PCOVでの計測結果がこちら。

# php -d pcov.enabled=1 ./vendor/bin/phpunit tests/TestCase/ORM/
PHPUnit 8.5.0 by Sebastian Bergmann and contributors.

.............................................................   61 / 1331 (  4%)
.............................................................  122 / 1331 (  9%)
...
..................................................            1331 / 1331 (100%)

Time: 24.55 seconds, Memory: 126.00 MB

OK, but incomplete, skipped, or risky tests!
Tests: 1331, Assertions: 3917, Skipped: 3, Incomplete: 2.

Generating code coverage report in Clover XML format ... done [10.48 seconds]

Generating code coverage report in HTML format ... done [46.87 seconds]

なんと、Xdebugを使った場合と比べると約10分の1の時間で完了しています!カバレッジを計測していない場合と比べると、約3.5倍。Xdebugと比べると許容範囲かなと思います。しかも、メモリ使用量もXdebugと同じという結果でした

まとめ

まとめたものがこちら

実行時間 使用メモリ
カバレッジなし 7.04 s 36.00 MB
Xdebug 220.80 s 126.00 MB
PCOV 24.55 s 126.00 MB

実際のコードで実行時間に差は出るとは思いますが、かなりの高速化が期待できそうです。

CakePHP4へのアップデートはプラグインやライブラリの対応など、まだ色々とハードルはありますが、PHPUnitのバージョンアップはかなり魅力的だと思っています。PHPUnit自体の変更については全く追えていないので今後キャッチアップしていきたいと思っています。

参考

PHPでSAS tokenを使ってAzure Blob Storageにファイルをアップロードする

現在開発中の案件で、Shared Access Signature(SAS)を使ってBlobにデータを上げる必要があって、若干ハマったのでメモ。

先日プレビュー版がリリースされた user delegation SASってのもありますが、今回はストレージアカウントのキーを使う感じで。user delegation SASに関しては、亀淵さんのブログが参考になると思います。

今回使用するバックエンドはPHPなので、SASの作成には、microsoft/azure-storage-blobを使うことにします。

ざっとコード書くてみたのがこんな感じ...

$accountName = getenv('STORAGE_ACCOUNT_NAME');
$accountKey = getenv('STORAGE_ACCOUNT_KEY');
$containerName = getenv('CONTAINER_NAME');

$helper = new BlobSharedAccessSignatureHelper($accountName, $accountKey);
$sas = $helper->generateBlobServiceSharedAccessSignatureToken(
    Resources::RESOURCE_TYPE_BLOB,
    "{$containerName}/composer.json",
    'w',
    Chronos::now(new \DateTimeZone('UTC'))->addSeconds(60)->format(DATE_ATOM),
    Chronos::now(new \DateTimeZone('UTC'))->subSeconds(10)->format(DATE_ATOM),
    '', // リクエスト元のIPアドレス
    'https'
);

echo "sas: {$sas}\n";

これで試すと、 403 Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature. こんなエラーがでてしまいます。

散々悩んだ挙げ句、ブラウザーで JavaScript と HTML を使用して BLOB をアップロード、一覧表示、および削除するを実際に動かして、リクエスト内容などを確認した結果ようやく原因がわかりました。

PHPの日付フォーマット DATE_ISO8601 がiso 8601準拠ではないので、 DATE_ATOM を使っていたのですが、TimeZoneの書式が違うことが原因でした。

$ cat date.php
<?php
$date = new \DateTime('now', new \DateTimeZone('UTC'));

echo $date->format(DATE_ISO8601)."\n";
echo $date->format(DATE_ATOM)."\n";
echo $date->format('Y-m-d\TH:i:s\Z')."\n";

$ php date.php
2019-11-01T01:46:38+0000
2019-11-01T01:46:38+00:00
2019-11-01T01:46:38Z     // -> この形式でないとだめ

'Y-m-d\TH:i:s\Z' を使うことで正常にアップロードできました。めでたしめでたし。

最終的にできあがったサンプルがこちら

<?php
require './vendor/autoload.php';

use Cake\Chronos\Chronos;
use GuzzleHttp\Client;
use MicrosoftAzure\Storage\Blob\BlobSharedAccessSignatureHelper;
use MicrosoftAzure\Storage\Blob\Internal\BlobResources;
use MicrosoftAzure\Storage\Common\Internal\Resources;

$accountName = getenv('STORAGE_ACCOUNT_NAME');
$accountKey = getenv('STORAGE_ACCOUNT_KEY');
$containerName = getenv('CONTAINER_NAME');

$helper = new BlobSharedAccessSignatureHelper($accountName, $accountKey);
$sas = $helper->generateBlobServiceSharedAccessSignatureToken(
    Resources::RESOURCE_TYPE_BLOB,
    "{$containerName}/composer.json",
    'w',
    Chronos::now(new \DateTimeZone('UTC'))->addSeconds(60)->format('Y-m-d\TH:i:s\Z'),
    Chronos::now(new \DateTimeZone('UTC'))->subSeconds(10)->format('Y-m-d\TH:i:s\Z'),
    '', // リクエスト元のIPアドレス
    'https'
);

echo "sas: {$sas}\n";

try {
    $client = new Client(['base_uri' => sprintf('https://%s.%s/%s/', $accountName, Resources::BLOB_BASE_DNS_NAME, $containerName)]);

    $response = $client->put('composer.json', [
        'body' => file_get_contents('./composer.json'),
        'headers' => [
            'x-ms-version' => BlobResources::STORAGE_API_LATEST_VERSION,
            'x-ms-blob-type' => 'BlockBlob',
        ],
        'query' => $sas,
    ]);

    echo sprintf("StatusCode: %d\n", $response->getStatusCode());
} catch(\Exception $e) {
    echo sprintf("StatusCode: %d\n", $e->getCode());
    echo $e->getMessage() . "\n";
}

サンプルコードは、こちらにもあげてあります。

Azure DevOpsのself hosted agentを試してみた

暑いので快適な仕事部屋で、CodeCoverageと戦っているわたなべです...

self hosted agent試すことになった理由

現在開発中の案件では、Azure DevOpsを利用して、CI/CD環境を構築しています。バックエンドはPHPで開発していて、UnitTestを結構しっかり書いています。

開発作業に追われてなかなか手をつけられていなかった Code Coverage を見ようと設定をしたところ、以下のようなエラーが発生してしまいました。

f:id:kaz_29:20190804114332j:plain

表示されているリンクの内容を確認すると...

For 60 minutes on Microsoft-hosted agents with a private project or private repository

ということで、Microsoft-hosted agentsでプライベートリポジトリのjobを実行する場合は、60分 の実行時間制限がある様子。

Web系のアプリのCI/CDであれば、実行に60分もかかることはほとんどないと思いますが、今回はCode Coverageを収集するために Xdebug を有効にしてテストを実施しているため通常より実行にかなりの時間がかかることが原因でした。(通常のCI/CDのpipelineではCode Coverageは取得していません)

phpdbgを利用すると速度はかなり改善するのですが、こちらはメモリの非常に大量に消費します。

また、今年公開されたばかりのPCOVを使用すると実行速度、メモリ使用量共に劇的に改善できそうなのですが、PHPUnit8でないと対応していないため、今回使用しているCakePHPのアプリには現状適用できません。

ということで、 Xdebug を使う方式でなんとか CodeCoverageを取るしかなさそうです...。

そこで、今まで使ったことのなかった、self hosted agentを試すことにしました。

agentの種類

Azure DevOpsのagentには以下の2つの提供方式があります。

Microsoft-hosted agents

Microsoft-hosted agentsは、MSさんが提供しているagentで以下のような種類があります。

Microsoft-hosted agentsの制限事項は以下の点。

  • パブリックプロジェクト且つ、パブリックリポジトリのjobは、360分(6時間)まで
  • プライベートプロジェクトまたは、プライベートリポジトリのjobは60分まで。

Self-hosted agents

自前のVMなどに、agentをインストールしてAzure DevOpsから利用することが出来ます。Azure DevOpsにはSelf-hosted agents 1つであれば無料で利用することができるので、VMの利用料のみで使用可能です。また、Docker上での実行もサポートされているので、k8s上で実行することもできそうです。

Self-hosted agentsの場合は、実行時間の制限はありません。

self hosted agentの設定

今回は実験ということで、VMを1台立ててagentをインストールすることにします。

dockerのインストール

今回のjobではdocker-composeを使用するのでまずは、dockerをインストールします。

$ sudo apt install -y docker docker-compose
$ sudo usermod -g docker user

agent用ディレクトリを作成

実験で使用するjobの設定がhosted agentのディレクトリ構成に依存しているので以下のディレクトリを作成しました。依存しないように直したほうが良いですね...

$ cd /home
$ sudo mkdir vsts
$ sudo chown user.user vsts

PAT(Private access token)を生成

ここを参考に、利用するdevops organizationのPATを作成する。

作成したPATは、agentインストール時に使用します。

agentのインストール

  • Azure DevOpsの Organization SettingsからAgent Poolを選択します。
  • Default poolを選択して、 New Agent ボタンを押すと以下のようなインストール手順の解説が表示されます。

f:id:kaz_29:20190803190723j:plain

ここで取得できる、agentのダウンロードURLを使用して、指示通りに設定を進めます。

$ mkdir agent
$ wget https://vstsagentpackage.azureedge.net/agent/2.155.1/vsts-agent-osx-x64-2.155.1.tar.gz
$ cd agent
$ ./config.sh
>> End User License Agreements:

Building sources from a TFVC repository requires accepting the Team Explorer Everywhere End User License Agreement. This step is not required for building sources from Git repositories.

A copy of the Team Explorer Everywhere license agreement can be found at:
  /home/kaz/agent/externals/tee/license.html

Enter (Y/N) Accept the Team Explorer Everywhere license agreement now? (press enter for N) > Y

>> Connect:

Enter server URL > https://dev.azure.com/[your organization]/
Enter authentication type (press enter for PAT) > PAT
Enter personal access token > [your personal access token]
Connecting to server ...

>> Register Agent:

Enter agent pool (press enter for default) > 
Enter agent name (press enter for devopsagent) > 
Scanning for tool capabilities.
Connecting to the server.
Successfully added the agent
Testing agent connection.
Enter work folder (press enter for _work) > /home/vsts/work
2019-08-03 01:55:47Z: Settings Saved.

agentの実行

以下のコマンドで、agentをserviceとして登録できます。

$ cd ~/agent
$ sudo ./svc.sh install

## Start
$ sudo ./svc.sh start

## Stop
$ sudo ./svc.sh stop

## Status
$ sudo ./svc.sh status

Pipelineの設定

agentが正常に起動すると、インストール時に指定したorganizationのagent poolに表示されます。

f:id:kaz_29:20190804114447j:plain

agent が登録されたら、あとは既存のbuild pipelineをコピーするなどして、以下のように使用するagent poolを指定すれば完了です。

f:id:kaz_29:20190804113101j:plain

まとめ

self hosted agentは以外に簡単に導入できました。

Docker上での実行もサポートされているようなので、今回のプロジェクトではステージング環境のAKSクラスタ上でカバレッジ用のagentを実行できないか検討しているところです。

CIが成長してくると、ビルド時間との戦いになることはよくあることではないかと思います。 テスト、ビルド手順の見直しも必要ですが、CI環境自体をパワーアップして改善する必要があることもあります。 その際の手法として、self hosted agentはかなり有効だと思います。

参考URL

PHPでも簡単 Azure Application Insights

先月末に開催された、de:code 2019でMVP パーソナル スポンサーとしてPHP用のライブラリを公開しました。

Azure Application Insights

docs.microsoft.com

Application Insights は、上記のページにも書かれている通り、複数のプラットフォームで使用できる Web 開発者向けの拡張可能なアプリケーション パフォーマンス管理 (APM) サービスです。

.NETやNode.jsなどで利用する場合は、とても簡単に様々な機能を利用できるのですが、PHP用のSDKはシンプルなライブラリなので、アプリケーションに組み込むにはある程度自前での実装が必要になります。

そこで、現在、製作中の案件などで利用するために実装を進めていたものをde:code 2019での公開に向けて、公開可能な形にまとめ直したのが、Phaiです。

Phai - Application Insights PHP middleware

github.com

Phaiは、PSR-15準拠のミドルウェアです。現在、PSR-15を標準でサポートしているフレームワークZend Expressive位であまり多くはありませんが、今後サポートする方向に進むのではないかと思います。また、各フレームワークでPSR-15をサポートするライブラリが公開されているため、問題なく利用することができます。

下記で紹介している、PSR-15 Middleware support for CakePHP は今回Phaiを開発するにあたって、PSR-15 Middleware support for Slim Framework v3を参考に私が開発したものです。

PSR-15対応ライブラリ

他にもありそうですが普段使っていないフレームワークに関しては探しきれていません...。他に何かあれば教えて下さいm(__)m

使用方法

Phaiには、Slim3とCakePHP3用のサンプルが同梱されていますが、以下に簡単な使い方を解説します。

Application Insightsの作成

Azure PortalからApplication Insightsの作成を選択し、利用するサブスクリプションやリソースグループ、リソース名、配置するリージョンなどを選択し Next を押して次に進みます。

f:id:kaz_29:20190616164220j:plain

次の画面ではタグを指定できますが、今回は特になにも設定せずに Next を押して次に進みます。

f:id:kaz_29:20190616172324j:plain

問題がなければ、 Createを押してApplication Insightsを作成します。

f:id:kaz_29:20190616172342j:plain

しばらく待つとApplication Insightsの作成が完了し、以下のような画面を見ることができます。 概要(Overview)画面に表示されている、 Instrumentation Key は後ほど使用しますのでメモしておいてください。

f:id:kaz_29:20190616164302j:plain

Phaiのサンプルを実行

以下の手順でPhaiをcloneして、CakePHPのサンプルの設定をします。

$ git clone git@github.com:kaz29/phai.git
$ cd phai/example/cakephp
$ composer install 
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Package operations: 86 installs, 0 updates, 0 removals
  - Installing cakephp/plugin-installer (1.1.0): Loading from cache
  - Installing aura/intl (3.0.0): Loading from cache

....

Generating autoload files
> Cake\Composer\Installer\PluginInstaller::postAutoloadDump
> App\Console\Installer::postInstall
Created `config/app.php` file
Created `/Users/kaz/dev/labo/phai/examples/cakephp/logs` directory
Created `/Users/kaz/dev/labo/phai/examples/cakephp/tmp` directory
Created `/Users/kaz/dev/labo/phai/examples/cakephp/tmp/cache` directory
Created `/Users/kaz/dev/labo/phai/examples/cakephp/tmp/cache/models` directory
Created `/Users/kaz/dev/labo/phai/examples/cakephp/tmp/cache/persistent` directory
Created `/Users/kaz/dev/labo/phai/examples/cakephp/tmp/cache/views` directory
Created `/Users/kaz/dev/labo/phai/examples/cakephp/tmp/sessions` directory
Created `/Users/kaz/dev/labo/phai/examples/cakephp/tmp/tests` directory
Set Folder Permissions ? (Default to Y) [Y,n]? y
Permissions set on /Users/kaz/dev/labo/phai/examples/cakephp/tmp/cache
Permissions set on /Users/kaz/dev/labo/phai/examples/cakephp/tmp/cache/models
Permissions set on /Users/kaz/dev/labo/phai/examples/cakephp/tmp/cache/persistent
Permissions set on /Users/kaz/dev/labo/phai/examples/cakephp/tmp/cache/views
Permissions set on /Users/kaz/dev/labo/phai/examples/cakephp/tmp/sessions
Permissions set on /Users/kaz/dev/labo/phai/examples/cakephp/tmp/tests
Permissions set on /Users/kaz/dev/labo/phai/examples/cakephp/tmp
Permissions set on /Users/kaz/dev/labo/phai/examples/cakephp/logs
Updated Security.salt value in config/app.php

以下のように、先程作成したApplication InsightsのInstrumentation Key を指定してサンプルアプリを実行します。 Your application insights "Instrumentation Key"の部分にご自身の環境のInstrumentation Keyを指定してください。

$ env APPLICATION_INSIGHTS_KEY='Your application insights "Instrumentation Key"' ./bin/cake server 

Welcome to CakePHP v3.7.7 Console
---------------------------------------------------------------
App : src
Path: /Users/kaz/dev/labo/phai/examples/cakephp/src/
DocumentRoot: /Users/kaz/dev/labo/phai/examples/cakephp/webroot
Ini Path:
---------------------------------------------------------------
built-in server is running in http://localhost:8765/
You can exit with `CTRL-C`

正常に実行できたら、表示されているURL(http://localhost:8765/)にアクセスすると、テレメトリ情報がApplication Insightsに送信されます。

Application Insightsでのログの確認

Application Insightsの Search メニューを選択し、Click hereをクリックします。

f:id:kaz_29:20190616170214j:plain

以下のように、アクセスログが、レスポンスタイム付きで表示されます。 ポータル上に反映されるのに数分かかりますので、少しお時間をおいてから確認してください。

f:id:kaz_29:20190616172453j:plain

ここで、http://localhost:8765/pages/hoge のような存在しないページにアクセスしてみます。 すると下記のような例外のログがApplication Insightsに送信されてエラーの内容を確認することができます。

f:id:kaz_29:20190616172512j:plain

ログレベルの設定

Phaiに含まれる、標準のTelemetry_Clientをラップした、\kaz29\Phai\ApplicationInsights\Telemetry_Clientを使用し、以下のように指定することで、指定したログレベル以下のものはApplication Insightsへの転送は行われません。

環境変数などを利用して、実行環境ごとに、ログレベルを設定したり一時的にデバッグログを有効にするなどの切り替えが簡単にできるようになっています。

$client = new \kaz29\Phai\ApplicationInsights\Telemetry_Client(
    null,
    null,
    \ApplicationInsights\Channel\Contracts\Severity_Level::Warning
);

サンプルコードの簡単な解説

config/bootstrap.phpの末尾で、環境変数をもとに設定情報を保存しています。

/**
 * Initialize Phai
 */
$client = new \ApplicationInsights\Telemetry_Client();
\kaz29\Phai\Phai::initialize($client, env('APPLICATION_INSIGHTS_KEY'));
Configure::write('Phai.client', $client);

src/Application.php内で以下のように、Phaiを登録しています。

    public function middleware($middlewareQueue)
    {
        $middlewareQueue
            // Catch any exceptions in the lower layers,
            // and make an error page/response
            ->add(new ErrorHandlerMiddleware(null, Configure::read('Error')))
            // ** ここで Phaiを指定している
            ->add(new PsrMiddleware(new ApplicationInsightsMiddleware(Configure::read('Phai.client'))))
            // Handle plugin/theme assets like CakePHP normally does.
            ->add(new AssetMiddleware([
                'cacheTime' => Configure::read('Asset.cacheTime')
            ]))
            // Add routing middleware.
            // Routes collection cache enabled by default, to disable route caching
            // pass null as cacheConfig, example: `new RoutingMiddleware($this)`
            // you might want to disable this cache in case your routing is extremely simple
            ->add(new RoutingMiddleware($this, '_cake_routes_'));
        return $middlewareQueue;
    }

まとめ

いかがでしたか?とても簡単にApplication Insightsを導入できることがご理解いただけたのではないかと思います。 現状は、まだ以下のような基本的な機能のみしか利用できませんが、今後も開発を続けてもっと便利にApplication Insightsを利用できるようにしていく予定です。

  • リクエストログ
  • 例外ログ
  • PSR-3 Logger Interfaceに準拠したロガー

Azure Kubernetes Service (AKS)の構築とAzure DevOpsを使用したCI/CD pipelineの作成

昨年11/9,10に開催された寺田さん(https://twitter.com/yoshioterada)によるMVP受賞者向けAKSハッカソンに参加したご縁で、1/21-25にAKS/Azure DevOpsのワークショップを開催していただきました。

ここでは、AKSクラスタ構築の概要とAKS、Azure DevOpsを利用したCI/CD pipeline構築の一例を紹介したいと思います。なお、Kubernetes,AKS,Azure DevOps全て日々成長を続けていますのでここで紹介する内容は執筆時点での情報なのでご注意下さい。

Azure Kubernetes Service (AKS)

Azure Kubernetes Service (以下、AKS)は、フルマネージドなKubernetesクラスタを提供するサービスです。Kunernetes全体を管理するクラスター マスター(俗にいう管理ノード)は、マネージド Azure リソースとして提供され、このノードには課金されず、Kubernetesクラスタをう構成する各ノード(実態はAzure Vrtual Machine (VM)です)のサイズ、ノード数により課金されます。

azure.microsoft.com

Container Registoryの準備

AKSの構築に先立って、Azure Container Registoryに、アプリのコンテナイメージを用意した状態で作業を開始しました。

AKSの構築

今回のターゲットになるアプリは、Azure Database for PostgreSQLと、Azure Redis Cacheを使用します。Azure Database for PostgreSQLは、どのサイズでもVNetに対応していますが、Redis CacheはプレミアムプランでないとVNet対応機能が使えません。プレミアムプランは今回のアプリ規模にはマッチしないということで、VNetへの配置は諦め、スタンダードプランを利用し、IPアドレスで制限する形にすることにしました。

実際の構築はステージング環境を想定して、MSの寺田さんが公開されている下記を雛形にAzure CLIを使って順次構築していきました。

github.com

構成図

今回は、初期開発ということもあり、配置するアプリケーションもシンプルな構成なので、クラスターも極力シンプルな構成を取る方針で下記のような構成をとりました。

f:id:kaz_29:20190127161049j:plain
構成図

デプロイ

admin

adminに関しては、管理者のみが使用する機能なので、シンプルに特定のブランチが更新されると、Azure DevOpsでビルドが始まり自動でリリースされる方式を取りました。

api

apiに関しては、B/Gデプロイメントに近い以下のような形を取りました。

  • 特定のブランチの更新をトリガーにビルド開始
  • UnitTestを実行
  • ビルド番号をタグとして利用し、ACRにコンテナイメージをプッシュ
  • ビルド番号をコンテナのタグ、セレクタとして指定し、green serviceにapply
  • 担当者にリリース承認メールを送信
  • green側で動作確認後に、担当者がリリース
  • blue serviceのセレクタを新しいビルド番号に更新しapply
  • 問題がなければ古いビルド番号のdeploymentを手動で削除

最後の古いデプロイメントの削除に関しては、ビルド番号がエラーなどで飛んでしまうなど自動化するためにはいくつかハードルがあるため、現状は手動で削除する形をとっています。

Azure DevOps側の詳細については後述します。

今回のワークショップでは、モブプログラミングの様なフォーマットで、定期的にドライバを交代しながら作業を進めました、そのおかげで参加した全員が実際の構築を体感できたので、後半では活発にアドバイスすや意見が出て全員の理解度が上がっていることを体感できました。この形式は今後会社の業務でも取り入れていきたいと思います。

最終的にクラスタの構築は、弊社のイケメンエンジニアたちがTerraformで自動構築できるようにしてくれて、いつでもクラスタを再現できるようになりました。AKSクラスタの構築に関する詳細は近いうちにブログにまとめられるはずなので、公開されたら追記します。

Azure DevOps

Azure DevOpsのオーガニゼーションを作成する

まだ、Azure DevOpsのオーガニゼーションがない場合は、https://dev.azure.comにアクセスし作成します。

dev.azure.com

雛形のDevOps Psojectを作成

今回の扱うアプリはPHP(CakePHP)を使っていますが、現状PHPのテンプレートのデプロイターゲットにはAKSが含まれていません。

今回は初のAzure DevOps/AKSの利用ということで、AKSへのデプロイ方法の参考にするために、Java/SpringのテンプレートをつかってAKSへデプロイするプロジェクトを作成し、それをベースにカスタマイズして行く方法をとりました。

まず、雛形となるテンプレートとしてJavaを選択します。

f:id:kaz_29:20190127162442j:plain

フレームワークは何でもいいのでとりあえずSpringを選択します。

f:id:kaz_29:20190127162510j:plain

デプロイ先はAKSを選択します。

f:id:kaz_29:20190127182454j:plain

Azure DevOpsのプロジェクト名、オーガニゼーションを選択し、AKSクラスタはすでに構築済みなので、Use Existingを選択して、デプロイ先のAKSクラスタを指定します。

f:id:kaz_29:20190127162251j:plain

Doneをクリックすると構築が始まるのでしばらく待ちます。いろいろとデプロイが進みAzure Portalに以下のような画面が現れます。

f:id:kaz_29:20190127162100j:plain

これで実際にAzure DevOpsから、自動デプロイできる環境ができますので、内容を確認して各自の環境に合わせてカスタマイズしていくと良いでしょう。 もともと作成されるpipelineを直接直すのではなく、pipelineを新規に作成、サンプルを参考に構築していくのが良いと思います。

DeployのTIPS

秘匿情報の管理

今回のアプリは、JWKトークンを利用するため、ビルド時に証明書のアクセス権などを設定する必要がありますが、証明書をリポジトリに含めることは避けるべきです。当初、KayValutを使用しようと考えていましたが、Azure DevOpsにはSecure filesという機能があることを発見したのでこの機能を利用してビルド時に証明書を取得することにしました。

Pipelines -> Library に Secure filesタブがあるのでここで証明書ファイルをアップロードします。

f:id:kaz_29:20190127184907j:plain

アップロードしたファイルは、ビルドpipeline内で利用できる Download Secure File タスクを使用してダウンロードすることができます。タスクを実行すると、$(Agent.TempDirectory)以下にダウンロードされますので、Command Lineタスクなどを使用して、所定の場所に移動するなどして利用可能です。

f:id:kaz_29:20190127161900j:plain

共通処理をグルーピング

複数のpiplineで共通の処理をtask groupとしてまとめて再利用することができます。

下記のように、まとめたいtaskを選択して、右クリックして Create task group を選択すると作成することができます。

f:id:kaz_29:20190127183055j:plain

Manage task groupを選択すると、Task groupの内容を編集することができます。

f:id:kaz_29:20190127183124j:plain

deplyment.yamlの更新

テンプレートでは、hemlを利用してデプロイを実施しているのですが、今回はビルド時にdeployment.yamlのタグやバージョンを更新してapployする方式をとっています。

下記のようにsedコマンドを使用して、テンプレート内のVERSIONという文字列を、実行中のビルド番号に置換して実現しています。

sed -i -e "s|VERSION|$(Build.BuildId)|g" /home/vsts/work/1/s/kubernetes/deployment.yaml

deployment.yaml抜粋

apiVersion: apps/v1
kind: Deployment
metadata:
  name: appname-VERSION
spec:
  replicas: 3
  selector:
    matchLabels:
      app: appname
...
  template:
    metadata:
      labels:
        app: appname
        version: vVERSION
        stage: staging
    spec:
      securityContext:
        runAsUser: 33
      imagePullSecrets:
        - name: docker-reg-credential
      containers:
      - name: appname
        image: appnameacr.azurecr.io/appname-api:VERSION
....

service.yaml

apiVersion: v1
kind: Service
metadata:
  labels:
    app: appname
  name: green-service
spec:
  ports:
  - port: 80
    name: http
    targetPort: 8080
  selector:
    app: appnemr
    version: vVERSION
  sessionAffinity: None
  type: ClusterIP

metadataの値は文字列である必要があるため、ビルド番号ではなく v10 の様にプレフィックスをつけています。

変更したyamlファイルをrelease pipelineに引き渡す

前の手順でsedで変更したファイルを、release pipelineで利用するために、Artifactディレクトリにコピーする必要があります。

Copy file taskを利用して、必要なファイルを$(build.artifactstagingdirectory) にコピーすることで、後続のpipelineに受け渡すことができます。

f:id:kaz_29:20190127183214j:plain

Build pipeline

で最終的に、build pipelineはこんな形になりました。 Build jobの前に、UnitTest用のJobを実行し、正常に終了したらbuildに進むように設定しています。

証明書ファイルの展開・コピーと、yamlファイルの更新は、Task group化しています。

f:id:kaz_29:20190127162346j:plain

Release pipeline

Release pipelineはこんな感じになりました。

f:id:kaz_29:20190127183314j:plain

Apply to greenApply to blueは以下のように、Post-deployment approvalsを有効にして、担当者の承認後に進むように設定しました。

f:id:kaz_29:20190127183352j:plain

Apply to green では、deploymment.yamlとgreen_service.yamlDeploy to Kubenetes taskを指定してapplyしています。

f:id:kaz_29:20190127183418j:plain

Apply to blueでは、blue_service.yamlをapplyしています。

f:id:kaz_29:20190127183440j:plain

まとめ

ここまでの環境を設定するのは、かなり大変ですし、いろいろとハマったところもあるので、ワークショップで寺田さんにサポートしてもらえなければ挫折していたかもしれません。本当にありがとうございました。

今まで様々な環境を利用してCI/CD環境を構築/運用してきましたが、その中でもAzure DevOpsは、VSTSで蓄積されたノウハウが詰まっていて凄く良いと感じました。もちろんOSSを使ったアプリでも問題なく使えます。このプロジェクトを通して使い倒してノウハウが溜まったらまたいつか紹介できるようにしたいと思います。

また、Kubernetesを利用する場合でも、環境や思想はそれぞれ異なるので、すべての環境が今回の形で対応できるわけではないでですが、構築を検討している方の参考になると嬉しいです。

実際のアプリの開発は、これからが本番なので気合を入れて開発を進めていきたいと思います。

参考URL

Azure DevOpsを使って、PHPコンテナをWeb App for Containersに簡単デプロイ

何度もつぶやいたことがある気がするけど、予定が詰まっているときに限って仕事と関係ないコードを書いてしまい、結局ブログまで書き始めてしまった渡辺です。

2019-03-03追記

記事執筆時点ではDeploy to slotが正常に動作しなかったのですが、Azure App Service Deploy タスクの4.*がリリースされていたのでこちらを使うように変更したところ、正常にデプロイできました!

Azure DevOps

azure.microsoft.com

CI/CDを実現するためのサービスで、以下のような機能が含まれています。

  • Azure Boards - かんばんボード、バックログダッシュボードなど...
  • Azure Pipelines - CI/CDを実行するサービス
  • Azure Repos - Git Repositoryサービス
  • Azure Test Plans - 探索的テスト ツール(?)
  • Azure Artifacts - パッケージ管理ツール(?)

今回は、Azure Test Plans/Azure Artifactsは試していないのでよくわかってないです...。

Azure DevOps Projectの作成

Azure DevOps Projectの作成を開始すると下記のように、ウィザード形式で設定が進みます。

f:id:kaz_29:20190113170315j:plain
Runtimeの選択

  • 次にFrameworkの選択です。Simple PHP/Larabelが選択できますが、今回はSimple PHPを選択します。

f:id:kaz_29:20190113170551j:plain
Frameworkの選択

  • 次にDploy先のサービスを選択します。今回はDocker Containerを使用しますので、Web App for Containersを選択します。

f:id:kaz_29:20190113170727j:plain
Serviceの選択

  • 最後に、プロジェクトの設定をします。

f:id:kaz_29:20190113171043j:plain
プロジェクトの設定

  • Additional settings をクリックすると、Containerや、Container Registryの細かな設定ができます。

f:id:kaz_29:20190113171639j:plain
Additional settings

設定が完了すると、いろいろとデプロイが進みAzure Portalに以下のような画面が現れます。

f:id:kaz_29:20190113172801j:plain
DevOps Projects

画面右上の、エンドポイントURLをクリックするとdeployされたサイトを見ることができます。

f:id:kaz_29:20190113173041j:plain
作成直後のWebサイト

ここまでで、10数分くらいですかね。これで、リポジトリからPipeline経由でデプロイされる環境ができちゃいます。簡単ですね。

Simple PHPの中身を覗いてみる

sshでcloneするには、Azure Reposの右上にある Clone をクリックすると出てくる画面で、Manage SSH keys に遷移すると公開鍵を登録することができます。

f:id:kaz_29:20190113181341j:plain
Azure Repos

Simple PHPをdeployすると初期状態では、リポジトリには下記のようなシンプルなPHPアプリが生成されています。

$ tree -L 2 .
.
├── Application
│   ├── Dockerfile
│   ├── LICENSE
│   ├── Readme.md
│   ├── composer.json
│   ├── css
│   ├── fonts
│   ├── img
│   └── index.php
└── ArmTemplates
 ├── container-webapp-template.json
 └── containerRegistry-template.json

ちょっと中身を覗いてみましょう。

Application/composer.json

application-insightsのパッケージだけが含まれている、シンプルなアプリ

{
    "require": {
        "microsoft/application-insights": "*"
    }
}

Application/Dockerfile

PHPコミュニティ公式の 7.0.6-apache をベースにしているようです。 いくつかのaptパッケージ、php拡張、composerコマンドをインストールして、composer installしています。

FROM php:7.0.6-apache
LABEL maintainer="Azure App Service Container Images <appsvc-images@microsoft.com>"
RUN apt-get update -y && apt-get install -y openssl zip unzip git
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
RUN docker-php-ext-install pdo mbstring

COPY . /var/www/html/
RUN composer install

Application/index.php

autoloadして、application-insightsにリクエストログ投げているだけのほぼ静的なページです。

<?php
    require_once 'vendor/autoload.php';
    $app_insights_instrumentation = getenv('APPINSIGHTS_INSTRUMENTATIONKEY');
    $telemetryClient = new \ApplicationInsights\Telemetry_Client();
    $telemetryClient->getContext()->setInstrumentationKey($app_insights_instrumentation);
    $telemetryClient->trackRequest('Server Requests','Azure DevOps Project', time());
    $telemetryClient->flush();
?>

<!DOCTYPE html>
<html lang="en">

...

ArmTemplates/

ArmTemplates以下には、Container RegistryとWeb Appのプロビジョニングに使用するarm templateが保存されています。

要は、80でWebサーバさえ上がっていればOKそうです。~/Application/ って配置がちょっと気になりますが...。

Azure DevOps内の流れ

初期状態では、Pipeline内にはBuilds/Releasesの2つにpipelineが設定されています。

Build

Buildには以下のようなpipelineが設定されています。

  • materブランチの更新をトリガーに実行される
  • Container Registoryを作成する
  • コンテナをbuild
  • コンテナをContainer Registryにpush

f:id:kaz_29:20190113181533j:plain
Pipelines/Build

Releases

Releasesには以下のようなpipelineが設定されています。

  • Builds完了をトリガーに実行される
  • Web App for Containerを作成する
  • コンテナをdeployする

f:id:kaz_29:20190113181619j:plain
Pipelines/Releases

デプロイのオプションには、slotにデプロイさせる設定などがありますね。

まとめ

こんな感じで、単純なWebアプリをデプロイするpipelineがすごく簡単に構築できます。 他のサービスの構築やPipeline内でテスト、静的解析を実行したり、B/G deploymentをするなど、普段別サービスを使ってやっていることを置き換えていろいろ試してみようと思います。

おまけ

slotを使って、B/G deploymentをしようとしたのですが、staging slotにdeployする設定にしたのにproduction slotにもデプロイされてしまう謎挙動が発生して解決できていません (;_; 2019-03-03修正

Git submoduleを含むサービスをAWS CodePipelineで扱う

新しい職場もあっという間に半年経ちますが、おかげさまで元気で毎日忙しい日々を過ごしているAWS初心者のわたなべです。

AWS CodePipeline

現在いくつかの案件を並行して進めているのですが、その中で知人が在籍している某スタートアップのインフラ改善支援で利用しているサービスがAWS CodePipelineです。

aws.amazon.com

CodePipelineは、Jenkinsやcircleciのように、CI/CDを実現するためのサービスで、AWS謹製ということでAWSの他のサービスと連携しやすいのが特徴かと思います。

B/G デプロイメント

今回の要望として、ECS Fargeteを使用してBlue/Greenデプロイメントを実現するということで、aws-examplesにある以下のサンプルを参考にしています。

github.com

このリポジトリfargate ブランチがFargate上でB/Gデプロイメントを実現するサンプルで、ざっくり以下のような流れでB/Gデプロイメントを実現しています。

  • Githubの更新
  • コンテナのビルド
  • ECRにPush
  • Deploy先の検出
  • Fargateにデプロイ - Green側のTaskを更新
  • 承認
  • FargateタスクのSwitch

今回の背景

今回対象になるサービスは、以下のような形でいくつかのAPIをGit submoduleとして構成されています。

  • example-center

全体を管理する、example-centerの更新をトリガーに配下のAPI群をBuild => Deployしたいのですが、AWS CodePipelineではgit submoduleはサポートされていません。

パイプラインのエラー: GitHub ステージ自分のソースは Git サブモジュールが含まれているが、AWS CodePipeline 初期化されていません

上記FAQに、 可能な変更: 別スクリプトの一部として直接 GitHub リポジトリをクローンすることを検討してください。 と書かれているので実際に試してみました。

今回やってみたこと

git submodule updateで行けるだろうとおもっていたのですが、Buildコンテナには .git ディレクトリが含まれていないので無理でした。仕方がないので pre_build フェーズで以下のような形でsubmoduleを一つづつcloneすることにしました

                - git clone "https://$GITHUB_TOKEN@github.com/bar/foo-api.git"

実際のビルド処理、pushする処理及びartifactとして次の処理に渡すbuild.jsonは、以下のようなスクリプトで生成するようにしてみました。このスクリプトでは、変更されていないsubmoduleのビルドを省くために、コンテナのタグにgitのhashを使うよう変更しています。

#!/bin/bash

# usage 
# create-build-script.sh REPOSITORY_URL REPOSITORY_NAME

services=(
    "foo-api" # 実際には複数のサブモジュールを定義している
)

declare -A tags=()

echo "#!/bin/bash" > /tmp/build.sh
echo "#!/bin/bash" > /tmp/push.sh

for service in "${services[@]}" ; do
    if [ ! -d "./${service}" ]; then
        echo "[${service}] Service directory not found"
        continue
    fi

    hash=`git -C ./${service} log --pretty=%H | head -n 1`

    echo "[${service}] Current tag => $2:${hash}"
    result=`aws ecr list-images --repository-name $2 | grep $hash | wc -l`

    if [ $result -eq 0 ]; then
        echo "docker build --tag $1:${hash} ./${service}"
        
        echo "echo \"docker build --tag $1:${hash} ./${service}\"" >> /tmp/build.sh
        echo "docker build --tag $1:${hash} ./${service}" >> /tmp/build.sh

        echo "echo \"docker push $1:${hash}\"" >> /tmp/push.sh
        echo "docker push $1:${hash}" >> /tmp/push.sh
    else
        echo "[${service}] Latest version image exists, build process was skipped."
    fi

    tags[$service]=$hash
done

for key in "${!tags[@]}"; do
    printf '%s\0%s\0' "$key" "${tags[$key]}"
done |
jq -Rs '
  split("\u0000")
  | . as $a
  | reduce range(0; length/2) as $i 
      ({}; . + {($a[2*$i]): ($a[2*$i + 1]|fromjson? // .)})' > /tmp/build.json

このスクリプトで使用している jq ですが標準のビルド環境でインストールすると1.3という少し古いバージョンがインストールされるので、 install フェーズで強引にjq-1.5をインストールしています。

また、スクリプトではawsコマンドを使用して、ecrのイメージ一覧を取得しているので、 CodeServiceRollecr:ListImages 権限を追加する必要があります。

...

そして、最終的なBuild処理の定義はこんな感じです。(実際に使用しているものからは少し変えています...)

  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.1
          phases:
            install:
              commands:
                - apt-get update && apt-get -y install python-pip git wget jq
                - pip install --upgrade python
                - pip install --upgrade awscli
                - wget https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64
                - chmod +x jq-linux64
                - mv jq-linux64 $(which jq)
            pre_build:
              commands:
                - printenv
                - git clone "https://$GITHUB_TOKEN@github.com/bar/foo-app.git"
                # - echo -n "$CODEBUILD_LOG_PATH" > /tmp/build_id.out
                # - printf "%s:%s" "$REPOSITORY_URI" "$(cat /tmp/build_id.out)" > /tmp/build_tag.out
                # - printf '{"tag":"%s"}' "$(cat /tmp/build_id.out)" > /tmp/build.json
                - $(aws ecr get-login)
                - bash create-build-shript.sh "$REPOSITORY_URI" "$REPOSITORY_NAME"
            build:
              commands:
                # - docker build --tag "$(cat /tmp/build_tag.out)" .
                - bash /tmp/build.sh
                - cat /tmp/build.json
            post_build:
              commands:
                # - docker push "$(cat /tmp/build_tag.out)"
                - bash /tmp/push.sh
          artifacts:
            files: /tmp/build.json
            discard-paths: yes
      Environment:
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: "aws/codebuild/docker:1.12.1"
        Type: "LINUX_CONTAINER"
        EnvironmentVariables:
          - Name: AWS_DEFAULT_REGION
            Value: !Ref AWS::Region
          - Name: REPOSITORY_URI
            Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository}
          - Name: REPOSITORY_NAME
            Value: !Ref Repository
          - Name: GITHUB_TOKEN
            Value: !Ref GitHubToken
      Name: !Sub ${AWS::StackName}-codebuildproject
      ServiceRole: !Ref CodeBuildServiceRole

この設定で作成される build.json はサンプルとは異なりますのでこのデータを受け取る deployer.py の該当箇所も修正する必要があります。

def get_build_artifact_id(build_id):
... snip
        print(objbuild['foo-api']) """サンプルでは 'tag' になっているので変更
        return objbuild['foo-api']

実際は複数のsubmoduleをデプロイする場合には、他にもいろいろと修正が必要ですが、ひとまずこのような形でgit submoduleに対応できそうです。

AWSはあまり使ったことがなく、試行錯誤しているところなのでもっといい方法がないか含め引き続きいろいろ試してみます。