Photoruction工事中!

Photoructionの開発ブログです!

チームでドラッカー風エクササイズをやってみた

こんにちは!株式会社フォトラクションでプロダクトマネージャーをしています南風原はえばる)です。

Photoruction Advent Calendar 2022の1日目の記事になります。

きっかけ

この記事を読んでくれているみなさんは、ドラッカー風エクササイズをご存知でしょうか?

私がこの言葉と出会ったのは社内で週1で開催している輪読会の中です。

また、社外のエンジニアイベントに参加した際、実際にチームで取り組んだ方の話を聞いてぜひうちのチームでもやってみたいと思いました。

というのも、9月からチームが再結成し新メンバーも加入してきたタイミングだったため、メンバー同士の理解を深めていきたかったのと、お互いに期待していることのすり合わせを行うことでよりチームとして結束してプロダクト開発を進めていきたいという気持ちもありました。

ドラッカー風エクササイズとは

  • 書籍「アジャイルサムライ」で紹介された、チームメンバー同士でお互いの考えを交換し合う手法です。
  • 4つの質問にそれぞれ答え、チームメンバーに共有することで、お互いの考えや価値観、期待のすり合わせを行います。

    【4つの質問】

    1. 自分は何が得意なのか?
    2. 自分はどういう風に仕事をするか?
    3. 自分が大切に思う価値はなにか?
    4. チームメンバーは自分にどんな成果を期待してると思うか?

どうやったのか?

今回は、「お互いを知る」ことと「お互いの期待を知る」ことを第一目的としたかったので、本家の4つ質問を少しアレンジして以下の質問に答えていく形をとりました。

【4つの質問】

  1. 自分の得意なこと?
    • 業務における得意分野、性格的な特性。他人と比較せず、自分が得意だと思うことでOK
  2. 仕事をする上で大切にしている価値観
    • 仕事のスタイル、こんなことでモチベ上がる、これは避けてるなど
  3. チームメンバーから期待されていると思うこと
  4. メンバーに対して期待すること

進め方としては、最初に質問1~3を各自記入しそれぞれ共有してもらってメンバー同士の理解を深める時間をとりました。

最後に質問4を記入してメンバーに期待していることと、自分が期待されていると思っていたところのすり合わせや感想を言い合う時間をとりました。

ツールはMiroを用いて行いました↓

まとめ

実際やってみての感想としてメンバーからは以下のような声をいただきました。

  • 普段、業務をする中で仕事に対する価値観や大切にしていることを考える機会がないのでこの時間で考えるきっかけになって良かった
  • 他のメンバーの大切にしている価値観とかを知ることができて新しい発見になった
  • メンバーから期待されていることが知れて良かった …etc

普段、業務に追われて中々こういったチームビルディングを目的としたワークショップを開催できずにいたのですが今回ノリでやってみよ〜と提案し、実際に開催することができよかったです。

(ノリに乗ってくれたチームメンバーには感謝!!)

実際に開催してみて、個人としてもメンバーの得意なこと考えていることや価値観、期待していることなどを知ることができたのでとても有意義な時間でした。

一度の開催で終わらせずに定期的に開催していこうと思います。

株式会社フォトラクションでは一緒に働く仲間を募集しています

光学文字認識(OCR)をフロントエンドで実装してみました

こんにちは。webチームのジョンです。

今回実装するのはクライアントサイドで走るOCRになります。

始める前にご注意を!

そのため、ライブラリーのパフォーマンスはクライアントのパソコンのスペック次第になります。 クライアント側のスペックがそこそこ必要なため、商用利用は非推奨です。

自分のサイトにまともなOCR機能を実装したい場合はバックエンドで走らせるか(今回使うライブラリーもバックエンドで走らせます)、他のサービス(Amazon TextractやGoogle Vision Apiなど)を使用するかをご検討してください。

光学文字認識 (OCR) とは

光学文字認識 (OCR) は簡単に説明しますとテキストの画像を機械で読み取り可能なテキスト形式に変換するプロセスです。

光学文字認識OCRは決して新しい技術ではありません。現代のスマホにも既にある機能です。iOS15からライブテキストという機能があり、iPhoneで取った写真のテキストを自動的に読み込まれます。スマホのアプリストアからも「ドキュメントスキャナー」などを検索すれば、いろんなOCRアプリが出てきます。

技術スタック

OCRブラウザーのクライアントサイドに走らせるため、Javascriptの知識は必須になります。

パッケージマネージャを使う場合はNodeJsの知識、少しでも必要になります。

さて、今回使うライブラリーはTesseract.jsというライブラリーになります。このライブラリーはTesseract OCRからJavascriptにポートされたライブラリーとなります。Tesseractは元々ヒューレット・パッカード(HP)が開発して2005年にオープンソースされたOCRエンジンです。2006年から2018年まではGoogleがメンテしていました。

インストール方法

インストール方法は2つあります。おすすめはパッケージマネージャを使ってインストールする方法です。でももしパッケージマネジャを使えないもしくはそもそも使っていない既存案件にインストールしたい場合はCDN URLからライブラリーを使えます。

CDN

<script src='https://cdnjs.cloudflare.com/ajax/libs/tesseract.js/3.0.3/tesseract.min.js'></script>

スクリプトタグを埋め込んだ後にTesseractというグローバル変数がアクセスできるようになります。

Tesseract.jsのGithubページでインストール方法のところにバージョン1とバージョン2のインストール方法しか書かれていないため、別のCDNを使いました。他のバージョンを使いたい場合はこちらから参照してください。

パッケージマネジャ

# For v3
npm install tesseract.js
yarn add tesseract.js

# For v2
npm install tesseract.js@2
yarn add tesseract.js@2

パッケージマネジャからインストールする場合はバージョン3がNodeJsバージョン14以上は必要になります。

使用方法

Tesseract.jsのGithubページではTesseract基本の使い方は2つ記載されています。Tesseract.recognize()ファンクションの使い方とウェブワーカーの使い方です。

import Tesseract from 'tesseract.js';

Tesseract.recognize(
    'https://tesseract.projectnaptha.com/img/eng_bw.png',
  'eng',
  { logger: m => console.log(m) }
).then(({ data: { text } }) => {
  console.log(text);
})
import { createWorker } from 'tesseract.js';

const worker = createWorker({
  logger: m => console.log(m)
});

(async () => {
  await worker.load();
  await worker.loadLanguage('eng');
  await worker.initialize('eng');
  const { data: { text } } = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png');
  console.log(text);
  await worker.terminate();
})();

Tesseract.recognize()ファンクションの裏ではワーカーと同じ使い方になっているためどっちを使っても変わりがありません。

Tesseract.js

const createWorker = require('./createWorker');

const recognize = async (image, langs, options) => {
  const worker = createWorker(options);
  await worker.load();
  await worker.loadLanguage(langs);
  await worker.initialize(langs);
  return worker.recognize(image)
    .finally(async () => {
      await worker.terminate();
    });
};

file型のインプットから読み込みの例

<!DOCTYPE html>
<html lang="en">
<body>
    <input type="file" name="my_file" id="my-file">
    <script src="js/from_input.js"></script>
</body>
</html>
import Tesseract from 'tesseract.js';

const readImage = (imageUrl) => {
    Tesseract.recognize(imageUrl, 'jpn', { logger: m => console.log(m) }).then(({ data: { text } }) => {
        console.log(text);
    });
};

(() => {
    const fileInput = document.getElementById('my-file');

    fileInput.addEventListener('change', (e) => {
        const file = e.target.files[0];

        // fileインプットから選択した画像のデータURL発行
        let fileReader = new FileReader();
        fileReader.readAsDataURL(file);
        fileReader.onload = function () {
            readImage(fileReader.result); // 画像のデータURL
        }
    })
})();

日本語縦書きについて

パッケージマネジャを使っている場合は、デフォルトでサポートしている言語を簡単に確認することができます。下記のファイルにご参照して下さい。

node_modules/tesseract.js/src/constants/languages.js

TesseractOCRでは日本語縦書き用のjpn_vertという言語オプションはあります。でも上記のlanguages.jsファイルを確認しますとjpn_vertはありませんがjpn_vertをそのまま使えそうです。

Tesseract.recognize(
    'https://画像/URL/です.png',
  'jpn_vert',
  { logger: m => console.log(m) }
).then(({ data: { text } }) => {
  console.log(text);
})

IndexedDBを確認しますと、ちゃんと.traineddataがダウンロードされているようです。

自分でOCRを学習させたい場合

この記事のスコープ外になりますが、Tesseract.jsにも自分で学習させた.traineddataファイルを使えそうです。こちらにご参照ください。

実際に使ってみましょう!

上記file型のインプットから読み込みの例に書いている通りをCodepenに反映しました。

サンプル画像として簡単にGoogle文章画像をイメージ検索して下記のを取りました。自分が使えたい画像で試してもいいです。

test_image.png

Codepenを開いてConsoleを開いてください

上記のテスト画像を読み込みますと、下記のような結果を得られます。

アウトプットを見ますとほとんど合っていますが、間違っているところは3か所あります。

最後に

TesseractJsはそのまま使用することはできますが、場合によって予想している結果を得られない時もあります。もし、機械学習の経験があるなら、実際に使うデータによってTesseractを学習させた方が一番いいとは思います。

株式会社フォトラクションでは一緒に働く仲間を募集しています

Amazon Athena 使ってみました

こんにちは、PhotoructionでWebエンジニアをしている田村です。

先日ログ検索機能の実装を担当しAWSのプロダクトである Amazon Athena について調査したのでどのようなサービスか簡単に説明したいと思います!

Amazon Athena とは

標準的なSQLAmazon S3に格納したデータを分析することを簡単に行えるサービスです。

AthenaにS3バケットをデータベースとして定義し、テーブルに対してクエリを実行することができます。

前提

実際は、S3バケット作成・ログファイルをS3に転送・IAMロール定義など準備が必要ですが、今回ここでは詳細に説明しませんので別途調べてみてください。

ちなみにIAMユーザーのポリシーは最低限こんな感じでとりあえずAthenaによる検索はできると思います。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "athena:*"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "glue:GetDatabase",
                "glue:GetDatabases",
                "glue:GetTable",
                "glue:GetTables",
                "glue:GetPartition",
                "glue:GetPartitions",
                "glue:BatchGetPartition"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:ListMultipartUploadParts",
                "s3:AbortMultipartUpload"
            ],
            "Resource": [
                "arn:aws:s3:::sample-bucket/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketLocation",
                "s3:ListAllMyBuckets"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "lakeformation:GetDataAccess"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

S3にログファイルを格納

下記のようなディレクトリ構成でログファイルを格納します。

sample-bucket/sample-logs 以下の2022 10 30 31 などは年・月・日を表しています。(詳細は後述)

sample-bucket
└── sample-logs
    └── 2022
        └── 10
            ├── 30
            │   ├── 20221030-1.log
            │   └── 20221030-2.log
            └── 31
                ├── 20221031-1.log
                └── 20221031-2.log

各ログファイルはJSON形式のレコードが1行ずつ蓄積していく形です。

{"log_date":"2022-10-30","item1":"value11","item2":"value21","item3":"value31"}
{"log_date":"2022-10-30","item1":"value12","item2":"value22","item3":"value32"}
{"log_date":"2022-10-30","item1":"value13","item2":"value23","item3":"value33"}

(…以下略)

Athenaでテーブル作成

下記のような定義のテーブルを作成します。

これは”sample_dbs3://sample-bucket/sample-logs/マッピングした samples という名前のテーブルを作成する”というような内容となります。 カラム定義はlog_dateitem3ですがこれはログファイルのレコードとなるJSONの項目と一致します。

CREATE EXTERNAL TABLE sample_db.samples (
    `log_date` STRING,
    `item1` STRING,
    `item2` STRING,
    `item3` STRING
)
PARTITIONED BY (year INT, month INT, day INT)
ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe'
LOCATION 's3://sample-bucket/sample-logs/'
TBLPROPERTIES (
    'projection.enabled' = 'true',
    'projection.year.type' = 'integer',
    'projection.year.range' = '2000,2200',
    'projection.year.digits' = '4',
    'projection.month.type' = 'integer',
    'projection.month.range' = '1,12',
    'projection.month.digits' = '2',
    'projection.day.type' = 'integer',
    'projection.day.range' = '1,31',
    'projection.day.digits' = '2',
    'storage.location.template' = 's3://sample-bucket/sample-logs/${year}/${month}/${day}'
);

PARTITIONED BY TBLPROPERTIES などが気になりますね。 これらはデータのパーティション分割のための定義となります。

Athenaの機能”データのパーティション分割

クエリによってスキャンされるデータの量を制限できるようになるため、パフォーマンスが向上し、コストが削減されます。

実はAthenaはスキャンしたデータ量によって料金がかかってきます。(スキャンされたデータ 1 TB あたり 5.00USD) 上記サンプルログはパッと見た感じでそれほど気になりませんが、Photoructionの操作ログを記録するとなると恐ろしく大量のレコード数になることが予想されます。

そのため、あらかじめ任意のキーでデータをパーティションに分割し、クエリで「この条件でスキャン対象を絞って検索結果を返してね」という記述をしてスキャン量を抑えることができます。

テーブル上の実際のレコード

SELECT * FROM sample_db.samples を実行するとこのような結果が得られます。

テーブル作成時のカラムには指定していなかったyear month day が一緒に返ってきました。

# log_date item1 item2 item3 year month day
1 2022-10-30 00:00:00 value11 value21 value31 2022 10 30
2 2022-10-30 00:00:00 value12 value22 value32 2022 10 30
3 2022-10-30 00:00:00 value13 value23 value33 2022 10 30
4 2022-10-30 00:00:00 value14 value24 value34 2022 10 30
5 2022-10-31 00:00:00 value11 value21 value31 2022 10 31
6 2022-10-31 00:00:00 value12 value22 value32 2022 10 31
7 2022-10-31 00:00:00 value13 value23 value33 2022 10 31
8 2022-10-31 00:00:00 value14 value24 value34 2022 10 31

つまり”PARTITIONED BY” “TBLPROPERTIES” とは

TBLPROPERTIESディレクトリとyear month dayマッピング(と併せて型や範囲も)定義していることになります。 sample-bucket/sample-logs/2022/10/30/xxxxxx.log の場合は、データとしてyear=2022 month=10 day=30 となります。

またPARTITIONED BY によって year month dayパーティション分割するということになります。

というわけでSQLSELECT * FROM sample_db.samples WHERE year = 2022 AND month = 10 day = 30 のようにすると、実際にはsample-bucket/sample-logs/2022/10/30/ 以下のファイルのみスキャン対象とし、それ以外のディレクトリはスキャンしないため、その分スキャン量を抑えられる(=サービスの利用料金を抑えることができる)ということになります。

実装サンプル

それでは実際にAWSSDKPHP版)を使用してAthenaを検索してみます。

前提:composerでaws/aws-sdk-php3.x系 を利用

// ①クエリ
$query = 'SELECT * FROM sample_db.samples WHERE year = 2022 AND month = 10 AND day = 30';

// ②Athenaクライアント
$athenaClient = new Aws\Athena\AthenaClient([
    'region' => 'ap-northeast-1',
    'version' => 'latest',
    'credentials' => [
        'key' => 'SAMPLE_AWS_ACCESS_KEY_ID',
        'secret' => 'SAMPLE_AWS_SECRET_ACCESS_KEY',
    ],
]);

// ③クエリ実行
$startQueryResponse = $athenaClient->startQueryExecution([
    'QueryString' => $query,
    'ResultConfiguration' => [
        'OutputLocation' => 's3://sample-bucket/sample-results'
    ]
]);

// ④QueryExecutionId取得
$queryExecutionId = $startQueryResponse->get('QueryExecutionId');

for ($times=0; $times < 20; $times++) {
    // ⑤QueryExecutionIdを元に実行ステータスを取得
    $responseExecution = $athenaClient->getQueryExecution([
        'QueryExecutionId' => $queryExecutionId
    ]);
    $status = $responseExecution->get('QueryExecution')['Status']['State'];

    // ⑥ステータスが「QUEUED」(受付済み)、「RUNNING」(実行中)だったら1秒待って繰り返す
    if (in_array($status, ['QUEUED', 'RUNNING'])) {
        sleep(1);
        continue;
    }

    // ⑦ステータスが「SUCCEEDED」(成功)だったら結果を取得してみる
    if ($status === 'SUCCEEDED') {
        // ⑧QueryExecutionIdを元に結果セットを取得
        $responseResults = $athenaClient->getQueryResults([
            'QueryExecutionId' => $queryExecutionId
        ]);
        $resultSet = $responseResults->get('ResultSet');

        // meta情報
        $meta = $resultSet['ResultSetMetadata']['ColumnInfo'];

        // 検索結果
        $rows = $resultSet['Rows'];
    }

    break;
}

AthenaをSDKを使用して検索する場合、非同期処理で行われるので、実行後に状態を監視して完了するのを待ってから結果を得る必要があります。 よって、

  1. SDKクライアントの初期化(②)
  2. クエリ実行し、結果取得用キー取得(③④)
  3. 結果取得用キーを使ってステータスをチェック(⑤)
    1. 「実行中」なら少し待って再度ステータスチェック(⑥)
    2. 「成功」なら結果取得用キーを使用して検索結果を取得する(⑦⑧)

というような流れになります。

また、③のOutputLocationでログファイルを格納しているディレクトリとは別のS3ディレクトリを指定していますが、Athenaはクエリを実行して成功すると自動で結果セットのCSVをこのディレクトリに出力します。 ですので、例えば検索結果をフロントエンドで確認しつつクエリ結果CSVをダウンロードする機能なども容易に実装することが可能です。

最後に

いかがでしょうか?

今回ご紹介した機能はAthenaのごく一部の機能となり、実際には非常に多くの機能があります。 当然ですが実際にはAthena以外の箇所でログの出力方法・出力先、S3に転送するタイミングや手段などしっかり設計する必要があります。

いろいろと便利なサービスやツールを組み合わせて実装する機会はこれからも多々あるかと思います。 それらをうまく利用して作業時間の短縮やコストの削減を図り、本来時間をかけたい部分に注力してより良いプロダクトを作っていきたいと思います💪

エンジニアカンファレンスのスポンサードについて

Androidエンジニアの久木田です。

僕は本職でAndroidエンジニアをしながら、エンジニア採用に関する業務や今回の主題であるエンジニアカンファレンスのスポンサードの窓口などもやっています。

スポンサードしたいと思っているカンファレンス

正直、無限にスポンサードしてエンジニアみんなが知っているようになれば最高ですが、予算という制約があり、投資対効果という点を重視してスポンサードするカンファレンスを選んでいる状況です。

ですので、Photoructionで採用している技術が主題になっているカンファレンスにスポンサードする方針を取っています。

具体的にどういう技術かというと

についてです。

これらに関するカンファレンスで、投資対効果が妥当だと判断したものには積極的にスポンサードしていきたいと思っています。

これまでにスポンサードしたカンファレンス

時系列順にこれまでスポンサードしたカンファレンスを上げると

です。

「あれ?あのカンファレンスなくね?」っていうのがあると思いますが、いくつか僕のミスで申し込みが出来なかったものがあります。。。

来年はリベンジします🔥

なぜスポンサードするのか

多くの企業と同じく究極も目的はエンジニア採用です。

なのでスポンサードをすることでエンジニアのみなさんにフォトラクションの名前やロゴについて認識してもらい、あわよくば事業内容も知ってもらえると嬉しいなと思っています。

まだまだ始めたばかりなので成果は全くないですが、これから数年続けることで採用にもいい結果が返ってくるようになってほしいです。もちろんそのために入りたいと思える魅力ある開発部隊にしていきたいと思います。

目指したい姿

エンジニアならみんなPhotoruction知ってるよねという状態になればいいなと思っています。

そのためにも、今できてないあれやこれやをできるように色々やっていきたいと思っています。

とりあえず、今はノベルティグッズすら用意できてないのでそこからですね。

あと、他社のスポンサードの告知記事だとよく「弊社からは〇〇が登壇します」というのがあるので、そういうのも出来るように社内から登壇者が出てくるようにしたいなと思っています。

登壇に関しては、意欲のあるひとがいればスポンサーセッションを申し込むこともできるので興味のある人は是非お声掛けください。カジュアルにお話しだけでも!

最後に

これからもいろんなカンファレンスに(予算が許す限り)スポンサードしていきたいと思っています。

もし、上記の技術に関するカンファレンスを主催されている方で、スポンサードして欲しいとかあれば、お声掛けいただけると検討いたしますのでお気軽にお声掛けください。(スポンサード募集の情報とか意外と見つけづらいので)

株式会社フォトラクションでは一緒に働く仲間を募集しています

corporate.photoruction.com www.wantedly.com

ドキュメント配信のインフラリソースをCloudFormationでコード化した

はじめに

こんにちは、エンジニアの酒井です。

以前、チームで頻繁に使用する関数やメソッドを共通ライブラリとして開発効率を向上させる取り組みを行い、ライブラリのドキュメントはS3とCloudFrontを用いた構成で配信することにしました。

その際、今後も同じような静的コンテンツの配信をするときにすぐにこの構成を再現できるようCloudFormationでインフラリソースをコード化しました。

今回はその際のテンプレートの内容を記述したいと思います。

構成

レスポンス速度の向上のため、S3だけではなく、CloudFrontを間に設置し、エッジロケーション経由でコンテンツを配信する。

方法

  • 全体のソースコード

      AWSTemplateFormatVersion: 2010-09-09
      Description: Static contents distribution using S3 and CloudFront.
    
      Resources:
        # S3 バケット
        MyBucket:
          Type: AWS::S3::Bucket
          Properties:
            BucketName: "sample-bucket"
    
    
        # S3 バケットポリシー
        MyBucketPolicy:
          Type: AWS::S3::BucketPolicy
          Properties:
            Bucket: !Ref MyBucket
            PolicyDocument:
              Statement:
                - Action: s3:GetObject
                  Effect: Allow
                  Resource: !Sub arn:aws:s3:::${MyBucket}/*
                  Principal:
                    AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}
    
    
        # CloudFront ディストリビューション
        CloudfrontDistribution:
          Type: AWS::CloudFront::Distribution
          Properties:
            DistributionConfig:
              Origins:
                - Id: S3Origin
                  DomainName: !GetAtt MyBucket.RegionalDomainName
                  S3OriginConfig:
                    OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
              Enabled: true
              DefaultRootObject: index.html
              Comment: !Sub ${AWS::StackName} distribution
              DefaultCacheBehavior:
                TargetOriginId: S3Origin
                ForwardedValues:
                  QueryString: false
                ViewerProtocolPolicy: redirect-to-https
    
        # CloudFront OAI
        CloudFrontOriginAccessIdentity:
          Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
          Properties:
            CloudFrontOriginAccessIdentityConfig:
              Comment: !Ref AWS::StackName
    

S3バケットの設定

Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: "sample-bucket"

CloudFront ディストリビューションの設定

  • DistributionConfigディストリビューションの設定をしていきます。
  • Originsでコンテンツを配信する元となる場所(サービス)を指定します。
    • 今回はS3なのでそれぞれ設定します。
      • Idは任意の名前を記述します。
      • DomainNameでは先ほど設定したMyBucketのRegionalDomainNameを指定します。
      • S3OriginConfigのOriginAccessIdentityで次に作成するOrigin Access Identityを指定します。
  • Enabledディストリビューションを有効にするか無効にするかを指定します。
  • DefaultRootObjectディストリビューションのルートURLを要求したときに、CloudFrontがオリジンから要求したいオブジェクトを指定します。
  • Commentディストリビューションの説明を記述します。
  • DefaultCacheBehaviorでキャッシュの動作を指定します。
    • TargetOriginIdで先ほど指定したOriginのIdを指定します。
    • ForwardedValues は非推奨ですが、指定が簡単なので使用しました。
      • QueryString: false によってクエリ文字列が異なっても同じコンテンツとみなしてキャッシュします。
    • ViewerProtocolPolicyはPathPattern のパスパターンに合致する要求があった場合に、TargetOriginId で指定されたオリジンのファイルにアクセスするために視聴者が使用できるプロトコルを指定します。
      • 今回は redirect-to-https として「ビューアー(クライアント)がHTTPリクエストを送信すると、CloudFrontはHTTPステータスコード301(Moved Permanently)をHTTPS URLとともにビューアーに返します。
        • クライアントはその後、新しいURLを使用してリクエストを再送信します。
Resources:
  CloudfrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - Id: S3Origin
            # DomainName: !GetAtt MyBucket.DomainName
            DomainName: !GetAtt MyBucket.RegionalDomainName
            S3OriginConfig:
              OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
        Enabled: true
        DefaultRootObject: index.html
        Comment: !Sub ${AWS::StackName} distribution
        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          ForwardedValues:
            QueryString: false
          ViewerProtocolPolicy: redirect-to-https

OriginAccessIdentityの設定

Resources:
  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Ref AWS::StackName

S3バケットポリシーの設定

  • PropertiesBucket で先ほど設定したバケットの論理IDを指定します。
  • PolycyDocumentStatement でポリシーの内容を記述します。
    • Principal:誰が
    • Resource:どのサービスどのリソースに対して
    • Action:どの操作を
    • Effect:許可(Allow) or 禁止(Deny)する
    • 記述内容以下のようになります。
      • 「CloudFrontのOrigin Access Identity(ユーザー)」が「MyBucket(先ほど設定したバケット)の全てのオブジェクト」に対して「オブジェクトの読み込み操作」を行うことを「許可」する
Resources:
  MyBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref MyBucket
      PolicyDocument:
        Statement:
          - Action: s3:GetObject
            Effect: Allow
            Resource: !Sub arn:aws:s3:::${MyBucket}/*
            Principal:
              AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}

さいごに

インフラをコードベースで管理することでインフラリソースの状態を把握できますし、今後同じ構成で何かを開発する際はインフラの構築時間を削減できると感じました。

今後も業務の中で人が行う必要がない部分は積極的に自動化して、自分を含めチームメンバーが本質的な部分に力を注げるよう行動していきたいなと思います!

株式会社フォトラクションでは一緒に働く仲間を募集しています

corporate.photoruction.com www.wantedly.com

web hookを利用したAPIのテストにngrok(pyengrok)を使いローカルサーバーにレスポンスを送る

はじめに

こんにちは、AIエンジニアの山下です。

これまではweb hookを利用したAPIの解析結果を確認・テストするのにPostmanwebhook.siteを使用していました。

少し確認する程度ならこれでもよかったのですが、何度もテストを行いたい場合などは毎回ブラウザーを開いて確認するとなると結構面倒。

そこで、python+Flask(Webアプリケーションフレームワーク)で簡易的なローカルサーバーを構築し、ngrokというツール(ローカルサーバーと外部ネットワークを繋ぐツール)を使用し、ブラウザーやターミナルを開かず、jupyter notebook内でAPIへのリクエストの送信からローカルサーバーでのレスポンスのデータの保存までできることを目指しました。

(*ネットワークやwebアプリケーションに慣れていないので細かい部分で認識が間違っていたらすみませんmm)

*OSはMac

前提として

  • ngrokにユーザー登録が済んでいる
  • 今回想定しているのはwebAPIにリクエストを送り、そのレスポンス(解析結果)をweb hookとして送る(指定したURLにPOSTリクエストする)という構成
  • jupyter notebookでpythonモジュール(app.py)を読み込んだ上でリクエスト送信→APIからwebhookをローカルサーバーに送る→ローカルサーバーでレスポンス内容をjsonに変換したものを保存するまでの処理を行いたい
  • ngrokにはngrokコマンドをインストールして使えるが、処理をjupyter notebook内で完結させたいため、Pythonラッパーのpyngrokというライブラリーを使用
  • python内で”pip install flask pyengrok”が事前に必要

流れ

  1. 作成したpythonファイル(モジュール)とその説明
  2. 1.を読み込むjupyter notebookの説明

app.py

  • 作成したモジュールです、この後に内容を説明していきます。
import json
import logging
import os
import threading
from logging import config

from flask import Flask, jsonify, request
from pyngrok import ngrok
from werkzeug.serving import run_simple

from config.logging_config import LOGGING_CONFIG

class CreateApp():
    ana_js = None

    def __init__(self, on_colab: bool = False) -> None:
        os.environ["FLASK_DEBUG"] = "development"

        # Open a ngrok tunnel to the HTTP server
        port = 8888  # ポート番号
        public_url = ngrok.connect(port).public_url
        self.public_url = public_url.replace('http', 'https')  # httpsに変換
        print(" * ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}\"".format(
            self.public_url, port))

        app = Flask(__name__)
        app.debug = True
        app.config["SECRET_KEY"] = "secret key"  # 要変更
        # Update any base URLs to use the public ngrok URL
        app.config["BASE_URL"] = self.public_url
        app.config["ENV"] = "development"
        app.config['JSON_AS_ASCII'] = False  # JSON日本語文字化け対策
        # .. Update inbound traffic via APIs to use the public-facing ngrok URL

        # logging設定
        config.dictConfig(LOGGING_CONFIG)
        logger = logging.getLogger()

        # Start the Flask server in a new thread
        if on_colab is False:
            threading.Thread(
                target=lambda: run_simple('localhost', port, app)).start()
        else:
            # on Colab, you can use the following code instead of the above
            # line
            threading.Thread(
                target=app.run, kwargs={"use_reloader": False}).start()

        # Define Flask routes
        @app.route("/", methods=['GET'])
        def index():
            return "Web App with Python Flask!"

        @app.route('/receiver', methods=['POST'])
        def get_json():
            js = request.get_json()
            if js is not None:  # JSONが送られてきた場合、DB & ローカルに保存
                logger.error('ERROR')
                self.ana_js = js  # class変数に保存
                with open(f'./result/{js["metadata"]["pdf_name"]}.json',
                          'w') as f:
                    json.dump(js, f, indent=4)
            return jsonify(js)

    def receive(self) -> dict:
        while True:
            if self.ana_js is not None:
                return self.ana_js
            else:
                continue

pyngrokを使用し、HTTPサーバーを開く

  • port は何でも良さそうでdefaultだと5000になります
  • public_urlがngrokが作成したトンネルのURLになります
# Open a ngrok tunnel to the HTTP server
port = 8888  # ポート番号
public_url = ngrok.connect(port).public_url
self.public_url = public_url.replace('http', 'https')  # httpsに変換
print(" * ngrok tunnel \"{}\" -> \"http://127.0.0.1:{}\"".format(
    self.public_url, port))

Flask側の設定

  • app.config[”BASE_URL”]に上記の”public_url”を指定する
  • loggingも設定しましたが、本筋ではないので詳細は省きます
app = Flask(__name__)
app.debug = True
app.config["SECRET_KEY"] = "secret key"  # 要変更
# Update any base URLs to use the public ngrok URL
app.config["BASE_URL"] = self.public_url
app.config["ENV"] = "development"
app.config['JSON_AS_ASCII'] = False  # JSON日本語文字化け対策
# .. Update inbound traffic via APIs to use the public-facing ngrok URL

# logging設定
config.dictConfig(LOGGING_CONFIG)
logger = logging.getLogger()

python HTTPの設定

  • このあたりちゃんと理解できていませんが二つの処理を同時に実行させるために並列処理を行いFlaskサーバーをスレッドを使用し、実行しているようです
# Start the Flask server in a new thread
if on_colab is False:
    threading.Thread(
        target=lambda: run_simple('localhost', port, app)).start()
else:
    # on Colab, you can use the following code instead of the above
    # line
    threading.Thread(
        target=app.run, kwargs={"use_reloader": False}).start()

Flaskの処理とルーティング

  • こちらなくても良いのですが、サーバーが起動しているかどうかをブラウザーlocalhost:8888にアクセスし確認するために処理を入れました。
# Define Flask routes
@app.route("/", methods=['GET'])
def index():
    return "Web App with Python Flask!"
  • こちら、ローカルサーバーで受け取ったレスポンスをjsonに変換し、ローカルの指定したディレクトリに保存する処理になります。(また、そのjsonをクラス変数に代入する処理も行ってます、この変数に代入されたデータは後ほど使います)
@app.route('/receiver', methods=['POST'])
def get_json():
    js = request.get_json()
    if js is not None:  # JSONが送られてきた場合、DB & ローカルに保存
        logger.error('ERROR')
        self.ana_js = js  # class変数に保存
        with open(f'./result/{js["metadata"]["pdf_name"]}.json',
                  'w') as f:
            json.dump(js, f, indent=4)
    return jsonify(js)

クラス内関数を定義

  • APIにリクエストを送り、レスポンスが返ってくるまでにタイムラグがあり、ローカルサーバーにレスポンスが送られるまで実行し続ける関数になります
  • 何のための関数か分かりづらいかもしれませんが、後のjupyter notebook内の全ての処理(セル)を一度の実行で行いため実装しています
def receive(self) -> dict:
    while True:
        if self.ana_js is not None:
            return self.ana_js
        else:
            continue

JupyterNotebook

  • 1番目のセル
    • 必要なライブラリを読み込んでます(app→先に作成したモジュール)

  • 2番目のセル

    • 1行目:ローカルサーバーの起動
    • 2行目:public_urlの取得(パブリックngrokURL)
    • 3行目:アクティブなトンネルをリストで取得

  • 3番目のセル

    • 各パラメーターを指定し、APIにPOSTリクエストし、APIの処理が実行されローカルサーバーにweb hookを送るまでの処理

    *url, api_key, pdf_path, dataの内容はダミー

  • 4番目のセル

    • app.pyのCreateAppクラス内関数receiveを実行
    • このセルが実行完了(ローカルサーバーがレスポンスを受信、かつ、レスポンスjsonの保存)次第、次のセルが実行される

    *この処理を入れずに次のセルを実行するとローカルサーバーがレスポンスを受信する前にトンネルを閉じてしまう場合がある

  • 5番目のセル

    • 開いているトンネル閉じる

    *jupyter notebookを再起動するだけでトンネルは閉じられる

jupyter notebook コード

import json

import requests
from pyngrok import ngrok

from app import CreateApp
# local server を起動
ca = CreateApp()
public_url = ca.public_url
tunnels = ngrok.get_tunnels()
# APIにデータを送信
url = 'https://OOOO.com/OOO' # ダミー
api_key = 'api_key'. # ダミー
webhook_url = public_url + '/receiver'

pdf_path = 'OOOO/OOOO/OOO.pdf'
metadata = {'pdf_name': '0000.pdf'}
# postする
data = {
    'data:': 'data', webhook_url: 'webhook_url', metadata: {'hoge': 'test'}}
js_str = json.dumps(data).encode('utf-8')
response_local = requests.post(
    url, data=js_str, headers={'x-api-key': api_key})
print(f'response_local: {response_local}')
ca.receive().keys()
print(f'Before tunnels:\n{ngrok.get_tunnels()}')
# Close all open ngrok tunnels
_ = [ngrok.disconnect(tnnl.public_url) for tnnl in tunnels]
print(f'After tunnels:\n{ngrok.get_tunnels()}')

最後に

webやネットワークに慣れてないこともあり、言語化するのに苦労しました。

(*今回のものはあくまで、開発テスト確認用になります)

次はFlaskのローカルサーバーにログイン機能を実装してみたいと思います。

株式会社フォトラクションでは一緒に働く仲間を募集しています

corporate.photoruction.com www.wantedly.com

エンジニアのファーストキャリアをCREチームで働いてみて感じたこと。

CREチームでWEBエンジニアをしている桑畑です。 WEBエンジニアをするまでは、マーケティング、データアナリストなどをしてきました。 個人開発でのWebアプリケーション開発経験はありましたが、職業としてWEBエンジニアをさせて頂いたのはフォトラクションのCREチームが初めてです。

エンジニアファーストキャリアをCREチームでスタートした感想を述べたいと思います。

CREチームとは


まずは、フォトラクションのCREチームについて紹介します。

ミッション

CREチームのミッションは、顧客が抱く自社サービスへの技術的な不安や不満を解消してカスタマーサクセスを実現していくことです。

日々多くのお客様がサービスをご利用頂く中で、様々なトラブルや要望の声を頂きます。そういったお客様の声に対して、技術的な側面からサポートしております。サービスをより快適に、安心してご利用頂けるような環境を作っていくことがテーマです。

チーム構成

現在は、WEBエンジニアが4名在籍

技術要件

フロントエンド、バックエンド、インフラ、データベースなど

CREチームで働くメリット


CREチームで働くメリットを紹介していきます。

幅広い技術領域に挑戦できる

前述の技術要件でも紹介しましたが、幅の広さが一番の魅力だと感じています。トラブルや要望は様々な技術レイヤーで頂きます。そのレイヤーに応じて、必要となるスキルが異なります。スキルが弱い部分でも要件に応じて、知識をキャッチアップして問題を解決していく過程で、幅広い技術領域への経験が積める環境だと思います。

企業によってはエンジニアの役割が細分化されているところもあるかと思います。専門性を伸ばしていく上では非常に大事だと思いますが、それと同時に、大局的にサービスと関わることによって様々な技術レイヤーを横断したシステム全体の理解に繋がるかと思っております。

不具合時の問題切り分けに能力がつく

緊急な不具合の対応などもCREチームが請け負うことが多いです。不具合のときに初めて触る機能やコードもたくさんあります。そういった場合に、落ち着いて問題の処理を特定することが求められます。現時点で出来ているか分かりませんが、求められる状況はたくさんありますので鍛えられていると思います。不具合時に限らず、問題の切り分け能力は大事だなと感じています。

プロダクトへの評価を目近で聞こえる

フォトラクションでは、お客様との接点はサポートやフィールドセールスのチームが担っています。ただし、開発組織におりながら、トラブルや要望といった声を目近で聞くことが出来るのもCREチームの魅了だと思います。

開発組織とビジネス組織が縦割りで情報共有が乏しい状態は不健全な状態かと思います。CREチームでは、プロダクト改善の目線を持ちながら開発に深く関わることが出来ます。

他部署/チームとの関わり


CREチームでは様々な部署の方との関わりがあります。技術理解を深める上でも、サービスに関わった開発をしていく上でも、多くの組織との関わりは魅力かもしれません。

ビジネス組織との関わり

コミュニケーション接点が1番多いのはサポートチームの皆さんです。フォトラクションでは、サービス利用のお客様からの声をサポートチームが集約し、快適に安心して利用頂けるよう努めています。

サポートチームからは緊急対応の依頼や、調査、開発ロードマップに載っていないが改善したいことなど、様々なオーダーや提案を頂きます。そういった声からCREチームでは開発やトラブル対応などをしていきます。

開発組織の他チームとの関わり

フォトラクションでは、CREチーム以外にも目的別に複数のチームが構成されております。他チームの方々との連携も日々あります。

WEBエンジニア全員が集う定例ミーティング・開発レビュー・開発/技術相談など、オンラインコミュニケーションを中心にして、チームの垣根無くコミュニケーションしております。

まとめ


フォトラクションの開発組織やCREチームで働く魅力、エンジニアファーストキャリアとしてのCREチームで働くメリットなどを感じて頂けましたでしょうか。

課題もまだまだ沢山ありますが、それらも含めて楽しめるエンジニアの仲間を募集しております。少しでも興味を持って頂けた方は連絡頂けると嬉しいです。

最後まで読んで頂き、ありがとうございました。

 

株式会社フォトラクションでは一緒に働く仲間を募集しています