Puppteerでキャプチャ画像を取得 part2

## 概要
エンジニアもどき2年目に入りました。色々あって以下を行うLambda関数を作成することになりました。
色々学びがあったので備忘録がわり兼アウトプットのために書くことにしました。

1. 特定のページのキャプチャ取得
2. 取得したキャプチャをS3に保存
3. S3に保存したURLをDyanmoに記録

part2では`2. 取得したキャプチャをS3に保存`の実装とデプロイまで行います。
part2での実装内容は[こちら](https://github.com/syu-y/puppeteer-capture/tree/part2)

## 前提

[前回(part1)](https://qiita.com/yimi_getu/items/4c0dca38f8f55e8e6d3d)の続きです。part1に引き続き以下の準備は完了している前提です。
前回(part1)の実装内容は[こちら](https://github.com/syu-y/puppeteer-capture/tree/part1)


- nodejs
- ServerlessFramework
- yarn
- webpack
- iamのユーザ作成済み

## 実装作業
### 1. 準備
ローカルでの動作確認のためにローカルにS3のバケットを作成できる[`serverless-s3-local`](https://github.com/ar90n/serverless-s3-local)を使用します。

```zsh
# 追加
yarn add -D serverless-s3-local
```

続いて`serveress.yml`にS3の設定を追加します。

```yml:serverless.yml
service: puppeteer-capture
provider:
  # ・・・　省略　・・・
  # テーブル名やバケット名など環境変数
  environment:
    # ・・・　省略　・・・
    # 追加1 : バケット名
    CAPTURE_BUCKET: ${self:provider.environment.PREFIX}-capture-bucket
  iamRoleStatements:
    # 追加2 : S3の設定
    - Effect: Allow
      Action:
        - 's3:PutObject'
        - 's3:PutObjectAcl'
      Resource: 'arn:aws:s3:::${self:provider.environment.S3_BUCKET}*'
# 追加3 : ローカルでの動作用設定
custom:
  defaultStage: dev
  s3:
    port: 8081
    directory: .s3
    cors: false
# 追加3 : プラグイン
plugins:
  - serverless-webpack
  - serverless-s3-local
# ・・・　省略　・・・
# 追加4 : リソースの設定
resources:
  Resources:
    CaptureBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:provider.environment.CAPTURE_BUCKET}
```

ローカルで起動できるか確認します。

```zsh
sls s3 start -c ./serverless.yml
```

以下のように起動していれば完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137875/11e17db0-85be-eceb-b468-3551a3407128.png)

ローカルにS3のバケットが作成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137875/a8fda27f-8bec-c4ed-8d3f-46170c196676.png)

また、AWSのリソース(S3やDynamoDB)を操作する必要があるので、`aws-sdk`を使用します。

```zsh
# 追加
yarn add -D aws-sdk
```

Lambdaの実行環境には AWS SDK for JavaScriptが含まれているためdevDependenciesにのみ追加しています。
`webpack.config.js`の外部依存に`aws-sdk`を追加して準備は完了です。

```js:webpack.config.js
  //　・・・　省略　・・・
  target: 'node',
  // 追加
  externals: ['chrome-aws-lambda', 'aws-sdk'],
  //　・・・　省略　・・・
};
```

### 2. 実装

前回は取得したキャプチャ画像をローカルに書き出して保存していましたが、Lambdaは実行後、使用されたすべてのリソース（が破棄されてしまうので、S3に保存するように修正します。

```js:handler.js
// ・・・　省略　・・・
// 追加1 : S3のインポート
import { config, S3 } from 'aws-sdk';

// 追加2 : S3の設定とオプション（LOCALで実行した際の設定）
config.update({ region: process.env.AWS_REGION });
const s3Options = process.env.LOCAL
  ? {
      s3ForcePathStyle: true,
      accessKeyId: 'S3RVER',
      secretAccessKey: 'S3RVER',
      endpoint: new AWS.Endpoint('http://localhost:8081'),
    }
  : {};

// 追加3 : s3のクライアント/バケット名/アップロード関数
const s3 = new S3(options);
const bucket = process.env.CAPTURE_BUCKET;
const putObject = ({ key, body, contentType, acl }) => {
  const params = {
    Bucket: bucket,
    Key: key,
    Body: body,
    ContentType: contentType,
    ACL: acl,
  };
  console.log(params);
  return s3.putObject(params).promise();
};

const getCapture = async (url) => {
  // ・・・　省略　・・・
};

export const captureFunction = async event => {
  // ・・・　省略　・・・
  // 修正 : ファイルに書き出しからS3へアップロードするように変更
  // Before : ファイル書き出し
  // writeFileSync('/tmp/hoge.jpg', jpgBuf);
  // After : S3へアップロード
  try {
    await putObject({
      key: 'hoge.txt',
      body: jpgBuf,
      contentType: 'image/jpeg',
      acl: 'public-read',
    });
  } catch (error) {
    console.log(error);
    return { statusCode: 500, body: error.message };
  }

  return { statusCode: 200, body: 'キャプチャの取得に成功しました。'};
};
```

注意する点は`acl`、つまりアクセスコントロールリストに`public-read`の設定を忘れるとS3にアップロードはできるもののAccess Deniedとなりリソースにアクセスができなくなってしまいます。

### 3. 動作の確認
まずはローカル環境での動作の確認です。

```zsh
LOCAL=true sls invoke local --function captureFunction -c ./serverless.yml
LOCAL=true sls invoke local --function captureFunction --data '{"url":"https://qiita.com/"}' -c ./serverless.yml
```

実行して 200でレスポンスが帰ってくること、ローカルのバケットにキャプチャ画像が保存されていることを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137875/4aef9347-56f4-bc49-4bb7-1f8808d5c02e.png)

バケットに保存されている`hode.jpg._S3rver_object`を`hoge.jpg`にリネームして開くと確認することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137875/ec54a717-dcfd-8d65-d90f-d3fb44092773.png)

### 4. デプロイと動作確認

デプロイします。

```zsh
serverless deploy
```

実行し200でレスポンスが返ってくることを確認します。
また、AWSのコンソールからS3にアクセスしてバケットの中を確認します。

```zsh
sls invoke --function captureFunction -c ./serverless.yml
sls invoke --function captureFunction --data '{"url":"https://qiita.com/"}' -c ./serverless.yml
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137875/36b4c8a2-297a-f504-24fb-636e8aaffc13.png)

以上でpart2の作業は完了です。
次はS3に保存した画像のURLをDynamoDBに書き出す処理までの実装を行います。

## おわりに

- ACLのところで結構引っかかった（Bucketに保存できるもののアクセスできない）
- ローカルでのS3の使い方、ServerlessFrameworkでS3のバケットの作成など色々勉強になった
- 今回は日本語のページのキャプチャは必要なかったので対応していませんが、Lambdaのインスタンスに日本語フォントが含まれておらず、日本語を含むページのキャプチャした際に文字化けが発生します。（フォントを用意して設定する必要がある）

## 参考
- [AWS SDK (Node.js) で S3 putObjectするときに気をつけること](https://hotchpotchj37.wordpress.com/2017/03/08/aws-sdk-node-js-%E3%81%A7-s3-putobject%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AB%E6%B0%97%E3%82%92%E3%81%A4%E3%81%91%E3%82%8B%E3%81%93%E3%81%A8/)
- [AWS LambdaでPuppeteerを動かす
](https://qiita.com/kodai-saito/items/9051d2b30a29c7d64f7d)