はじめに
今回試したコードはGitHubで公開しています。
この記事の内容を執筆するにあたり、技術検証段階でAzureサポートのOさんに大きなご助力をいただきました。この場をお借りして感謝申し上げます💐
今回やりたいこと
WebスクレイピングやE2Eテストで利用されるPuppeteerを、Azure Functionsで動かしたい。
ただし、公式で利用できると発表されている環境とは、少し異なる設定で開発したい。
公式発表: Azure Functions Linux Consumption(消費量)プラン であれば実行可能
動かしたい環境: Azure Functions Linux App Service プラン
Azure Functions on Linux では、公式にheadless Chromiumがサポートされている
ただし、LinuxのConsumption(消費量)プランのみ。
この発表は2020年8月だが、自分が調べた感じ現在も変わっていないっぽい🤔
前提として、なぜDockerを使うのか
公式にpuppeteerが動作すると保証されているAuzre Functionsの価格プランは「Consumptionプラン」のみですが、「App Service プラン」を選択しています。このプランだと、puppeteerが動作するのに必要な依存ライブラリがインストールされていないので、Dockerを利用し、コンテナに依存を事前にインストールするようにします。
「App Service プラン」を選択する理由
- VNet統合を使いたいから
Dockerを利用する理由
- 従量課金である「消費量」プランではなく、「App Service プラン」を使いたいが、Chromium実行に必要な依存ライブラリがインストールされていない
- Azure Functions (Linux) で Puppeteer が使えるようになってた - ほりひログで言及されているように、日本語フォント問題がある
どちらも、package.json
のpostinstall
scriptなんかでshellを実行し、ライブラリやフォントをインストールして対策することはできると思います。ですが、PaaSであるFunctionsのランタイムに直接コマンド実行するのはなんか違くね?😑 とも思ったので。
では、Dockerを使ってAzure Functionsでpuppeteerを動かしていきましょう💪
Functionsリポジトリを作成
Azure Functions Core Toolsはv4を使用しています。
$ func version
4.0.5030
$ mkdir azure-function-docker-puppeteer
$ cd azure-function-docker-puppeteer
# typescriptのFunctions雛形を作る
$ func init --worker-runtime node --docker --language typescript
Writing .funcignore
Writing package.json
Writing tsconfig.json
Writing .gitignore
Writing host.json
Writing local.settings.json
Writing /Users/user/Desktop/azure-function-docker-puppeteer/.vscode/extensions.json
Writing Dockerfile
Writing .dockerignore
# "sample"という名前の関数を、HTTP Triggerで作成する
$ func new --template "HTTP trigger" --name sample --authlevel function
Select a number for template:HTTP trigger
Function name: [HttpTrigger] Writing /Users/user/Desktop/azure-function-docker-puppeteer/sample/index.ts
Writing /Users/user/Desktop/azure-function-docker-puppeteer/sample/function.json
The function "sample" was created successfully from the "HTTP trigger" template.
依存パッケージをインストールします📦
テンプレートで用意されてるパッケージのバージョンが少し古いので、そのアップデートも含みます。
$ npm i puppeteer
$ npm i -D typescript @types/node @azure/functions azure-functions-core-tools
puppeteerの動作確認をするためのコードを記述します✍️
import { AzureFunction, Context, HttpRequest } from '@azure/functions';
import puppeteer, { Browser } from 'puppeteer';
const httpTrigger: AzureFunction = async function (
context: Context,
req: HttpRequest
): Promise<void> {
let browser: Browser;
try {
browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox'],
defaultViewport: {
width: 1200,
height: 900,
},
// WindowsPCでデバッグするときはコメントアウトを外す
// See https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-windows
// ignoreDefaultArgs: ['--disable-extensions'],
});
} catch (error) {
context.log.error(error);
throw new Error('Failed to launch puppeteer browser.');
}
try {
const url = req.query.url || 'https://google.com/';
const page = await browser!.newPage();
await page.setExtraHTTPHeaders({
'Accept-Language': 'ja-JP',
});
await page.goto(url);
const screenshotBuffer: string | Buffer = await page.screenshot();
context.res = {
body: screenshotBuffer,
headers: {
'content-type': 'image/png',
},
};
} catch (error) {
context.log.error(error);
throw new Error('Error!!');
} finally {
await browser!.close();
}
};
export default httpTrigger;
ローカルでFunctionsを起動してみる
まずはローカルで起動してみる。
※まだDockerは使用しません。
azure-function-docker-puppeteer $ npm start
> azure-function-docker-puppeteer@1.0.0 prestart
> npm run build
> azure-function-docker-puppeteer@1.0.0 build
> tsc
> azure-function-docker-puppeteer@1.0.0 start
> func start
Azure Functions Core Tools
Core Tools Version: 4.0.5198 Commit hash: N/A (64-bit)
Function Runtime Version: 4.21.1.20667
Functions:
sample: [GET,POST] http://localhost:7071/api/sample
For detailed output, run func with --verbose flag.
[2023-06-08T07:12:32.943Z] Worker process started and initialized.
ブラウザでhttp://localhost:7071/api/sample
にアクセスすると、Googleのトップ画面が表示されます🖥️
確認できたら、Cmd
+ C
or Ctrl
+ C
で起動しているfunctionを停止しておきましょう。
Dockerfileを作成
Puppeteer公式のDocsにはRunning Puppeteer in Docker
の見出しで、Dockerfileが公開されていますが、そのまま使ってもFunctionsとして上手く動かすことができなかったので編集しています。
作成したDockerfileはこちら↓
# To enable ssh & remote debugging on app service change the base image to the one below
# FROM mcr.microsoft.com/azure-functions/node:4-node16-appservice
FROM mcr.microsoft.com/azure-functions/node:4-node18-appservice
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
AzureFunctionsJobHost__Logging__Console__IsEnabled=true \
DEBCONF_NOWARNINGS=yes
WORKDIR /home/site/wwwroot
COPY . /home/site/wwwroot
RUN cd /home/site/wwwroot
RUN apt-get update \
&& apt-get install -y wget gnupg \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# If running Docker >= 1.13.0 use docker run's --init arg to reap zombie processes, otherwise
# uncomment the following lines to have `dumb-init` as PID 1
# ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_x86_64 /usr/local/bin/dumb-init
# RUN chmod +x /usr/local/bin/dumb-init
# ENTRYPOINT ["dumb-init", "--"]
# Uncomment to skip the chromium download when installing puppeteer. If you do,
# you'll need to launch puppeteer with:
# browser.launch({executablePath: 'google-chrome-stable'})
# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
RUN npm install \
# NPM 利用プロジェクトにおける ユーザー名前空間再割当てエラーに対する対策
# See https://jpazpaas.github.io/blog/2023/02/15/Docker-User-Namespace-remapping-issues.html#npm-%E5%88%A9%E7%94%A8%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B-%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E5%90%8D%E5%89%8D%E7%A9%BA%E9%96%93%E5%86%8D%E5%89%B2%E5%BD%93%E3%81%A6%E3%82%A8%E3%83%A9%E3%83%BC
&& find /home/site/wwwroot/node_modules ! -user root | xargs --no-run-if-empty chown root:root
CMD ["google-chrome-stable"]
RUN npm run build
FunctionAppにデプロイ
デプロイに Azure container registry (ACR)を使用するなら、私が以前書いたこちらの記事を参考にできるかもしれません。
今回は、AzurePipelinesで以下のような流れでデプロイする前提とします。
- DockerImageをbuildする
- buildしたimageをACRにpushする
- FunctionAppにデプロイ設定する
今回紹介するやり方だと、FunctionApp内でimageをpullし、コンテナを起動するのはAzureが勝手にやってくれます。
pipeline YAMLを作成する
長いので折りたたんでいます✂️
trigger: none
resources:
- repo: self
# Pipelineが以下の変数を参照できるように設定してください。
# - AzureServiceConnectionId: Service Connection Name or UUID
# - FunctionAppName: The resource name of Azure Functions
# - ACRRepositoryName: The repository name of ACR. This is also used as an Docker image name
# - ACRResourceName: Sub domain name of ACR server
variables:
pool: 'ubuntu-latest'
Azure.ServiceConnectionId: $(AzureServiceConnectionId)
FunctionApp.Name: $(FunctionAppName)
ACR.Name: $(ACRRepositoryName)
ACR.ImageName: '$(ACR.Name):$(Build.BuildId)'
ACR.FullName: '$(ACRResourceName).azurecr.io'
jobs:
- job: BuildAndPushImage
displayName: Build And Push an Docker Image
condition: succeeded()
pool:
vmImage: $(pool)
steps:
- task: Docker@1
displayName: 'Build an image'
inputs:
azureSubscriptionEndpoint: '$(Azure.ServiceConnectionId)'
azureContainerRegistry: '$(ACR.FullName)'
imageName: '$(ACR.ImageName)'
command: build
dockerFile: '$(Build.SourcesDirectory)/Dockerfile'
- task: Docker@1
displayName: 'Push an image'
inputs:
azureSubscriptionEndpoint: '$(Azure.ServiceConnectionId)'
azureContainerRegistry: '$(ACR.FullName)'
imageName: '$(ACR.ImageName)'
command: push
- job: UpdateAppServiceConfiguration
displayName: Update AppService Configuration to Deploy an Image
dependsOn: BuildAndPushImage
condition: or(succeeded(), eq(dependencies.BuildAndPushImage.result, 'Succeeded'))
pool:
vmImage: $(pool)
steps:
- task: AzureFunctionAppContainer@1
displayName: 'Azure Function App on Container Deploy: $(FunctionApp.Name)'
inputs:
azureSubscription: '$(Azure.ServiceConnectionId)'
appName: $(FunctionApp.Name)
imageName: '$(ACR.FullName)/$(ACR.ImageName)'
Pipelineを動かす
Pipelinesが成功して、しばらく待てばAzure Functionsリソースでimageがpullされ、コンテナが起動しているはずです。
Functionsで動作確認
デプロイできたら、実際のAzure FunctionsリソースのHTTPエンドポイントをコールして動作確認してみましょう。
/api/sample?url=https://qiita.com/YOS0602
(※function keyは省略)にアクセスすると、日本語が文字化けせず表示されていますね🧑💻
興味本位でアラビア語のサイトを試してみたけど表示されました。豆腐にならなくて良かった😌
参考記事