RAKSUL TechBlog

ラクスルグループのエンジニアが技術トピックを発信するブログです

OpenAPI Generator typescript-fetch を使ってみる

この記事は ラクスルの2022年アドベントカレンダー 18日目の記事です。

こんにちは!グループ会社のダンボールワンに出向中の 🐈 miyahkun です。現在はフロントエンドエンジニアとして働いています。今回は OpenAPI を利用したクライアントコードの自動生成について紹介したいと思います。

背景

OpenAPI のクライアント生成として OpenAPI Generator というプロジェクトが有名です。弊社でも多くのプロジェクトで使用されており、生成先の言語として TypeScript、HTTP クライアントに axios を用いる typescript-axios というオプションが最も使用されています。

最近 OpenAPI のクライアント生成を新たに行う機会があり、いつものように typescript-axios をインストール・実行しました。そして自動生成されたコードを確認してみると、既存のプロジェクトで使用しているバージョンとは異なる挙動となっていました。

ここでは typescript-axios で気になった挙動の変化と解決策について共有します。

typescript-axios はもうケース変換してくれない

私たちの開発チームでは JSON 形式の API レスポンスにおいて、プロパティを snake_case で提供しています。一方、その API を利用する JavaScript (TypeScript) で書かれたクライアント内では camelCase を使用したくなります。これは JSON API を扱うにあたって非常にありふれた話題かと思います。

せっかくクライアントコードを自動生成するからには、以下のような処理も自動でやってほしいですよね。

  • リクエストのパラメーターは camelCase -> snake_case へ変換
  • JSON 形式のレスポンスではプロパティを snake_case -> camelCase へ変換
  • それに対応する TypeScript の型もケース変換されたものを生成

私たちが既存のプロジェクトで使用しているバージョンの typescript-axios では、これらの要求の一部に応えてくれていました。

  • リクエストのパラメーターは生成されたコード内でケース変換
  • レスポンスは TypeScript の型としてケース変換したものを生成する。一方で、実体は自前でケース変換する必要がある。

しかし、このケース変換の要求に応えてくれていた生成時のオプション modelPropertyNaming が v5.3.0 で削除されていることを知りました。削除された理由はこちらの issue で述べられています。

modelPropertyNaming: original というスキーマ定義を尊重する設定にした場合でも、ドットなどのケース変換不可な文字を含むプロパティ名が意図せず変換されていました。例えば、 product.name というプロパティ名が product_name になるといった具合です。

代替案の検討

私のチームは代替案としては以下の3つを挙げました。

  1. modelPropertyNaming がサポートされている typescript-axios の古いバージョンを使う。
  2. axios-case-converter などでプロパティ名の変換を行い、TypeScript の型を変換する処理は自前で書く。
  3. modelPropertyNaming がサポートされている Fetch API を利用した typescript-fetch を使用する。

(1) は該当バージョンが 1 年ほど前のリリースということもあり、積極的にメンテナンスされることが期待できないため候補から外しました。

(2) は自動生成されるそれぞれの型に対して、ケース変換を一度通さないといけない点が煩わしいと感じて却下しました。

(3) で述べているように OpenAPI Generator には HTTP クライアントとして Fetch API を使用する方法も用意されています。こちらは typescript-axios で以前にサポートされていたものと同等の modelPropertyNaming が存在しており、今回はこちらの案を採用することにしました。

私のチームが開発しているサービスでは IE11 のサポートが廃止されたこともあり、Fetch API を気軽に使えるようになりました。これにより Fetch API のポリフィルや axios を使わないことでバンドルサイズを削減できます。また、Node.js v18 から experimental ではありますがフラグなしでグローバルに生えた fetch 関数を利用できるようになったのも採用の後押しとなりました。

参照:New globally available browser-compatible APIs

typescript-fetch と typescript-axios の比較

typescript-fetchtypescript-axios は生成方法のオプションに差異があり、デフォルト値が異なる場合や、対応するオプションがそもそも存在しない場合もあります。私のチームが使うオプションについて、実際に動作させて挙動を確認してみました。

(以下の比較は Docker イメージ openapitools/openapi-generator-cli:v6.2.0 で実行しています。)

オプション typescript-axios typescript-fetch
API 呼び出し処理の生成先 apiPackage で指定必須 ./apis (該当オプションなし)
モデルの生成先 modelPackage で指定必須 ./models (該当オプションなし)
モデルと API 呼び出し処理のファイル分割 withSeparateModelsAndApi = false (デフォルト値) 分割される (該当オプションなし)
パラメーターの受け渡しにオブジェクトを使用するか useSingleRequestParameter = false (デフォルト値) useSingleRequestParameter = true (デフォルト値)

これらのオプションを設定することで、私のチームではインターフェースに大きな差異なく使用できています。

typescript-fetch のケース変換処理

typescript-fetch では愚直に snake_case <-> camelCase 間の変換を行なっています。以下がOpenAPI スキーマと自動生成されたケース変換しているコードの一部です。ケースを動的に変換する方式と比較すると生成されるコード量としては多くなってしまいますが、複雑な TypeScript の型変換が不要で個人的には気に入っています。

paths:
  /search:
    get:
      responses:
        '200':
          $ref: '#/components/schemas/product'

components:
    schemas:
        search_response:
              properties:
                products:
                  type: array
                  items:
                    $ref: '#/components/schemas/product'
              type: object
        product:
          required:
            - name
            - price_tax_included
            - dot.separated
          properties:
            name:
              type: string
            price_tax_included:
              type: integer
            dot.separated:
              type: string
          type: object
// レスポンスのケース変換処理
export function ProductFromJSONTyped(json: any, ignoreDiscriminator: boolean): Product {
    if ((json === undefined) || (json === null)) {
        return json;
    }
    return {
        
        'name': json['name'],
        'priceTaxIncluded': json['price_tax_included'],
        'dotSeparated': json['dot.separated'],
    };
}

最後に

本記事ではケース変換の問題を発端に typescript-fetch を紹介しました。IE 11 サポートの終了や Node.js の対応などで Fetch API を利用できる環境が整ってきました。それに伴い OpenAPI Generator の typescript-fetch は今後より積極的に採用されると思っています。この記事が何かの助けになれば幸いです。

参照

RでSnowflakeに接続する

この記事はRAKSUL Advent Calendar 2022の17日目の記事です。

こんにちは、ノバセル株式会社 データサイエンティストの松村です。 この記事ではRとSnowflakeに関する小ネタを紹介したいと思います。

RとSnowflake

ノバセルではDWH(データウェアハウス)としてSnowflakeを使っており、データ分析の際はSnowflake上のデータを使うことが非常に多いです。 また、自分を含めデータサイエンティスト職の人はPythonを使うことが多いですが、案件によってはRも普通に使います。 SnowflakeにはSnowsightというWeb UIがあるため、そこでSQLを書いて結果を取得、csvでダウンロードといったことができまるので、そのcsvをPythonやRで読み込めば分析はできます。 しかし、PythonやRのコード上でデータの取得まで完結すると、他のメンバーにコードを共有したり、GitHubにコードをアップした際に他の人が同じコードで同じ結果を再現することが容易になります。

Pythonの場合、Snowflakeに接続してデータを取得する専用のコネクタが存在するのですが、Rは自分が探した限りでは専用のパッケージ等がありません。そこで、この記事ではRからSnowflakeに接続しデータを取得する方法について、2通りの方法を紹介をします。

1. SQL文を書いてODBC経由で実行する

ODBCはMicrosoft社が定めた、アプリケーションからデータベースにアクセスするための仕組みです。Snowflakeはこの規格に対応しており、ODBCドライバーが提供されています。

docs.snowflake.com

このようなODBCドライバーをRから操作するものとして、odbcパッケージがあります。

ODBCドライバーや依存ライブラリのインストール

以下では、macOSなどUnix系のOSでの方法に絞って説明をします。

まず、Snowflake用のODBCドライバーをインストールします。

macOS用 ODBC ドライバーのインストールと構成 — Snowflake Documentation

そして、Rなどのプログラミング言語からODBCを操作するために必要な unixodbc をインストールします。 macOSであれば、ターミナルからbrew install unixodbcでインストールできます。

Rパッケージのインストール

次に、必要なRパッケージをインストールします。Rコンソール上で、以下のコマンドでインストールができます。

install.packages(c("odbc", "DBI"))

DBIパッケージは、ODBCとは別で、Rから何らかのデータベースに接続するとき必要なパッケージです。

Snowflakeに接続してクエリを投げる

準備が整ったので、実際にSnowflakeに接続をします。

まず、Snowflakeへの接続情報を作成します。

# Snowflakeへの接続情報を作成
conn <- DBI::dbConnect(odbc::odbc(),
                       Driver = "Snowflake",
                       Server = "<Account Name>.snowflakecomputing.com",
                       UID = "<User Name>",
                       PWD = "<Password>"
  )

この接続情報を作る時点で、以下のようにデータベース名やスキーマ名を指定することもできます。

conn_db <- DBI::dbConnect(odbc::odbc(),
                          Driver = "Snowflake",
                          Server = "<Account Name>.snowflakecomputing.com",
                          Database = "<Database Name>",
                          SCHEMA = "<Schema Name>"
                          UID = "<User Name>",
                          PWD = "<Password>"

SnowflakeでMFA(多要素認証)を利用している場合は、このコードを実行した時点で、モバイルへのプッシュ通知送信などが行われます。 作成した接続情報を使えば、DBI::dbGetQuery()関数で任意のクエリに対して結果をデータフレームの形で取得することができます。

# SQL文を書いてデータを取得
query <- "
select
  column1, column2
from
  tbl_name
where
  some conditions
"
df <- DBI::dbGetQuery(conn_db, query)

データ取得や分析が終わったら、作成した接続情報は解除しておくと安全です。

# Snowflake接続解除
DBI::dbDisconnect(conn)
DBI::dbDisconnect(conn_db)

2. dbplyrパッケージでデータを操作する

Rユーザーであれば、SQLを書くよりもtidyverse(特にdplyrパッケージによう列選択や行の絞り込み、集計処理など)を使ってデータ操作をしたいと思う方もいるかも知れません。

kazutan.github.io

dbplyrパッケージ(パッケージ名にbがついてることに注意)はデータベース上のデータに対してSQLを書かずにdplyrの文法でデータ操作が行える機能を提供しています。 この場合もデータベースへは接続するので、接続情報を作るのは同じです。

library(dbplyr)
library(magrittr) # パイプ演算子のため

# Snowflakeへの接続情報を作成
conn_db <- DBI::dbConnect(odbc::odbc(),
                          Driver = "Snowflake",
                          Server = "<Account Name>.snowflakecomputing.com",
                          Database = "<Database Name>",
                          SCHEMA = "<Schema Name>"
                          UID = "<User Name>",
                          PWD = "<Password>"

# Rオブジェクト上にテーブルへの接続を作る
source_table <- dplyr::tbl(conn_db, "<tbl_name>")

# dplyr の文法でデータを取得
garapon_genres13 <- source_table %>%
  # 列の選択
  dplyr::select(column1,
                column2) %>%
  # 条件に依る行の絞り込み
  dplyr::filter(some condition) %>%
  # データベースから結果の出力
  dplyr::collect()

このように、Rユーザーであれば慣れ親しんだ文法でSnowflake上のデータを取得することができます。

おまけ: ユーザー名やパスワードをコードに残したくない

さて、Snowflakeへの接続やデータ操作ができることは分かったのですが、どのパッケージを使うにしてもユーザー名やパスワードを使って接続情報を作る必要がありました。 ユーザー名やパスワードなどの認証情報、特にパスワードは他の人に知られるてはならず、GitHubなどのホスティングサービス上には決して置いてはいけないものです。 Rでコーディングするとき、このような場面で、コード上に認証情報を直接書かなくても良くなる方法を2つ紹介します。

rstudioapiパッケージでインタラクティブに入力

1つ目はIDE(統合開発環境)としてRStudioを用いている場合に限定されますが、インタラクティブに認証情報を入力するものです。先程のSnowflakeへの認証情報を作成する部分で、認証情報に当たる部分を以下のように書き換えます。

conn <- DBI::dbConnect(odbc::odbc(),
                       Driver = "Snowflake",
                       Server = "<Account Name>.snowflakecomputing.com",
                       # ユーザー名、パスワードはインタラクティブに入力
                       UID = rstudioapi::askForPassword("Database user"),
                       PWD = rstudioapi::askForPassword("Database password")
  )

このコードを実行すると、以下のようなポップアップが出現し、ユーザー名を入力することができます(ユーザー名の入力が終わるとパスワードの入力ポップアップが開きます)。

keyringパッケージを使う

もう一つ、keyringパッケージを使う方法があります。これは、macOSならキーチェーン、WindowsならCredential StoreといったOS側の仕組みを使って認証情報を管理する方法を提供するパッケージです。 こちらは、コード上に認証情報を書かなくても良いですが、実行環境のOSに依存してしまうため、他の人とコードを共有する目的のときは使いづらいかもしれません。そのため、参考記事の紹介に留めます。バックエンドとしてOSのkeyringだけではなく.envファイルを使った環境変数の読み込みにも対応しているので、個人での開発の際は使ってみると良いかもしれません。

qiita.com

おわりに

本記事では、RからSnowflakeに接続してデータを取得する方法や、コード上に認証情報を残さない工夫について書きました。R×Snowflakeのユーザーはあまり多くないかもしれませんが、そんな方々への参考になれば幸いです。

PMとベトナムチームの開発体制のご紹介!

はじめに

こんにちは!ラクスルの印刷事業部でPMをしています。
ラクスルアドベントカレンダー16日目を担当します。

本題に入る前に、まずは自己紹介です。
私はラクスルが2社目で、前職はERPシステムを提供する企業でエンジニアをしており、要件定義・実装・保守運用などに携わっておりました。
ラクスルに入社したタイミングで、エンジニアからPMと職種が変わり、最初はチラシや名刺など主に紙の印刷物を取り扱っているプロダクトのPMとして前任者に伴走していただきながら1人前のPMになるべく業務をしておりました。
その後はトートバッグやタオルなど紙以外のモノを取り扱うラクスルノベルティ、次に作業着やコックコートなど企業の方が業務で使用する衣類を取り扱うアパレル・ユニフォームに携わってきました。

ラクスルの印刷事業部はフェーズが異なる事業が複数存在する点が非常に魅力的だと思っております。

ラクスル価値創造レポート2022 P.20


そんなラクスルで働くPM業務の一部をノベルティ事業部を事例としてお伝えしたいと思います。

 

ラクスルノベルティのご紹介

まずはラクスルノベルティの説明を簡単にさせてください。
ラクスルはチラシや名刺など紙の印刷物をメインに取り扱っていましたが、以下の考えにもとづき事業拡張をしております。

ラクスル価値創造レポート2022 P.25

ラクスルノベルティは上記の図の中で左側に位置しており、2019年にリリースをしました。
リリース以来順調に成長を続け、今ではたくさんのお客様がトートバッグやタオル、ボールペンなどモノの印刷物を求めてくださいます。
そんなラクスルノベルティ事業のプロダクトはベトナムのエンジニアが開発をしています。
2020年にベトナム法人を設立し、ワンチームで開発をしています。
リリース当初は日本人エンジニアも開発しておりましたが、数年前から全員ベトナム支社のエンジニアになっております。

ラクスルの印刷事業部でPM採用をする中で何度か質問があったので、ベトナムのエンジニアとの開発体制についてお伝えしようと思います。
ベトナムの開発チームと働くPMの雰囲気が伝われば幸いです。
(ラクスル全体での決まりではなく、あくまでノベルティ事業部のプロダクト開発に関わるチームの事例になります)

開発体制のご紹介

[役割分担]

プロダクトのロードマップや要件定義は主に日本側で行っており、それをこれから記載するMTGで共有しています。
共有したロードマップや要件を元にしてリリースするまではベトナムのエンジニアやPMがメインになるので、バトンタッチをするイメージです。
実装前の開発タスクへの落とし込みや実装中の進捗管理、実装後の受け入れ(= テスト)も主にベトナムのエンジニアやPMが行っています。
無事にリリースができたら渡したバトンが再び返ってくるイメージで、リリースしたプロダクトによってどんなアウトカムがうまれたのかを主に日本側で検証/測定しています。


[コミュニケーション方法]

ベトナム側に日本語を話せるPMがいるので、大枠はベトナムのPMに伝えています。
頻度はそれほど多くはないですが、プロダクトや事業の現状共有やプレゼンを英語ですることもあります。
またSlackでは仕様やリリース内容について英語でコミュニケーションを取っています。
ベトナムのエンジニア同士でのコミュニケーションはベトナム語でも行うため、開発用のSlackチャンネルでは、日本語・英語・ベトナム語と3ヶ国語でやりとりがされています。

初めはコミュニケーションのミスで、認識の齟齬が発生することもありましたが、一定フローが固まってくるとそのようなことはほぼなくなりました。
より開発工数が少なくて済む仕様や顧客価値につながる仕様の提案をしてくれるなど、技術力以外の面でも積極的に関わっています。
その他以下に記載の定期MTG以外にも、仕様の確認をしたいなどはGoogle meetでクイックに会話するなど気軽にコミュニケーションをとっています。


[MTG]

ラクスル全体では厳格な決まりはなく、ノベルティ事業部に関わるPMとエンジニアが行っている事例になります。ノベルティ事業部ではスクラム開発をしており、金曜始まりで木曜終わりの1週間スプリントにしています。

  • デイリー スクラム(Daily Scrum)では、日々の開発の進捗や開発以外のタスクについてベトナム側のPMと認識を合わせています。
  • リリース内容の共有(≒スプリント レビュー(Sprint Review))を火曜日に行っています。リリースする機能を各ステークホルダーに共有して問題がないかを確認しています。
  • プロダクト バックログ リファインメント(Product Backlog Refinement)は水曜日に行っています。ロードマップの確認、直近の開発進捗の確認、開発の優先順位の見直し、PRDの内容の共有やレビューなど開発に関する様々なコミュニケーションをとるMTGにしています。細かい仕様のやりとりについてはSlackでも行いますが、主な開発関連の話はこのMTGで行うようにしています。
  • スプリント プランニング(Sprint Planning)は木曜日にベトナム側のみで行っていて、次スプリントの開発計画を立てています。
  • スプリント レトロスペクティブ(Sprint Retrospective)はベトナム側と日本側で分けて実施していて、日本側では金曜日に主にコミュニケーションのための時間として使っています。

まとめ

この記事を通して、少しでもベトナムのエンジニアと働くPMのリアルな姿を知っていただけたら嬉しいです!
ラクスルではPMの採用をしています。より詳細を知りたいという方がいましたら、ぜひ以下のリンクからご連絡ください!

あとがき

この記事を読んでラクスルに少しでも興味を持っていただいた方に向けて(勝手に)TO DOリストを用意しました。

▼すぐにラクスルに応募したい方
・レジュメをアップデートする
応募する

▼転職を検討するタイミングで応募したい方
タレントプールに登録する
 ご希望のポジションで募集を開始する際に採用担当からご連絡いたします
・レジュメをアップデートしておく

みなさまのアクションをお待ちしています!

 

AWS CDKにおけるコンフィギュレーション管理のベストプラクティス

この記事は ラクスルの2022年アドベントカレンダー14日目の記事です。

はじめまして!ノバセル事業部 NA(Novasell Analytics)開発チーム、サーバーサイドエンジニアの浅田です。昨日の記事では、23新卒内定者インターン中のメンバーが、ラクスルグループ(ラクスル・ノバセル・ハコベル)の各サービスでどんなことをしているのかを紹介してくれました。

今日は、AWS CDKにおけるコンフィギュレーション管理のベストプラクティスに関する記事を書いていきます。よろしくお願いします!

前提

まず、前提として、CDKでAWSのインフラを構築する際には、コンストラクトに多くのプロパティを設定する必要があります。

例えば、以下の簡単な例では、account, region, vpcId, instanceTypeというプロパティを渡しているのがわかります。

import { App } from 'aws-cdk-lib';
import { InstanceClass, InstanceSize, InstanceType } from 'aws-cdk-lib/aws-ec2';
import { DbStack } from './DbStack';

const devEnv = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: process.env.CDK_DEFAULT_REGION,
};

const prodEnv = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: process.env.CDK_DEFAULT_REGION,
};

const app = new App();

// 開発環境のRDSインスタンスを作成
new DbStack(app, 'DevDb', {
    env: devEnv,
    vpcId: 'vpc-2f09a348',
    instanceType: 't3.micro',
});

// 本番環境のRDSインスタンスを作成
new DbStack(app, 'ProdDb', {
    env: prodEnv,
    vpcId: 'vpc-abcd0123',
    instanceType: 'r5.xlarge',
});

app.synth();

プロパティの数が少ないうちは、上記のように値を直書きでもいいかもしれませんが、一般的に、時間が経過するにつれて管理しなくてはいけない値は増えてくるので、別の管理アプローチが必要になります。

これらの値を定義し、時間の経過とともに維持する方法が、コンフィギュレーション管理です。

この管理手法には大きく分けて2つのやり方があります。Static Management、Dynamic Managementの2つです。以下ではそれぞれの概要を述べた上で、コンフィギュレーション管理のベストプラクティスを検討していきます。

Static Management(静的構成管理)

コンフィギュレーション管理の最も一般的な手法が、このStatic Management(静的管理)です。

この手法が優れているところは、同じ入力が与えられた場合、常に同じ出力を生成する点です。この特徴は、後述するDynamic Managementの箇所でも非常に効果を発揮します。この決定論的であるという特徴はcdkにおいて最も重要なことです。なぜならcdkが扱っているのはインフラストラクチャであり、これが予期せず変更されてしまうことは非常に危険なことだからです。

このStatic Managementにはいくつか方法があります。

一つが、先述したように値を直書きする方法です。ただし、この方法は、値が増えてきた時に管理、維持が大変になるのでやめておいた方が良いでしょう。

幸い、CDKは汎用プログラミング言語を使用しているため、この設定を管理するためにさまざまな方法を活用できます。以下では、さらに3つのアプローチを見ていきます。

1. Context Variables(コンテキスト変数)

1つ目は、コンテキスト変数を利用する方法です。

コンテキスト変数に関しては以下の記事参照。

docs.aws.amazon.com

まず、コンテキスト変数の設定ですが、いくつかやり方があります。

コマンドラインからcontext変数を設定する方法

以下のようにcdkコマンド実行時にオプションでcontext変数に値をセットする方法です。ただし、この方法は、複数の値を設定するには不向きでしょう。

cdk synth -c dev-vpc-id=vpc-4321abcd -c prod-vpc-id=vpc-9876edcb
cdk.jsonファイル内で設定

cdkプロジェクト内にデフォルトで置かれているcdk.jsonファイル内でcontext変数に値をセットするやり方もあります。これが最も一般的な方法かもしれません。

{
  "app": "npx ts-node --prefer-ts-exts src/main.ts",
  "context": {
    "dev-vpc-id": "vpc-4321abcd",
    "prod-vpc-id": "vpc-9876edcb"
  }
}



さらに、設定したコンテキスト変数は、以下のようにtryGetContextメソッドを用いて簡単に取得できます。

const devVpcId = app.node.tryGetContext('dev-vpc-id') ?? 'vpc-2f09a348';
const prodVpcId = app.node.tryGetContext('prod-vpc-id') ?? 'vpc-abcd0123';



このコンテキスト変数を利用する方法は、最もシンプルですが、うまくスケーリングしないという欠点があります。

例えば、cdk.json配下でより深い構造を表現したい場合は、次のような命名規則が必要になります。

{
  "app": "npx ts-node --prefer-ts-exts src/main.ts",
  "context": {
    "dev_vpc-id": "vpc-4321abcd",
    "dev_app_security-group": "sg-012345678'",
    "prod_vpc-id": "vpc-9876edcb",
    "prod_app_security-group": "sg-abcd01234'"
  }
}

確かに可能ではありますが、小さなタイプミスや間違いが後で整合性の問題につながることもあります。

また、全ての環境の全てのプロパティをひとつのファイルで管理しているので、時間経過につれて肥大化し、管理しにくくなってしまいます。

コンテキスト変数は少量の設定であればうまく機能しますが、多くの設定を扱うのであれば、移動したほうがよいでしょう。

2. Static Files

Static Managementのアプローチの2つ目は、静的ファイルを利用する方法です。

ここでも汎用言語を使用している恩恵に預かれます。つまり、その言語がサポートする場所であれば、データを移動したり保存したりできるということです。

先述した例では、cdk.jsonに全てのプロパティを設定していました。まずはこれらすべてを別のファイルにリファクタリングし、そしてそのファイルを読み込んで、そのプロパティをスタックに渡します。

const devProperties = require('./env/dev.json');
const prodProperties = require('./env/prod.json');

// DEV RDSインスタンスを作成する
new DbStack(app, 'DevDb', {
    env: devEnv,
    ...devProperties,
});

// prod RDSインスタンスを作成する
new DbStack(app, 'ProdDb', {
    env: prodEnv,
    ...prodProperties
});

dev.json

{
  "vpcId": "vpc-2f09a348",
  "instanceType": "t3.micro"
}

prod.json

{
  "vpcId": "vpc-abcd0123",
  "instanceType": "r5.xlarge"
}
  • 上では、特定の環境のすべての設定は、環境ごとに1つのファイルにあります。
    • すべてを1つの場所に置くことで、環境で何かを変える場所を知ることは非常に明確かつ簡単になります。
    • ただし、このアプローチにも課題があります。
      • この場合、instanceTypeは文字列「t3.micro」「r5.xlarge」で設定されています。
      • このような文字列値は、誤入力しやすく、間違って「t3, micro」を入力してしまった場合、エラーにつながります。
      • また、このようなエラーは非常に見つけにくいです。
      • 静的ファイルは、コピー/貼り付けているだけ、または間違えにくい文字列がたくさんある場合に最適だといえるでしょう。

3. Less Static-y Files

先ほど見たように、静的.jsonファイルでは、InstanceTypeクラスのようなより複雑な型を指定できません。代わりに以下のような.tsファイルの使用に変更すると、ある種の安全性を得られます。

prod.ts

import { InstanceClass, InstanceSize, InstanceType } from 'aws-cdk-lib/aws-ec2';
export const config = {
    vpcId: 'vpc-abcd0123',
    instanceType: InstanceType.of(InstanceClass.R5, InstanceSize.XLARGE),
};



ここでのもうひとつの利点は、dev.tsとprod.tsファイルがより複雑なロジックで、jsonファイルよりも複雑な型を表せることです。

たとえば、これらの値の一部を環境変数からオーバーライドしたいときには、以下のようにすることで対応できます。

prod.ts

import { InstanceClass, InstanceSize, InstanceType } from 'aws-cdk-lib/aws-ec2';
export const config = {
    vpcId: process.env.DEV_VPC ?? 'vpc-abcd0123',
    instanceType: InstanceType.of(InstanceClass.R5, InstanceSize.XLARGE),
};



ここで、注意するべきことは、コンテキスト変数や環境変数などについては、以下のように適切に検証する必要があるということです。

const vpcId = process.env.VPC_ID;
if (!vpcId || !isValidVpcId(vpcId)) {
    throw new Error("Please provide a valid VPC_ID environment variable");
}
  • もし、パイプラインの変更によりvpcIdが誤って変更された場合、スタックが失敗する可能性があるからです。
    • 先述したように、cdkのコードは常に決定論的になるように配慮することが最も重要なことです。
    • この点に関して言えばcdk.jsonなどで静的に管理するアプローチの方が優れています。
    • 基本はtsファイルで管理しつつ、コンテキスト変数や環境変数などの入力に対して適切にチェックするという手法がベストでしょう。

Dynamic Management

コードの設定を取得するためのよりダイナミックな方法もあります。設定値をサードパーティのサービスに置いて、実行時に動的に取得する方法です。

RDSインスタンスの設定がJSONファイルに保存されている例に戻りましょう。

{
  "vpcId": "vpc-2f09a348",
  "instanceType": "t3.micro"
}



JSONは、データ転送に移植性が高く、頻繁に使用される形式です。必要に応じて、このデータはローカルファイルではなくサービスから取得できます。

axios.get('https://someconfig.novasell.com/dev')
    .then(devConfig => {
        const app = new App();
        const stack = new DbStack(app, 'DevDb', results);
        app.synth();
    });
  • ここでは、シンプルなHTTPクライアントであるサードパーティのaxiosライブラリが、何らかのconfigサーバーを呼び出してconfig情報を取得し、dev DB Stackを作成します。

    • これは非常に強力な方法ですが、非常に危険な方法でもあります。
      • なぜなら、このコードが実行されるたびに、https://someconfig.novasell.com/devへの要求の結果が同じ結果を返すという保証はない、つまり決定論的ではないからです。
      • 入力が異なる場合、予期せぬタイミングでスタックが変容してしまう恐れがあります。
      • このアプローチをより決定論的にするためのステップとして、以下のようなアプローチがあります。

  • まず取得したデータをファイルに保存するように変更します。

axios.get('https://someconfig.novasell/dev')
    .then(devConfig => {
        fs.readFileSync('./env/dev.json', devConfig);
    });
  • そうすると、先述したような静的ファイルの読み込みというアプローチに帰着します。
const devProperties = require('./env/dev.json');

// Create our DEV RDS instance
new DbStack(app, 'DevDb', {
    env: devEnv,
    ...devProperties,
});
  • 上のようなアプローチをとることで、データの取得は2つの別々のステップになり、最初のステップで結果をキャッシュに書き込み、2番目のステップで使用するようになりました。
    • このように2つのステップに分けることで、予期せぬ変更が起こる可能性を減らせます。
    • なぜなら、コードを合成すると、静的な設定から再び駆動されるため、常に同じ出力になるからです。

ベストプラクティス

これまで、コンフィギュレーション管理の様々なアプローチを概観してきました。

まとめると、以下のように分類できます。

Static Management

  • contextで渡す

  • configの値をファイルに分ける(json)

  • configの値をファイルに分ける(ts)

Dynamic Management

  • サードパーティのサービスに置いて、実行時に動的に取得する



cdk.jsonに書いていく方法は、肥大化しがちです。

jsonファイルに分ける方法もありですが、例えばInstanceTypeのような複雑な型を指定するようなケースには不向きです。

実行時に動的に取得する方法は、configで制御するものが少ない場合、用意する手間と見合わないかもしれないので、動的に取得することによって得られる利益と用意する手間とを比較衡量して判断するようにしましょう。

以上をまとめると、最初はtsのファイルに分ける方法で、値が増えてきたら、実行時に動的に取得する方法に切り替えるのがいいでしょう。ただし、この方法を採用する場合は、コードが決定論的になるように、コンテキスト変数や環境変数などについては、適切に検証する必要があります。

おわりに

今回は、AWS CDKにおけるコンフィギュレーション管理の種々のアプローチを概観し、その中のベストプラクティスを検討してきました。

CDKは非常に奥が深いかつ非常に強力なツールなので、今後も継続してこの分野のインプット・アウトプットしていきます!

ラクスルの2022年アドベントカレンダーはまだまだ続きます!

ラクスルの内定者インターンって何してるの?23新卒エンジニアに聞いてみました

この記事は ラクスルの2022年アドベントカレンダー14日目の記事です。

はじめまして!ラクスル事業部 PBU(Printing Business Unit)開発チームで内定者インターンをしている23新卒の杉山です。今日の記事では、ラクスルグループ(ラクスル・ノバセル・ハコベル)の各サービスで内定者インターンをしている23新卒のメンバーにどんなことをしているのかを聞いてみました。

続きを読む