EatSmartシステム部ブログ

ウェブサイトの開発や運営に関する情報です。

Dockerコンテナが利用するDNSを指定する

先日ステージング環境が稼働するサーバの障害が発生したため、再構築を行いました。 以前は複数台に分かれていたものを、リソースに余裕のある1台に集約しました。 構築自体は本番環境と同様にansibleで実行したので、特に問題無く終わりました。

作業が終わり、担当するサービスをリリースするため、ステージング環境へデプロイを行った際に問題が出ました。 デプロイはコンテナ化したJenkinsから行うのですが、デプロイ先のサーバの名前が解決出来ません。 慌ててステージング環境が稼働するサーバでnslookupを行いましたが、問題無く解決出来ます。 そこでJenkinsに入り同様にnslookupを行ったところ、名前が解決出来ませんでした。 他のコンテナがどうなのか調べた限りでは、どうやらalpineをもとに作成したコンテナで同様の現象が発生しました。 このときは、とりあえず/etc/hostsへ記入することでしのぎました。

デプロイはこれで問題無く行うことが出来る用になりましたが、今度はアプリケーションからデータベースへ接続出来ない問題が発生しました。 このアプリケーションもalpineをもとに作成したコンテナで、データベースの名前が解決出来ていませんでした。 サービスで利用するコンテナのイメージを変更する場合、テストを行う必要があります。 このため、コンテナが利用するDNSを指定することにしました。

docs.docker.jp

まずは、起動するときにdnsオプションを指定しました。 実行すると、/etc/resolv.confに期待したサーバが指定されるようになりました。

docs.docker.jp

他のコンテナも同様の設定を反映するため、Dockerの設定を変更することにしました。 この設定は、log-driverを指定するためansibleで設定を行っていました。 そこで、log-driverの指定に加えdnsの指定を追加することにしました。

さくらインターネットのオブジェクトストレージを利用した動画配信

イートスマートが運営する料理教室情報サイト「クスパ」では、昨年からレッスン動画のサービスを開始しました。

cookingschool.jp パン・お菓子・料理の動画レッスン|料理教室検索サイト「クスパ」

サービスの成長に伴い、当初用意したストレージの残量がわずかになり、対策を取る必要に迫られました。 イートスマートではさくらインターネットのサービスを利用しているので、容量の追加が容易で安価なオブジェクトストレージを利用することにしました。

cloud.sakura.ad.jp

オブジェクトストレージを利用するための準備

まず、videoというバケットを作成し、アクセスキーIDとシークレットアクセスキーを準備します。

次に、aws-cliを動かすための準備を行います。 今回は、videoへアップロードするためのプロファイルを作成して利用することにしました。

aws configure --profile user-video

動画ファイルの保存方法の変更

いままでは、動画配信に利用しているサーバのローカルディスクに保存していました。 これを、オブジェクトストレージへアップロードするように変更します。

既存の動画ファイルを変換する処理の最後に、オブジェクトストレージへアップロードする処理を追加しました。 aws-cliを利用し、エンドポイントにオブジェクトストレージのものを指定します。 アップロードする対象のディレクトリを${DIR}変数で指定し、ffmpegを利用して変換した成果物の ts ファイルと m3u8 ファイルを s3 sync コマンドを利用してアップロードします。

aws --endpoint-url=https://s3.isk01.sakurastorage.jp s3 sync /var/video/${DIR} s3://video/${DIR} --include "*.ts" --include "*.m3u8"  --profile user-video

動画ファイルの配信方法の変更

配信にはnginxを利用しています。 オブジェクトストレージから動画を配信するには以下の記事を参考にしました。

さくらのオブジェクトストレージを独自のドメイン経由且つプライベートなオブジェクトを取得する方法 - Qiita

既存のnginxへluaを組み込むのではなく、新たに用意したnginxをオブジェクトストレージとの間でプロキシとして構築することにしました。 既存のnginxの設定を変更し、参照先をローカルディスクではなくプロキシへ変更します。

#root   /var/video;
proxy_pass http://${HOST_IP_ADDRESS}:8080;

以上で、オブジェクトストレージを利用した動画配信を実現することが出来ました。 AWS S3と互換があり、APIから操作することができるため容易に利用することが出来ました。

Github Actions でブランチのワークフローを手動で実行する

いま作業しているプロジェクトで、ブランチのワークフローを手動で実行する必要があったので、方法を調べました。

ワークフローを作成する

まず、ブラウザ上からワークフローを作成します。 仮に"test"という名前のワークフローとします。 この時点で、メインブランチの.github以下にtest.ymlというファイルが作成されます。

ブランチに同一名称のワークフローを作成する

続いて、リリースしたいブランチ("branch"とします)に同一名のファイルを作成します。 内容は、メインブランチからコピーしたものに、以下の修正を加えます。

# Controls when the workflow will run
on:
  workflow_dispatch:

あとは、必要に応じてステージで実行する処理を記述します。

ブランチのワークフローを実行する

Actionsタブから実行するワークフローを選択すると、"Run workflow"というボタンが追加されています。 これをクリックすると、ブランチの選択肢が表示されるので、branchブランチを選択してボタンを押します。

GithubActions

以上で、branchブランチの内容でワークフローが実行されます。

印刷用CSSでの改ページ指定

弊社サービスの管理機能で、お客様向けのレポートをPDF出力する機能があるのですが、レポートの内容をHTMLで組み立て、CSSで印刷用のレイアウトを指定して出力をしています。

基本的にはtableタグで表を組み立てているのですが、PDFで出力した際に表の項目単位でページを跨がないようするために、以下のstyleを指定して改ページされないようにしました。

tr {
    page-break-inside: avoid;
}

また、このレポートは1件ずつtableを組んだものが複数件あるのですが、tableごとに新しいページから開始するように、以下のstyle指定でtableごとの改ページを行いました。

table {
    page-break-before: always;
}

参考になれば!

subqueryで複数項目を取得したい場合(PostgreSQLでLATERALを使う)

SQLで複雑なクエリを作っていると、メイン・テーブルの1行に対して、サブ・テーブルの集計した値を使用したい場合に、

SELECT
  main.id,
  main.name,
  AVG(sub.value) as average_value
FROM main_table main
INNER JOIN sub_table sub
ON main.id = sub.id
GROUP BY
  main.id,
  main.name

などと書いたりすると思います。

このときに、メイン・テーブルから取ってくる項目が多い場合

-- SQL1
SELECT
  main.id,
  main.name,
  main.column1,
  main.column2,
  main.column3,
  main.column4,
  main.column5,
  main.column6,
  main.column7,
  main.column8,
  main.column9,
  AVG(sub.value) as average_value
FROM main_table main
INNER JOIN sub_table sub
ON main.id = sub.id
GROUP BY
  main.id,
  main.name,
  main.column1,
  main.column2,
  main.column3,
  main.column4,
  main.column5,
  main.column6,
  main.column7,
  main.column8,
  main.column9

など、集計関数があるせいでGROUP BY項目が多くなってしまい、またSELECT項目を増やすたびにGROUP BY項目を追加しないといけなくなります。

それを嫌がり

SELECT
  main.id,
  main.name,
  (
    SELECT AVG(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as average_value
FROM main_table main

とSELECT句のsubqueryとして書いた場合、集計項目が多くなると、

-- SQL2
SELECT
  main.id,
  main.name,
  (
    SELECT AVG(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as average_value,
  (
    SELECT MAX(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as max_value,
  (
    SELECT MIN(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as min_value,
  (
    SELECT SUM(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as sum_value,
  (
    SELECT COUNT(sub.value) 
    FROM sub_table sub 
    WHERE main.id = sub.id
  ) as count_value
FROM main_table main

のように、SELECT句の見通しがとても悪くなります。 こんなとき、LATERAL句(PostgreSQL9.3以上)を使うと見通しの良いSQLを作ることができます。

LATERAL句はテーブル(サブクエリ)のJOINとして働くのですが、結合対象のサブクエリ内で結合元の値を指定することができ、その値が遅延評価されます。(文章だけ見ても、意味が分からない…)

具体的には、

SELECT
  main.id,
  main.name,
  sub.average_value,
  sub.max_value,
  sub.min_value,
  sub.sum_value,
  sub.count_value
FROM main_table main
LEFT JOIN LATERAL (
  SELECT
    AVG(sub.value) as average_value,
    MAX(sub.value) as max_value,
    MIN(sub.value) as min_value,
    SUM(sub.value) as sum_value,
    COUNT(sub.value) as count_value
  FROM sub_table sub 
  WHERE main.id = sub.id
) as sub
ON TRUE

というSQLで、サブクエリ内のWHERE main.id = sub.idが、メイン・テーブルの1行ごとに評価され、集計関数の値を取得することができます。実行計画的にはSQL2と同等になるようです。

この程度であれば

SELECT
  main.id,
  main.name,
  sub.average_value,
  sub.max_value,
  sub.min_value,
  sub.sum_value,
  sub.count_value
FROM main_table main
LEFT OUTER JOIN (
  SELECT
    sub.id,
    AVG(sub.value) as average_value,
    MAX(sub.value) as max_value,
    MIN(sub.value) as min_value,
    SUM(sub.value) as sum_value,
    COUNT(sub.value) as count_value
  FROM sub_table sub 
  GROUP BY sub.id
) as sub
ON main.id = sub.id

と、サブ・テーブルをまるっとidのGROUP BYで集計してしまうこともできますが、もう少し複雑なSQLでも見通しを良くしようと思ったら、LATERALが有用なことがあると思います。

AWS Lambda で非同期に処理を実行する

先日、AWSを利用した開発を経験することが出来ました。

Lambda関数を利用してAPIを実装していますが、時間のかかる処理を分離する必要がでてきました。 どのような方法があるのか調査したところ、方法を2つ知ることが出来たので、書き残したいと思います。

Amazon SQS を利用する

Amazon SQS とは、メッセージキューイングサービスです。 処理したい内容をキューへ登録しておき、「Lambdaトリガ」へ処理を実行するLambda関数を指定します。

APIは以下のように、SQSへメッセージを送信します。

const AWS = require('aws-sdk');
AWS.config.update({region: 'REGION'});
const SQS = new AWS.SQS();
const MessageBody = JSON.stringify([{"key": "A"}, {"key": "B"}])
const QueueUrl = 'https://sqs.ap-northeast-1.amazonaws.com/xxxx/yyyy';
const result = await SQS.sendMessage({ MessageBody, QueueUrl }).promise()

トリガに指定されたLambda関数は、以下のようにeventからメッセージを取り出すことが出来ます。

exports.handler = async (event) => {
  const datas = [];
  for (let i = 0; i < event.Records.length; i++) {
    const record = event.Records[i];
    let json = JSON.parse(record.body);
    for (let j = 0; j < json.length; j++) {
      datas.push(json[j]);   
    }
  }
  console.log('datas->', datas);
  ....
};

Lambda関数を非同期実行する

Lambda関数からLambda関数を呼び出すことが出来ます。 今回は非同期で処理したいので、InvocationTypeに"Event"を指定しました。

const AWS = require('aws-sdk');
const lambda = new AWS.Lambda();
const params = {
  FunctionName: "[非同期実行するLambda関数名]",
  InvocationType: "Event",
  Payload: JSON.stringify([{"key": "A"}, {"key": "B"}])
}
await lambda.invoke(params).promise();

非同期で実行されたLambda関数は、eventからPayloadで指定した値を取り出すことが出来ます。

exports.handler = async (event) => {
  const datas = [];
  for (let i = 0; i < event.length; i++) {
    datas.push(event[i]);
  }
  console.log('datas->', datas);
  ....
};

VPCを設定したLambda関数で実行する方法

いずれの方法も、VPCを設定したLambda関数で実行するとタイムアウトが発生します。 その場合、VPCエンドポイントを利用することで、実行することができるようになります。

エンドポイントの作成から、SQS用はサービス名に「com.amazonaws.ap-northeast-1.sqs」を、Lambda関数用はサービス名に「com.amazonaws.ap-northeast-1.sqs」を指定し、必要なサブネットの設定を行うことで利用出来ました。

API Gateway + Lambda でAPIの関数をステージごとに切り替える

API Gateway を利用して API を実装するなかで、呼び出すLambda関数をステージごとに切り替える必要が出てきました。 /example というリソースに対して、本番環境とステージング環境で別の関数を呼び出せるようにします。

関数の作成

本番環境用と、ステージング環境用でLambda関数を作成します。

環境 関数名
本番環境 prd-example-function
ステージング環境 stg-example-function

環境ごとにLambda関数を分けることで、開発中のものをリリースしたり、環境に合わせた環境変数を設定することが可能になります。

ステージの設定

ステージは、本番環境用に production と、ステージ環境用に staging を作ります。 ステージごとに変数を定義して、呼び出すLambda関数を切り替えられるようにします。 "ステージ変数"タブから、"ステージ変数の追加"を選び、以下の変数を作成します。

環境 名前
本番環境 FunctionPrefix prd-
ステージング環境 FunctionPrefix stg-

こうすることで、リソースから"FunctionPrefix"という名前で値を参照することができるようになります。

リソースの設定

作成したリソースの"統合リクエスト"を選び、"Lambda 関数"を以下のように定義します。

${stageVariables.FunctionPrefix}ExampleFunction

"${stageVariables.FunctionPrefix}"という箇所にステージで定義した変数が入ります。 本番環境の場合、"prd-example-function"が呼び出されるようになります。