Dockerコンテナが利用するDNSを指定する
先日ステージング環境が稼働するサーバの障害が発生したため、再構築を行いました。 以前は複数台に分かれていたものを、リソースに余裕のある1台に集約しました。 構築自体は本番環境と同様にansibleで実行したので、特に問題無く終わりました。
作業が終わり、担当するサービスをリリースするため、ステージング環境へデプロイを行った際に問題が出ました。 デプロイはコンテナ化したJenkinsから行うのですが、デプロイ先のサーバの名前が解決出来ません。 慌ててステージング環境が稼働するサーバでnslookupを行いましたが、問題無く解決出来ます。 そこでJenkinsに入り同様にnslookupを行ったところ、名前が解決出来ませんでした。 他のコンテナがどうなのか調べた限りでは、どうやらalpineをもとに作成したコンテナで同様の現象が発生しました。 このときは、とりあえず/etc/hostsへ記入することでしのぎました。
デプロイはこれで問題無く行うことが出来る用になりましたが、今度はアプリケーションからデータベースへ接続出来ない問題が発生しました。 このアプリケーションもalpineをもとに作成したコンテナで、データベースの名前が解決出来ていませんでした。 サービスで利用するコンテナのイメージを変更する場合、テストを行う必要があります。 このため、コンテナが利用するDNSを指定することにしました。
まずは、起動するときにdnsオプションを指定しました。 実行すると、/etc/resolv.confに期待したサーバが指定されるようになりました。
他のコンテナも同様の設定を反映するため、Dockerの設定を変更することにしました。 この設定は、log-driverを指定するためansibleで設定を行っていました。 そこで、log-driverの指定に加えdnsの指定を追加することにしました。
さくらインターネットのオブジェクトストレージを利用した動画配信
イートスマートが運営する料理教室情報サイト「クスパ」では、昨年からレッスン動画のサービスを開始しました。
cookingschool.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ブランチを選択してボタンを押します。
以上で、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"が呼び出されるようになります。