Azure DevOps Pipelinesの自動ビルドでSSHを叩く / SSHでファイル転送をする


趣味で作っているホームページのリニューアルのため、勉強も兼ねてAzure DevOpsからさくらのレンタルサーバへ自動デプロイを試してみました。
Azure DevOpsはほぼほぼ個人で使っている分には無料の枠内で収まるため、さくらのレンタルサーバの運用費用だけという超低コストで、サーバへの自動ビルド、デプロイが実現できました。
ただ、さくらのサーバは制約が多いため非常につまづく箇所が多かったですので、共有したいと思います。

この記事では以下を実現したい方に有用です。

  • Azure Pipelinesでビルドした成果物をSSHでサーバへ送りたい
  • Azure Pipelinesからデプロイ先のサーバへSSHでコマンドを叩きたい

構成図

前提条件として、実際に構成した自動デプロイの方式を説明します。
現在開発中のホームページはSPAを導入しており、フロントエンドをVue.jsで、バックエンドをLaravel 5で実装しています。

この記事では、特に③と④について詳しくやり方をご説明します。

※ この方式は「今使っているもので自動デプロイをするにはこうすればできるだろう」と考えた結果の策であることをご留意ください。

1. Azure DevOps に SSH の接続情報を設定する

まず、Azure DevOpsにさくらのレンタルサーバへSSH接続するため、ホスト名などの情報を設定する必要があります。
※ パスワードでなく、秘密鍵での認証を行いたい方は、先に つまづきポイント の方をご確認ください。

Azure DevOpsの設定ページにアクセスし、「Project settings」から「Service connections」を選択します。

「New Service Connection」を選択すると、下図のように接続タイプを選択するパネルが出てきますので、「SSH」を選択します。

ホスト名、パスワードなどを入力する画面に移ります。さくらのレンタルサーバであれば、接続情報はこちらのページを参照ください。

つまづきポイント 秘密鍵での認証ができない

生成した秘密鍵によってSSHログインを行いたい場合は、Private Keyの項目に秘密鍵のテキストを挿入すればよいと思いますが、1行パスワードの入力項目になっており、無理やりテキストを入れても「Failed to connect to remote machine. Verify the SSH service connection details. Error: Error: Cannot parse privateKey: Unsupported key format.」のエラーが発生してしまいました。
下記のAzure PipelinesのIssue記事を参考にし、旧UIで秘密鍵のテキストを設定しました。
- Copy files over SSH: Cannot parse privateKey #11439 : https://github.com/microsoft/azure-pipelines-tasks/issues/11439

手順は下記のとおりです。
画面右上のMicrosoftアカウントのアイコンをクリックし、横の「…」のボタンを押下し、「User Settings」を選択します。場所がわかりにくい・・・

その後、「Preview features」を選択します。
「New service connections experience」がOnになっていると思いますので、Offに切り替えましょう。これで旧UIによる設定ができます。
フィードバックを入力する画面が出てきますが、特に何も入力しなくてもOKです。

「Project settings」から「Service connections」を選択すると、設定画面が変わっているのがわかります。

SSHの接続情報を入力する画面も下図のように変わっています。「Upload SSH private key file...」のリンクから秘密鍵のアップロードが可能です。

2. Azure PipelinesからSFTPでファイルを転送する

ここからは、Azure Pipelinesの自動ビルド、自動デプロイを定義するYAMLファイル (azure-pipelines.yml) の記述方法の説明になります。
Azure Pipelines全体の設定方法については、こちらの記事を参考にさせていただきました。
- 1rep - 3分でできるAzure DevOps Azure Pipelines編 ~ https://qiita.com/MarcyMarcy/items/c48b6ad5570a4c883c7f

さて、azure-pipelines.ymlでSFTPによってファイル転送を行うには、Copy Files Over SSH Task を使用します。
sshEndPointに 1. で設定したService connectionの名前を指定し、ソースフォルダとターゲットフォルダを指定するとコピーできます。

Node.jsでnpm installnpm buildした結果出力されたdistディレクトリをSFTPでコピーする例を下記に示します。

azure-pipelines.yml (抜粋)
steps:
- task: NodeTool@0
  inputs:
    versionSpec: '10.x'
  displayName: 'Install Node.js'

- script: |
    npm install
    npm run build
    cp -p .htaccess dist/
  displayName: 'npm install and build'

- task: CopyFilesOverSSH@0
  inputs:
    sshEndpoint: '<service-connection-name>'
    # 上のステップ (npm install and build) でビルドしたディレクトリを指定する
    sourceFolder: 'dist'
    contents: '**'
    targetFolder: '/home/user/path/to/dist'
  displayName: 'Send compiled frontend files'

※ 当初は cleanTargetFolder を true に設定していたのですが、さくらのレンタルサーバではその設定に伴い発行される rm -rfコマンドがなぜか動かなかったので、切っています。

3. Azure PipelinesからSSHでコマンドを実行する

azure-pipelines.ymlでSSHを実行するには、SSH Deployment Task を使用します。
sshEndPointに 1. で設定したService connectionの名前を指定し、インラインでコマンドを記述するか、Azure DevOps Reposにプッシュしているシェルスクリプトを指定することで実行できます。
ここで、 runOptions に commands を指定すると、それぞれのコマンドが別プロセスで動いてしまう ので注意が必要です。そのため、cdでカレントディレクトリを変えても、次のコマンドを実行するときには元に戻ってしまいます。従って、カレントディレクトリを変えるようなスクリプトを実行したい場合は、runOptions に inline または scriptを指定してください。

GitでAzure DevOps Reposからソースの取得、Composerで依存モジュールの取得、Artisanでキャッシュの更新を行う例を以下に示します。
さくらのレンタルサーバ側ですでにGitやComposerの設定は済ませている前提とします。

azure-pipelines.yml (抜粋)
steps:
- task: SSH@0
  inputs:
    sshEndpoint: '<service-connection-name>'
    runOptions: 'inline'
    failOnStdErr: false
    inline: |
      #!/usr/local/bin/bash
      cd path/to/backend/source
      # Gitからソースを取得する
      git pull origin develop
      # 依存ライブラリを取得する
      composer install --no-dev
      composer dump-autoload
      # キャッシュの更新を行う
      php artisan clear-compiled
      php artisan optimize
      php artisan config:cache
  displayName: 'Backend build'

※ さくらのレンタルサーバではシェルスクリプトの最初の行が #!/bin/bash ではなく、 #!/usr/local/bin/bash であることに注意してください。私はこれで30分ぐらいつまづきました・・・。

まとめ

最後に、出来上がったazure-pipelines.ymlの全体を示します。

azure-pipelines.yml
# トリガーとするブランチ
trigger:
- master

# ビルド環境のイメージ
pool:
  vmImage: 'ubuntu-latest'

steps:
# Node.jsをビルド環境にインストール
- task: NodeTool@0
  inputs:
    versionSpec: '10.x'
  displayName: 'Install Node.js'

# Node.js (Vue.js) のビルド
- script: |
    npm install
    npm run build
    cp -p .htaccess dist/
  displayName: 'npm install and build'

# ビルド成果物を公開サーバに送る
- task: CopyFilesOverSSH@0
  inputs:
    sshEndpoint: '<service-connection-name>'
    sourceFolder: 'dist'
    contents: '**'
    targetFolder: '/home/user/path/to/dist'
  displayName: 'Send built frontend files'

# PHP (Laravel) のデプロイ
- task: SSH@0
  inputs:
    sshEndpoint: '<service-connection-name>'
    runOptions: 'inline'
    failOnStdErr: false
    inline: |
      #!/usr/local/bin/bash
      cd path/to/backend/source
      git pull origin master
      composer install --no-dev
      composer dump-autoload
      php artisan clear-compiled
      php artisan optimize
      php artisan config:cache
  displayName: 'Backend build'

これで、Azure DevOps Reposへのプッシュに合わせて自動でさくらのレンタルサーバにデプロイされるようになりました。
ビルドスクリプトにテストのスクリプトを入れると、自動テストもやってくれます。ビルドが完了/失敗したらメールで通知もしてくれるので安心です。

ちなみに、1回のビルドに3~4分ほど時間がかかります。特に、Node.jsのビルド成果物をSSHで送るのがかかってしまっているようです。もう少しいい方法を考えたいと思っています。
Azure Pipelinesでは、毎月1800分間のビルド時間は無料枠のため、1回4分と考えても450回分は無料で利用できる計算になります。個人で使う分には十分無料枠に収められると考えています。