ExportしたApp Service 証明書にパスフレーズをつける

管理を手伝っている、友人のサイトでApp Service 証明書を移行する必要があってちょっとハマったので備忘録。

App Service 証明書のExport

CloudShellから以下のコマンドで、Exportできます。

$ az keyvault secret download \
    --file appservicecertificate.pfx \
    --vault-name <key-valut-name> \
    --name <保存先のシークレット名> \
    --encoding base64

vault-name には証明書作成時に設定した、キーコンテナ名を指定します。

f:id:kaz_29:20200627162614p:plain
キーコンテナ

name には証明書が保存されている、シークレットの名前を指定します。

f:id:kaz_29:20200627162651p:plain
kay-vault

このコマンドでExportした証明書には空のパスフレーズで生成されます。

このあたりの詳細は公式ドキュメントにも解説があります。

証明書のImport

で、この証明書をアップロードしようとするとアップロード画面ではパスフレーズが必須になっています。

f:id:kaz_29:20200627163303p:plain
Import Error

pfxファイルにパスワードをつける方法を探すのに少し手間取りましたが、以下で大丈夫でした。

# 一旦pem形式に変換
openssl pkcs12 -in appservicecertificate.pfx -out example.com.pem -nodes

# 再度pfx形式に変換、この際にパスフレーズの入力プロンプトが表示されます。
openssl pkcs12 -export -out example.com.pfx -in example.com.pem

ということで無事移行できました。

CakePHP4でRoutingのテスト

最近React+TypeScriptばかりで、CakePHPのコードはあまり書いていないわたなべです。

Routingのテスト

CakePHP1の頃の新原さんのブログ(なんと2009-05-25の記事、11年前!?)でも書かれているように、routes.phpの設定変更は、思わぬバグを出す可能性があるので、UnitTestでの動作確認は必須だと思っています。

CakePHP3までは、以下のような感じでテストできていましたが、CakePHP4でRoutingがmiddleware化した影響などでそのままでは動作しません。

<?php
declare(strict_types=1);
...
use Cake\Network\Request;
...

    public function testRouting($request, $expected)
    {
        $request = new Request([
            'url'         => '/api/articles.json',
            'environment' => ['REQUEST_METHOD' => 'GET']
        ]),
        $result = Router::parseRequest($request);
        $expected = [
            'pass'       => [],
            'plugin'     => null,
            'controller' => 'Articles',
            'prefix'     => 'api',
            '_ext'       => 'json',
            'action'        => 'index',
        ];
        $this->assertEquals($expected, $result);
    }
...

CakePHP4でのRoutingのテスト

最初書いたテストコードは以下のような感じ。

<?php
declare(strict_types=1);
...
use Cake\Http\ServerRequest;
...

    public function testRouting()
    {
        $request = new ServerRequest([
            'url'         => '/api/users/login.json',
            'environment' => ['REQUEST_METHOD' => 'POST']
        ]);
        $result = Router::parseRequest($request);
        $expected = [
            'pass' => [],
            'plugin' => null,
            'controller' => 'Users',
            'prefix' => 'Api',
            '_ext' => 'json',
            'action' => 'login',
            '_matchedRoute' => '/api/users/login',
            '_method' => ['POST'],
            '_middleware' => ['bodies']
        ];
        $this->assertEquals($expected, $result);
    }
...

これを実行すると、以下のようなエラーになってしまいます。もちろんroutes.phpには正しく設定されていて、Web経由でのアクセスにも問題ありません。

Cake\Routing\Exception\MissingRouteException: A route matching "/api/users/login.json" could not be found.

色々調べてみたところ、Routerがmiddleware化されたため、Applicationにmiddlewareが追加された時点でrouterを初期化するようになったようです。 このため、routingテーブルが空の状態になっていました。

解決方法

ということで、Routingテーブルを初期化する処理をtraitにしました。

<?php
declare(strict_types=1);

namespace App\Test\Utility;

use Cake\Routing\Router;

trait RoutingTestTrait {
    protected function initializeRoute()
    {
        Router::reload();
        $routes = Router::createRouteBuilder('/');
        require CONFIG . 'routes.php';
    }
}

前出のコードをこのtraitを使うようにこんな感じに修正するとroutingのテストができます。

<?php
declare(strict_types=1);
...
use Cake\Http\ServerRequest;
use App\Test\Utility\RoutingTestTrait;    // 追加
...
class UsersControllerTest extends TestCase
{
    use IntegrationTestTrait;
    use RoutingTestTrait;    // 追加
...

    public function testRouting()
    {
        $this->initializeRoute();    // 追加

        $request = new ServerRequest([
            'url'         => '/api/users/login.json',
            'environment' => ['REQUEST_METHOD' => 'POST']
        ]);
        $result = Router::parseRequest($request);
        $expected = [
            'pass' => [],
            'plugin' => null,
            'controller' => 'Users',
            'prefix' => 'Api',
            '_ext' => 'json',
            'action' => 'login',
            '_matchedRoute' => '/api/users/login',
            '_method' => ['POST'],
            '_middleware' => ['bodies']
        ];
        $this->assertEquals($expected, $result);
    }
...

まとめ

routingのテストを書いていたことで、routing変更時に何度も救われたことがあるので是非テストを書くことをお勧めします。 CakePHP4は、色々と改善されていい感じに進化しているので今後も使っていこうと思います。

以上、小ネタでした。

2020/6/25追記

RoutingのMiddleware化は、CakePHP3で実施されていました。具体的に何が理由でRoutingテーブルが初期化されていないかは不明。後日調べてみてわかれば追記します。

Github ActionsでMultiContainerなAzure WebApp for Containersをデプロイする

友達に頼まれて、@kunyamiさんのスライド 堅牢&運用楽々な WordPress を Azure App Service でを参考に、WordpressをMultiContainerなAzure WebApp for Containersで構築してたのですが、いくつかハマったのでメモとして残します。

基本的な設定は、@kunyamiさんの資料の通りに進めたのですが、今回は、一部Wordpress管理外の静的なページを含むため、独自コンテナをビルドする必要があったので、GitHub Actionsを使用して、コンテナイメージのビルド、Azure WebApp for Containersへのデプロイをすることにしました。

リポジトリは以下の様な構成になっています。

  • Dockerfile - デプロイ用Dockerfile
  • docker-compose.yml - デプロイ用 docker-composeファイル
  • 静的ファイル用のディレクト
  • その他設定ファイルなど

ベースイメージは、wordpress公式のものを使用して、静的なページは、ビルド時にコピーする様にしました

Dockerfileサンプル

FROM wordpress:latest

COPY static_webpages /var/www/html/static_webpages

事前準備

Azure WebAppデプロイ用クレデンシャルの作成

いつものですね。以下のコマンドを実行すると表示されるJSONがクレデンシャルです。

$ az ad sp create-for-rbac --name "https://[WebAppの名前].azurewebsites.net" --role contributor \
    --scopes /subscriptions/[サブスクリプションID]/resourceGroups/wetest \
    --sdk-auth

秘匿情報の設定

Github Actions内で利用する秘匿情報は、Githubの対象リポジトリ内の Settings -> Secrets で設定します。

f:id:kaz_29:20200504130051p:plain
github Secrets

今回は、ビルドしたイメージをDocker Hubに保存するので、Docker Hubアカウントパスワードと先ほど作成した、クレデンシャルをSecretsに追加します。

  • AZURE_CREDENTIALS - Azureクレデンシャル
  • DOCKER_PASSWORD - Docker Hubのパスワード

Github Actionsの設定

Github Actionsの設定は、リポジトリ内の .github/workfloes ディレクトリ以下に設定ファイルを配置します。今回作成したのは以下の様なファイルです。

name: Build And Deploy

env:
  DOCKER_USERNAME: [Docker Hubのユーザ名]
  DOCKER_IMAGENAME: [docker image名]
  AZURE_WEBAPP_NAME: [webappの名称]

on: 
  push:
    branches:
      - master
  
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v2
    
      - name: Azure authentication
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS  }}

      - name: Build & push Docker image
        uses: docker/build-push-action@v1
        with:
          image: ${{ env.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGENAME }}
          repository: ${{ env.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGENAME }}
          dockerfile: Dockerfile
          username: ${{ env.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
          tags: ${{ github.sha }} 

      - name: 'Deploy to Azure Web App for Container'   
        uses: azure/webapps-deploy@v2
        with: 
          images: ${{ env.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGENAME }}:${{ github.sha }}
          app-name: ${{ env.AZURE_WEBAPP_NAME }}
          configuration-file: docker-compose-webapp.yml

yamlファイルを見れば一目瞭然ですが、今回作成したJobには以下の4つのStepが定義されています。

  • Check out code - コードをチェックアウト
  • Azure authentication - Azureの認証処理
  • Build & push Docker image - docker imageのビルドとプッシュ
  • Deploy to Azure Web App for Container - Azure WebApp for Containersへのデプロイ

docker imageのビルドとプッシュ

docker imageのビルドとプッシュには、Docker公式のGitHub Actionを使っています。 公式サイトに詳しい解説があるので、そちらを参照してください。

今回は、ビルト時のタグにgit hashを使用しているので以下の様に指定しています。

          tags: ${{ github.sha }} 

Azureの認証とAzure WebApp for Containersへのデプロイ

Azure WebApp for Containersへのデプロイは、Azure 公式のGitHub Actionを使っています。 こちらも、公式サイトに詳しい解説があるので、そちらを参照してください。

MultiContainerなAzure WebApp for Containersのデプロイ方法

今回は、MultiContainerなAzure WebApp for Containersを使用しているのですが、公式ドキュメントにはMultiContainerの場合のデプロイ方法は書かれていないため、GitHub Actionのソースコードを確認したところ、以下の様に設定情報を初期化しているのを発見しました。

...
class ActionParameters {
    constructor(endpoint) {
        this._publishProfileContent = core.getInput('publish-profile');
        this._appName = core.getInput('app-name');
        this._slotName = core.getInput('slot-name');
        this._packageInput = core.getInput('package');
        this._images = core.getInput('images');
        this._multiContainerConfigFile = core.getInput('configuration-file');
        this._startupCommand = core.getInput('startup-command');
        this._endpoint = endpoint;
    }
...

ということで、MultiContainer時のdocker-composeファイルは configuration-file で指定できました。最終的なStepの設定が下記です。

      - name: 'Deploy to Azure Web App for Container'   
        uses: azure/webapps-deploy@v2
        with: 
          images: ${{ env.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGENAME }}:${{ github.sha }}
          app-name: ${{ env.AZURE_WEBAPP_NAME }}
          configuration-file: docker-compose-webapp.yml

docker-compose.yml 内でイメージ名を指定していますが、 images に今回ビルドしたタグを含むイメージ名を指定しています。 これもドキュメントには記載がないのですが、試行錯誤している最中に以下のエラーが発生してエラーメッセージの内容から発見したのですが、MultiContainer時にimage名を指定すると、docker-compose内のイメージ名を上書きしてくれますので、自前でdocker-composeファイルを書き換える様な処理は書かなくても大丈夫でした。

試行錯誤中に発生したエラーメッセージ

Deployment Failed with Error: Error: For single-container, just specify a valid image name. For multi-container specifying a Docker-Compose file is mandatory and specifying image names is optional. Provide image names if the tags in Docker-Compose file need to be substituted.

また、slot-name でデプロイ対象のスロット名も指定できる様なので、B/Gデプロイもできそうですね。

まとめ

マーケットプレイスで公開されているGitHub Actionはソースコードも公開されているので、ドキュメントに記載のない内容はソースコードにあたるとすんなり解決できることも多そうです。 GitHub Actionsかなり便利なので、これからも積極的に使っていきたいです。

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を使うことにします。

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

<?php
...
$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に準拠したロガー