LoginSignup
2
2

More than 1 year has passed since last update.

Azure Functions に Docker イメージをデプロイして puppeteer を使いたい

Posted at

はじめに

今回試したコードは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を利用する理由

  1. 従量課金である「消費量」プランではなく、「App Service プラン」を使いたいが、Chromium実行に必要な依存ライブラリがインストールされていない
  2. Azure Functions (Linux) で Puppeteer が使えるようになってた - ほりひログで言及されているように、日本語フォント問題がある

どちらも、package.jsonpostinstallscriptなんかで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の動作確認をするためのコードを記述します✍️

sample/index.ts
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はこちら↓

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で以下のような流れでデプロイする前提とします。

  1. DockerImageをbuildする
  2. buildしたimageをACRにpushする
  3. FunctionAppにデプロイ設定する

今回紹介するやり方だと、FunctionApp内でimageをpullし、コンテナを起動するのはAzureが勝手にやってくれます。

pipeline YAMLを作成する

長いので折りたたんでいます✂️
azure-pipelines.yml
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は省略)にアクセスすると、日本語が文字化けせず表示されていますね🧑‍💻

興味本位でアラビア語のサイトを試してみたけど表示されました。豆腐にならなくて良かった😌

参考記事

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2