Ponz Dev Log

ゆるい開発日記

AWS CDKでAmplifyにフロントエンドのアプリを乗せる環境を作る

最近AWSでフロントエンドのアプリをホスティングする時はAWS Amplifyを使うようになりました。 ただ、AmplifyのGUIからポチポチしていると複数環境、複数アプリで同じことをやるのは面倒です。 CloudFormationは個人的にはまだ苦手意識があるので、慣れた言語で書きやすいAWS CDKで環境を作ろうという話です。

以下のブログ記事を参考にしています。

aws.amazon.com

ホスティングするアプリを作る

簡単に考えるために、SSRのない静的サイトのフロントエンドアプリを作ります。 この記事ではサンプルとしてNext.jsで作成します。

$ npx create-next-app nextjs-blog --use-npm

アプリを作成したら静的サイトを出力するために package.json"build" スクリプトを、 "build": "next build && next export" に書き換えておきます。 これで npm run build を実行すると、out ディレクトリにAmplifyでホスティングする静的サイトのファイルが出力されます。

CDKのスタックを作る

続いてアプリケーションのプロジェクトルートでAWS CDKのスタックを作成します。 雛形は cdk コマンドで作成しますが、グローバルインストールしたくないので npx コマンド経由で実行します。

$ npx -p aws-cdk cdk init --language typescript
$ tree -L 1 amplify
amplify
├── README.md
├── bin
├── cdk.json
├── cdk.out
├── jest.config.js
├── lib
├── node_modules
├── package-lock.json
├── package.json
├── test
└── tsconfig.json

5 directories, 6 files

雛形が作成されたら、libディレクトリの下のスタックを編集します。

AWS CDK stack to create repository and applicatio…

編集したら npm run cdk deploy をキックするだけ。

環境作成直後はまだビルドがキックされません。リポジトリにコミットをプッシュすれば自動で動き出します。

ここで気をつけて欲しいのは、Gitリポジトリのルートに amplify.yaml を配置する必要があることです。 未検証ではありますが、ドキュメントを読むとCDK側にビルド定義(buildSpec)を記載すればルートにファイル配置しなくてもよいです。

なお、CDKのAmplifyモジュールのConstructはまだExperimentalのステータスです。最新の情報は公式ドキュメントを参照ください。

docs.aws.amazon.com

CloudFormationと比較する

ちなみにCloudFormationで全く同じリソースを作成する場合、テンプレートは以下のようになります。

作るリソースが少ないのでこれでも理解しやすいですが、CDKの方がシンプルなのが分かります。 行数にしてCDKが約30行、CloudFormationが約90行なのでCDKの方が1/3の記載に短縮できています。

AWS Amplify application stack. Once pushed your s…


以上。

Reactのhookを触る

ここ1~2年のお仕事フロントエンド開発でSPAといえばAngularばかりだったのですが、 最近になってReactを学び直すモチベーションが高まったのでhookを学ぶことにしました。 Reactを最後に開発で使ったのは2年以上前なので、hookについては完全に初心者です。

この投稿ではReact公式のガイド "Reactの流儀"で紹介されているミニアプリにReact hookを適用した時のメモです。 独自のhookについては扱いません。

ja.reactjs.org

そもそもhookとは?

hookは関数コンポーネントにstateやライフサイクルメソッドの機能を組み込む仕掛けと理解しました。

ReactでUIの部品を構成するコンポーネントは、 React.Componentクラスを継承したクラスコンポーネントとReact要素を返す関数である関数コンポーネントの2つに分類されます。

前者のクラスコンポーネントコンポーネントの中でユーザーのアクションや時間経過で変化する値であるstateを扱うことができます。 また、コンポーネントのマウント/アンマウントのタイミングで処理を実行できるライフサイクルメソッドを利用できます。 しかし、関数コンポーネントはstateとライフサイクルメソッドを利用できません。

hookを使うと、クラスコンポーネントでしか使えなかったstateやライフサイクルメソッドに相当する機能を関数コンポーネント組み込むことができます。

ja.reactjs.org

hookの種類

ひとまとめにhookといっても、React組み込みのhookだけでもいくつか種類があります。 React公式のドキュメントでは、"ステートフック"と"副作用フック"が紹介されています。

ステートフック

関数コンポートで状態を扱うhook。React.Componentのstateに相当する機能です。useState で使えるようになります。

副作用フック

画面表示に影響を与える副作用を持った処理を行うhook。useEffect で使えるようになります。 APIの呼び出し/クリーンアップなどクラスコンポーネントのライフサイクルメソッドでバラバラに実行していた処理を、 hook内で1箇所にまとめて宣言、実行できます。

React.Componentの代わりにhookを使ってみる

実際にここからミニアプリを題材に、クラスコンポーネントから関数コンポーネント + hookに置き換えてみます。

ステートフック

まずは検索フィールドと商品リストを表示するコンポーネントをクラスコンポーネントで書いてみます。 状態として検索フィールドの入力値をstateに持ったコンポーネントです。

クラスコンポーネントの例

import React from 'react';
import SearchBar from "./SearchBar";
import ProductTable from "./ProductTable";

class FilterableProductTable extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            filterText: '',
            isStockOnly: false,
        };

        this.handleTextChange = this.handleTextChange.bind(this);
        this.handleCheckChange = this.handleCheckChange.bind(this);
    }

    handleTextChange(e) {
        this.setState({filterText: e.target.value});
    }

    handleCheckChange(e) {
        this.setState({isStockOnly: !this.state.isStockOnly});
    }

    render() {
        return (
            <React.Fragment>
                <SearchBar
                    text={this.state.filterText}
                    onTextChange={this.handleTextChange}
                    onCheckChange={this.handleCheckChange}
                />
                <ProductTable
                    filterText={this.state.filterText}
                    isStockOnly={this.state.isStockOnly}
                />
            </React.Fragment>
        );
    }
}

export default FilterableProductTable;

次に上記のクラスコンポーネントと同等のUIを関数コンポーネント + hookで実現させます。

関数コンポーネント + hookの例

import React, {useState} from 'react';
import SearchBar from "./SearchBar";
import ProductTable from "./ProductTable";

function FilterableProductTable() {
    const [filterText, setFilterText] = useState('');
    const [isStockOnly, setIsStockOnly] = useState(false);
    const handleTextChange = event => setFilterText(event.target.value);
    const handleCheckChange = event => setIsStockOnly(!isStockOnly);

    return (
        <React.Fragment>
            <SearchBar
                text={filterText}
                onTextChange={handleTextChange}
                onCheckChange={handleCheckChange}
            />
            <ProductTable
                filterText={filterText}
                isStockOnly={isStockOnly}
            />
        </React.Fragment>
    );
}

export default FilterableProductTable;

クラスコンポーネントと比較して、関数コンポーネント + hookの方がだいぶスッキリしました。 子コンポーネントに渡すコールバック関数をthisにバインドする必要もなくなるので、これを忘れてイベントが発火しないといった事態が避けられるのも嬉しいですね。

また、setXxxで必要なプロパティだけ書き換えられるのも見通しがよいです。 管理する状態が多くなりすぎてsetXxxだらけになると必然的に見通しが悪くなるので、コンポーネントの責務が不適切と気づくきっかけにもなりそうですね。

副作用フック

続いてライフサイクルメソッドもクラスコンポーネントから関数コンポーネント + hookに置き換えてみます。 コンポーネントをマウント後に商品一覧を読み込んで表示するコンポーネントを例にします。

クラスコンポーネントの例

import React from 'react';
import FilterableProductTable from "./FilterableProductTable";

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            products: [],
        };
    }
    
    componentDidMount() {
        this.setState({products: PRODUCTS});
    }
    
    render() {
        return <FilterableProductTable products={this.state.products}/>;
    }
}

export default App;

上記のコンポーネントをhookで書き換えます。

関数コンポーネント + hookの例

import {useState, useEffect} from 'react';
import FilterableProductTable from "./FilterableProductTable";

const PRODUCTS = [...];

function App() {
    const [products, setProducts] = useState([]);

    useEffect(() => {
        // 本来は何かしらのAPI呼び出しで商品リストを取得する
        setProducts(PRODUCTS)
    }, []);

    return (
        <FilterableProductTable products={products}/>
    );
}

export default App;

正直このミニアプリだとライフサイクルメソッドが1箇所かつ単純な処理しか実行していないので嬉しさをさほど感じません。 componentDidMountcomponentWillUnmount にバラバラに処理が書かれている場合は、useEffectの関数の中に処理が纏まるので見通しが良くなりそうです。


Reactのクラスコンポーネントをhookで書き直す例でした。 既存の重厚なクラスコンポーネントを書き直すのはハードルが高いですが、 新しいコンポーネントの開発や関数コンポーネントに状態が欲しくなったときには少しずつ使っていきたいです。

以上。

2020年の振り返り

2020年の年末エントリです。

昨年末に書き起こしておいたものと今年の自分を比較して振り返ってみます。 昨年の年末エントリはこちら。

ponzmild.hatenablog.com

コロナウィルスと私

今年はコロナウィルスの影響なしでは語れません。 私は今年の4月から完全に在宅勤務に移行しています。 出社には上司の承認が必須であり、遠方の取引先への訪問も控えるようになりました。 開発がアジャイルウォーターフォールに関係なくフルリモートで仕事することになりました。

幸いにも近親者に感染された方はいなかったですが、いつどこで感染してもおかしくありません。 来年も手洗いうがいとマスク着用を続けるつもりです。

自宅環境はWi-FiとPCを問題なく準備できていたので、結果的に在宅ワーク自体は問題なかったように感じます。 取引先のWeb会議への理解と移行が早かったことも大きかったです。 備えって大事です。

もちろん在宅ワークならではの悩みもありました。 オフィスのように近くにプロジェクトメンバーがいないので、人に相談するハードルはすごく高かったです。 ほかの人も不安を感じないように、数分の相談でもチャットやWeb会議をやっていました。

また、相手にいつ話しかけて良いか分からないのも悩みだったので、 意識的にSlackのステータスのアイコン・コメントを変えるようにしてました。 意外とアイコン・コメントは見てくれる方が多く、「午前は打ち合わせなんだね、相談は午後にします」という提案もスムーズにもらえたのでお勧めです。

やってきたこと

お仕事

今年の11月まで1つのサービスにじっくり向き合い、幸運なことにリリース & 本格展開まで漕ぎ着けました。 サービスのリリースに立ち会うのはエンジニア人生で初めての経験です。 じっくり向き合えたおかげで、業務フローと開発機能のすり合わせやバックログの優先づけ、機能・非機能要件に合わせた技術選定とチャレンジできたのは本当に幸運です。 個人的には新規開発機能をすべてTypeScript化して開発・テスト効率を上げたのは大きな成果でした。 これで1ネタ知見をまとめたいですね。

上記案件を離れてからは、認証基盤構築で初のOpenShift、そして11月からはレガシーアプリケーションのマイクロサービスへの移行・設計をお手伝いしています。 関わり方も今までの開発者の役割ではなくアーキテクト的な役割を求められて苦戦しています。 どの責務で何を実装すべきか、どの選択肢を選ぶべきか、自分の中で指標を見つけて来年通して強化していきたいところです。 まずはマイクロサービス本を読んでいるので、自分なりの理解はどこかでまとめたい。

www.oreilly.co.jp

2020 仕事歴

  • 2020/01-09 流通業界企業のtoCサービスの開発 (Node.js, TypeScript, Blockchain)
  • 2020/10-11 流通業界企業のtoCサービスのアーキテクチャ設計支援
  • 2020/09-11 認証基盤構築 (OpenShift, Keycloak)
  • 2020/11- マイクロサービス設計支援×2 (Java)

個人ワーク

技術書典8/9も出典して技術書を1本出しました。Apache Kafkaの本です。

ありがたいことにKafka本はお仕事でも参考本として読んでいただけました。 Twitterでも良い本だったとコメントいただけたので、十分成果を得られたと感じています。

ただ、Kafkaのストリーム処理についてはまだ理解が浅いです。 Kafka StreamsやKafka Connect、ksqlDBなどユースケースと合わせて今後も学んでいきます。

ponzdou.booth.pm

私生活

今年の11月に結婚しました。 妻とは学生の時から6年の付き合いです。ここまで一緒にいてくれた妻には感謝しかありません。 今後はエンジニア業を頑張りつつ、妻との時間も大切にしていきます。

それと結婚を機に家具を新調してQoL爆あがりしています。 特に傘立て・ゴミ箱・サーキュレーターの3つは一人暮らしだと絶対買わないですが、買ってみると非常に良いです。 観察眼が鋭い妻ならではのセレクトで、心の健康が加速してます。

去年の目標と今の私

去年立てた2020年の目標を今一度みてみます。

技術書典8:予定としては前回出したNATS本の改訂版と、NATS/Kafka/RedisのPub/Subメッセージングソフトウェアの比較本を出す予定です。

本は個人ワークでも書いたように、1冊執筆して改訂版も出すことができました。 NATSの比較本は本家NATSがJetStreamというNATS Streamingに替わる新しいしくみを作るとアナウンスを出したので、いったん仕様が固まるまで手をやめてます。

英語力UP:TOEIC780点を目指します。

自分の中での優先度が低かったので、まったく手をつけられませんでした。 今年の10月からインドの開発者と一緒に仕事する機会があり、やっておけば良かったと盛大に後悔しています。 話すのはなんとかできますが、そもそも聞き取りができない。ここを来年なんとかしたいです。

資格:CKAD(Certified Kubernetes Application Developer)取ります!

CKADは今年の9月に受験はしたのですが、まさかの不合格でした。 後半で作業するNamespaceを間違えて時間をロスするという痛恨のミスが敗因です。

このミスは開発環境と本番環境を間違える人と本質的に同じことをやっている気がしたので、普段から大袈裟に指差し確認するところから意識を変えて出直すことにしました。 まだ再受験のチャンスが1回だけ残っているので、来年の3月までには合格を目指したいです。

ブログ:今年のペースを落とさず年24本書きます。

結果的には、この年末エントリを含めて16本になりました。 昨年と同じペースを目標にしたものの、仕事で求められる役割が変わってから去年と同じペースで書けなくなりました。 今までは1つを深く検証する記事が多かったので、まとまった時間がないとなかなか書き出せません。

ペースは落ちたとはいえ、来年も学びをやめないために今後もアウトプットは出していくつもりです。 来年は気付きや学びをこまめに出していきます。

体重増やす:来年末までに60kgまで増やします。もちろん健康的に自炊しながらね :)

なんと体重減りました。去年から-3kgの53kgです。 ただ、結婚してからは食生活は見直して、バランスを取るために自炊の頻度を増やしました。 クラシル で動画見ながら料理するの楽しいです。

2021年の目標

今年の反省を受けて、以下3つを目標にします。

  • 技術の学び直し

    • 2020年に取れなかったCKADをリベンジします。
    • AWS Certified Developer Associateの有効期限が切れたので、AWSの学び直しをやります。AWSの資格を1つ合格を目標にします。
  • ブログでアウトプット継続

    • インプットを継続した指標としてブログ記事を継続的に書きます。
    • 月1本を目標にします。
  • 体力増強

    • 在宅勤務が始まってから椅子に座りっぱなしですので、かなり疲れます。
    • 疲れて集中力も切れや少なったので、足腰を鍛えて体力増強します。

今年もブログを読んでくださった皆様ありがとうございました。 来年もよろしくお願いします。

以上。

IBM Cloud Secret ManagerでAPIキーを有効期限つきで発行する

この投稿はIBM Cloud Advent Calendar 2020の8日目の記事です。

qiita.com

IBM CloudではユーザーまたはサービスIDに紐づくAPIキーを発行・利用することでリソース操作が可能になります。 しかしユーザーやサービスIDが増えてくると、発行したAPIキーの権限や有効性、使用の有無の管理が面倒になります。管理を怠った結果、強力な権限を持ったAPIキーがGitリポジトリにコミットされて公開されてしまったら、リソースを削除されてサービス停止を招きかねません。

そこでIBM Cloud Secret Managerを使用してAPIキーを有効期限つきで発行・一元管理し、APIキーを悪用されないようにしてみます。

この記事で説明すること

  • Secret ManagerのAPIキー発行のしくみ
  • Secret Managerのインスタンスを立てる
  • Secret ManagerのIAMシークレットエンジンを有効化する
  • 有効期限つきAPIキーを発行する

この記事で説明しないこと

ponzmild.hatenablog.com

Secret ManagerのAPIキー発行のしくみ

APIキーを発行する前に、Secret Managerがいったい何をしているのか簡単に説明します。

Secret ManagerのIAMシークレットエンジンは、シークレット作成リクエストを受け取るとサービスIDとAPIキーを動的に発行しています。(下図の★) サービスIDはIBM Cloudのリソースにアクセスするアプリケーションを表すIDです。 APIキーはこのサービスIDに紐付けられるため、どのアプリケーションが操作したか識別できます。

また、サービスIDは権限とメンバーをグルーピングしたアクセスグループに紐付けられます。 Secret ManagerはサービスIDの権限付与にアクセスグループを利用するので、複数のAPIキー発行時も毎回同じ最小権限を持たせることが可能になっています。 サービスIDの権限付与にアクセスグループを利用するので、複数のAPIキー発行時も毎回同じ最小権限を持たせることが可能になっています。

f:id:accelerk:20201208021024p:plain
APIキーの権限階層構造

Secret Managerのインスタンスを立てる

それでは、本題のAPIキーを発行する前の準備します。 まずは公式ドキュメントに沿ってSecret Managerのインスタンスを立てるところから始めましょう。 インスタンス作成方法は公式ドキュメントを参考に、カタログから任意の名前を指定するだけでOKです。

Secrets Manager サービス・インスタンスの作成

コンソールをポチポチするのが面倒な人向けにTerraformでインスタンスを作成を用意しました。Terraformに慣れている方はこちらを参照してください。

github.com

IAMシークレットエンジンを有効化する

インスタンスを作成したら、APIキーを発行するためのIAMシークレットエンジンを有効化します。 シークレットエンジンは、Secret Managerに備わったシークレットを発行するコンポーネントです。IAMシークレットエンジンはそのうちの1つです。

インスタンス作成直後はIAMシークレットエンジンが無効化されているため、有効化しましょう。 『シークレット・エンジンの構成』ガイドを参照して進めます。

ここで作成するものは以下の3つです。 前節でTerraformを使用してインスタンスを作成した場合、サービスIDとアクセス・グループはすでに作成されています。この場合はシークレットエンジン用のAPIキー発行のみ実施してください。

  • サービスID
  • アクセス・グループ
  • シークレットエンジン用のAPIキー

サービスID作成

最初にSecret Managerを表すサービスIDを発行します。 サービスIDは"アクセス (IAM)"メニューの"サービスID"タブから作成できます。

f:id:accelerk:20201208021142p:plain
サービスIDを作成

アクセス・グループ作成

続いてサービスIDに紐付けるシークレットエンジン用のアクセスグループを作成します。 IAM Access Groups Service サービスにEditor役割を、IAM Idエンティティ Service サービスにOperator役割をつけます。

f:id:accelerk:20201208022731p:plain
Secret ManagerがAPIキーを発行するためのアクセス・グループ

アクセス・グループを作成したら、先ほど作成したサービスIDをメンバーに加えましょう。

シークレットエンジン用のAPIキー発行

シークレットエンジン用のAPIキーを発行します。 "アクセス (IAM)"メニューの"APIキー"タブから作成できないので、IBM Cloud CLIからAPIキーを発行しましょう。

以下のコマンドを実行します。 ここで発行されたAPIキーは2度と表示できないので、クリップボードテキストエディタにコピーします。

$ ibmcloud iam service-api-key-create \
  <APIキーの名前> <サービスIDのUUID> \
  --description "API key for Secret Manager IAM secret engine" 

IAMシークレットエンジンを有効化

最後に先ほど発行したAPIキーをシークレットエンジンに渡して有効化させます。 Secret Manager管理画面の"設定"メニューから、"IAM Secret Engine"欄を探してAPIキーを埋め込みます。

f:id:accelerk:20201208021302p:plain
IAMシークレットエンジンを有効化

有効期限つきAPIキーを発行する

ここから本題です。 本記事では、発行したAPIキーからアクセストークンを生成し、アカウントの使用量(=課金額)を取得してみます。

まずSecret Managerの管理画面から"秘密"メニューを開き、APIキーを発行します。 IAMシークレットエンジンを有効化しているので、"IAM credentials"を選択できるようになっています。 "IAM credentials"を選択し、APIキーの項目を入力します。

入力欄からアカウントの使用量を取得する権限を持ったアクセスグループを指定します。以下のようにBillingサービスに"Viewer"役割を持ったアクセス・ポリシーを付け足アクセスグループを事前に作成しておきます。

f:id:accelerk:20201208021213p:plain

また、有効期限を設定したいので最下部の"Lease Duration"欄に有効期間を設定します。今回は10分だけ有効にします。

f:id:accelerk:20201208021435p:plain
APIキーを発行

"Add"ボタンを押下すれば、これでAPIキー発行は完了です。 シークレット一覧に表示され、詳細表示するとUUIDが割り振られていることがわかります。

APIキーを使用する

APIキー自体はSecret Managerに保管されているので、使用時はSecret ManagerのREST APIを実行して取り出します。

Secret ManagerのREST APIは組込みSwagger UIから実行可能です。それではSwagger UIからシークレット取得APIを実行してAPIキーを取り出しましょう。 Swagger UIは https://<インスタンスID>.<リージョン>.secrets-manager.appdomain.cloud/swagger-ui/ からアクセス可能です。

f:id:accelerk:20201208021544p:plain
シークレット取得APIを実行

シークレット取得APIを見つけて先ほど確認したUUIDを渡すことで、Secret Managerに保存されたAPIキーを取り出します。 実行すると以下のようなJSONレスポンスにAPIキー api_key が含まれています。

{
  "metadata": {
    "collection_type": "application/vnd.ibm.secrets-manager.secret+json",
    "collection_total": 1
  },
  "resources": [
    "access_groups": ["AccessGroupId-**********"],
    "api_key": "z4a-*******************",
    "creation_date": "2020-12-07T11:00:00Z",
    "state_description": "Active",
    "ttl": 600
  ]
}

取り出したAPIキーからIBM Cloudをリソース操作するためにアクセストークンを発行します。

$ IAM_TOKEN=$(curl -k -X POST \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --header "Accept: application/json" \
  --data-urlencode "grant_type=urn:ibm:params:oauth:grant-type:apikey" \
  --data-urlencode "apikey=$IAM_API_KEY" \
 "https://iam.cloud.ibm.com/identity/token" | jq -r '.access_token')

最後にアカウントのIDとIBM Cloud Usage Reports APIを使用して2020年11月の使用量を取得します。 以下のようなレスポンスが取得できました。

$ ACCOUNT_ID=$(ic account show --output json | jq -r '.account_id')
$ curl -H "Authorization: $IAM_API_TOKEN" -H "Accept: application/json" "https://billing.cloud.ibm.com/v4/accounts/$ACCOUNT_ID/summary/2020-11" | jq .

{
  "account_id": $ACCOUNT_ID,
  "month": "2020-11-30T23:59:59.999Z",
  "resources": {
    "billable_cost": 0,
    "non_billable_cost": 0
  },
  ...
}

私のアカウントはライトアカウントのため使用量は0で表示されています。 無事に発行したAPIキーを使用してリソースを操作できるようになりました。

参考


以上。

IBM Cloud Secret Manager (Beta)でシークレットを保管・取得する

IBM Cloudにシークレットを一元管理するためのサービスとしてIBM Cloud Secret Managerがベータリリースされました。 この記事ではSecret Managerのサービスの概要を理解するために、GUIでのシークレット保管・取得を通じて探ります。

www.ibm.com

※ 2020/11時点でベータリリースの状態ですので、機能やUIが大幅に変更になる可能性があります。詳細は最新のドキュメントを参照してください。

IBM Cloud Secret Managerとは?

IBM Cloud Secret Manager (以降、Secret Managerと記載) とは、HashiCorp Vaultをベースに構築されたシークレットを一元管理するためのサービスです。 シークレットとして、IBM CloudなどのプラットフォームのAPIキー、SaaSAPIキー、DB接続情報、ユーザーIDとパスワードの組み合わせといった機微な資格情報を格納できます。

自分の理解では、このサービスを使うことで2つのメリットがあります。

1つ目はアプリケーション、ツール、ファイルストレージといったさまざまな場所に散らばりがちなシークレットを1箇所で管理できること。 あるアプリケーションでは環境変数、あるツールではハードコードといったシークレットが散らばった状態だと、シークレットを1つ更新するだけでも影響範囲の特定が難しくなります。1箇所にシークレットを管理することで、安全にシークレットを更新・削除できるので運用が簡単になります。

2つ目はシークレットをSecret Managerから取得するように構成することで、ソースコード内にシークレットのハードコードがなくなります。結果的に外部の人が閲覧できるリポジトリ経由でシークレットが漏洩するリスクを低減できます。GitHubにうっかりAWSの認証情報をコミットしてPublicリポジトリにさらすなんて事態を回避できます。

Secret Managerのアーキテクチャ

f:id:accelerk:20201127015313p:plain
IBM Cloud Secret Managerのアーキテクチャ

冒頭のAnnouncementの図にも表記されていますが、サービスのアーキテクチャはVault on Kubernetesのようです。 シークレットはCloud Object Storageに暗号化されて管理されています。

Vaultのバックエンドの構成やKubernetes上にどうやってStatefulなサービスを動かしているのかとアーキテクチャ的に気になりますね。 ただし、GUIを触る限りユーザーは内部アーキテクチャを意識することはありません。

Secret Managerでシークレットを操作してみる

やってみること

本題です。今回はSecret Managerから操作可能なシークレットのうち、ユーザー資格情報のシークレットをGUIから保管・取得してみます。

ちなみにSecret Managerでは以下の3種類のシークレットを操作可能です。デフォルトではIAM資格情報以外を設定なしで保管できます。 IAM資格情報の管理は別の記事で書きます。

  • ユーザー資格情報 (ユーザーIDとパスワードの組み合わせ)
  • IAM資格情報 (IBM CloudのService IDに紐づいたAPIキー)
  • 任意の資格情報 (上記以外のテキストで表現できる値)

シークレットの保管

まずはSecret Managerのインスタンスを作成します。 カタログからIBM Cloud Secret Managerのライトプランをオーダーすると、Secret Managerのトップページが表示されます。

f:id:accelerk:20201127135341p:plain
Secret Managerのトップ画面

ここから左メニューの「秘密」から「追加」→「User Credentials」(ユーザー資格情報)を選択すると、 以下のスクショの通りユーザー資格情報の入力画面が表示されます。 軽く触るだけにとどめたいので、必須項目のName、Username、Passwordのみ入力してシークレットを保管します。

f:id:accelerk:20201127135641p:plain
ユーザー資格情報入力画面

シークレットを保管したら、一覧に先ほど入力したユーザー資格情報が1行追加されています。

f:id:accelerk:20201127135900p:plain
シークレット一覧

また、Edit detailsを押すとNameとは別にID (UUID) が採番されています。 IDはシークレット取得時に使用するので、クリップボードにコピーしておきます。

シークレットのIDが採番されている

シークレットの取得

シークレットを保管できたので、続いてシークレットをSecret Managerから取得します。 シークレットの取得はREST APIから呼び出します。 なんとSecret ManagerはSwagger UIが事前構成されているので、REST APIを試すのもGUIで簡単です。 この記事ではSwagger UIからSecret Manager APIを呼び出します。

f:id:accelerk:20201127140235p:plain
Secret Manager APIのSwagger UI

APIを呼び出すにはサービスへのアクセストークンが必要です。 ibmcloudコマンドでログインとアクセストークンの取得を事前に済ませておきます。 アクセストークンはSwagger UIの「Authorize」ボタンを押すと出現するポップアップに入力すればOKです。

# ログインユーザーの権限をもったアクセストークンをクリップボードにコピー
$ ibmcloud iam oauth-tokens --output json \
    | jq -r '.iam_token' \
    | awk '{print $2}' \
    | pbcopy

準備は整ったのでREST APIを呼び出します。 シークレットの取得は、 /api/v1/secrets/{secret_type}/{id} エンドポイントです。 idはシークレット保管時に採番されたIDを指定します。

f:id:accelerk:20201127140812p:plain
Swagger UIでユーザー資格情報を取得リクエス

REST APIを呼び出すと、GUIから登録したUsernameとPasswordを取得できました。 これでシークレットを使用するクライアントは、シークレットをハードコードする代わりにIDからシークレットを取得して使えそうです。

f:id:accelerk:20201127141457p:plain
保管したシークレットを取得できた


次の記事では、Vaultの特徴を活かしたIAM資格情報の動的生成を実施してみます。

以上。

AsciiDocからPDFを生成する手順をテンプレートにまとめる

最近は仕事や個人のメモの執筆環境をMarkdownからAsciiDocに切り替えています。 Markdownは何より手軽ですしこのブログも全部Markdownで書いているのですが、 AsciiDocの表現力の豊富さに慣れてしまうとMarkdownには戻ることはできません。 Asciidoctorを使えばHTMLも作れますし、 Asciidoctor PDFを使えばPDF生成もできちゃいます。

今回はAsciiDocの文章からPDFを生成する手順をテンプレート化したよという話。 毎回忘れてGoogleしてしまうので、一式まとめたモノを作ってしまおうということです。

日本語が豆腐になる問題

Asciidoctor PDFはAsciiDoc文章からPDF生成するためのRuby製のライブラリです。 スタイルを特に指定せずともそれなりに綺麗なPDFが出力されます。

ただ難点として、日本語の文章をAsciidoctor PDFにそのまま入力すると日本語が全部豆腐になります。 これは既知の問題で回避策もIssue内に明示されています。

github.com

また、Qiitaにも多くの記事が書かれています。

qiita.com qiita.com

毎回やることは自動化しようぜ

上記の通り日本語豆腐問題はググればたくさんの解決策が出てきますが、毎回同じことをやるので自動化しちゃった方が後が楽になります。

実際に手順を自動化して出来上がったテンプレートのリポジトリは↓のようになりました。 github.com

あとはリポジトリをCloneしたりソースコードリポジトリにGitのサブモジュールとして組み込めば、 簡単にPDFドキュメントを生成できます。

テンプレートのリポジトリでは何をやっているのか?

やっているのは以下3点だけです。

  • 必要なライブラリ(Asciidoctor PDFなど)をGemfileにまとめる。
  • PDF生成コマンド一式をシェルスクリプトにまとめる。
  • ライブラリのインストールやシェル実行をMakefileに記述して実行する。

Makefileを一部抜粋すると、以下のように書いています。

.DEFAULT_GOAL := help

setup: ## 依存ライブラリをインストールする。
       @bundle config set path 'vendor'
       @bundle install
       @echo ">> Installed all dependencies"

pdf: ## PDFを生成する。
       @./scripts/build-pdf.sh

help: ## ヘルプを出力する。
       @grep -E '^[/a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | perl -pe 's%^([/a-zA-Z_-]+):.*?(##)%$$1 $$2%' | awk -F " *?## *?" '{printf "\033[36m%-15s\033[0m %-50s %s\n", $$1, $$2, $$3}'

また日本語豆腐問題の対応と同様に、このリポジトリの使い方自体も未来の自分は忘れている可能性が高いです。 なのでMakefile末尾のように、make help を打てば何ができるか一覧で出せるようにしています。

参考: Makefileに書いたコマンド一式をhelpに出す方法 postd.cc


PDFを生成する手順は自動化できたから、次は以前の記事にも書いた文章校正の自動化もこのテンプレートに組み込んでみたい。

以上。

k8sやOpenShiftでKeycloakのクラスタを組むにはHeadless Serviceがいる

最近OpenShiftを触る機会があり、認証・認可サーバのOSSであるKeycloak on OpenShiftでクラスタを組むのにハマりました。 解決のために調べたことをメモ書きします。

TL; DR

  • 外部公開用のServiceを使ってDNS_PINGクラスタを作成しても、Podごとにクラスタができてしまう。
  • 外部公開用のServiceとは別に、クラスタ内のPod間で疎通するためのHeadless Serviceを作成しましょう。

動作確認

CodeReady Containerで動作確認を行いました。 これ以外の環境では別途確認をお勧めします。

  • crc: 1.16.0+bf72d3a
  • OpenShift: 4.5.9

サンプルを流用してクラスタを作成する...しかしできない

まずはKeycloakの公式ドキュメント "Keycloak on Openshift" で紹介されているTemplateを使用してクラスタを作成してみます。

Pod1つで動かす

サンプル通りまずはPodを1つだけ立てるパターンを確認します。 上記のTemplateを oc process コマンドで適用して各種リソースを作成します。

$ oc get po -w
NAME                     READY   STATUS      RESTARTS   AGE
keycloak-demo-1-deploy   0/1     Completed   0          5m26s
keycloak-demo-1-n8gpd    1/1     Running     0          5m23s

ここで作成されたPodのログを確認するとクラスタを作成していることがわかります。 最初に立てるPodなのでほかのクラスタのメンバーが見つからないというメッセージが出力されます。

$ oc logs keycloak-demo-1-n8gpd -f
(...中略...)
07:26:27,828 INFO  [org.jgroups.protocols.pbcast.GMS] (ServerService Thread Pool -- 60) keycloak-demo-1-n8gpd: no members discovered after 3321 ms: creating cluster as coordinator

Podを2つ以上で動かす

上記のTemplateで同じPodを追加すると、同じクラスタに2つのPodが含まれると予想できます。 実際にoc scale コマンドでDeploymentConfigを2つ目のPodを立ち上げます。

$ oc scale dc keycloak-demo --replicas=2

$ oc get po -w
NAME                     READY   STATUS      RESTARTS   AGE
keycloak-demo-1-deploy   0/1     Completed   0          5m26s
keycloak-demo-1-j6p5h    0/1     Running     0          8s
keycloak-demo-1-n8gpd    1/1     Running     0          5m23s
keycloak-demo-1-j6p5h    1/1     Running     0          44s

$ oc logs keycloak-demo-1-j6p5h -f
(...中略...)
07:30:35,764 INFO  [org.jgroups.protocols.pbcast.GMS] (ServerService Thread Pool -- 60) keycloak-demo-1-j6p5h: no members discovered after 3322 ms: creating cluster as coordinator

Podは正常に立ち上がりましたが、上記のログを見ると1つ目のPodと同じように新しくクラスタを作成しています。 2つのPodが1つのクラスタのメンバーになることを期待していますので、期待通りの結果とはなりませんでした。

解決方法

Headless Serviceを作成

結論としてはHeadless Serviceを作成し、以下のようにKeycloakのPodではHeadless Serviceに対してDNSクエリを投げるように設定すれば解決できます。

- apiVersion: v1
  kind: Service
  metadata:
    annotations:
      description: Cluster discovery http port.
    labels:
      application: "${APPLICATION_NAME}"
    name: "${APPLICATION_NAME}-discovery"
  spec:
    ports:
      - port: 8080
        targetPort: 8080
    selector:
      deploymentConfig: "${APPLICATION_NAME}"
    clusterIP: None  # Headless Service
  - apiVersion: v1
    kind: DeploymentConfig
    metadata: ...
    spec:
      replicas: 1
      selector:
        deploymentConfig: "${APPLICATION_NAME}"
      template:
        metadata: ...
        spec:
          containers:
            - env:
                - name: KEYCLOAK_USER
                  value: "${KEYCLOAK_USER}"
                - name: KEYCLOAK_PASSWORD
                  value: "${KEYCLOAK_PASSWORD}"
                - name: DB_VENDOR
                  value: "${DB_VENDOR}"
                - name: JGROUPS_DISCOVERY_PROTOCOL
                  value: dns.DNS_PING
                - name: JGROUPS_DISCOVERY_PROPERTIES  # xxx-discoveryにクラスタ疎通のPINGを投げる
                  value: "dns_query=${APPLICATION_NAME}-discovery.${NAMESPACE}.svc.cluster.local"
                # - name: JGROUPS_DISCOVERY_PROPERTIES
                #   value: "dns_query=${APPLICATION_NAME}.${NAMESPACE}.svc.cluster.local"
              image: quay.io/keycloak/keycloak:11.0.2

この設定はKeycloakのクラスタを組むしくみである JGroup のドキュメントに説明がありました。
JGroupのドキュメントには、KubernetesやOpenShiftでクラスタを構築するにはHeadless Serviceが必要との記載があります。

なお、上記の設定では環境変数 JGROUPS_DISCOVERY_PROTOCOL に何も変更を加えていません。 JGroupのDNS_PINGで使用されるプロトコルがUDPと記載されている記事もあるのですが、現在はTCPがデフォルトになっているためここであらためて設定する必要はありません。

JGROUPS_TRANSPORT_STACK - an optional name of the transport stack to use udp or tcp are possible values. Default: tcp

出典: Reliable group communication with JGroups : 6.4.15. DNS_PING

Podを2つ以上で動かしてクラスタを組む

Headless Serviceを作成してあらためてクラスタを組んでみます。 上記の通り追記・修正したTemplateを再度適用し、Podを2つ立てみます。

# 外部疎通用とクラスタ疎通用の2つのServiceを作成する
$ oc get svc
NAME                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
keycloak-demo             ClusterIP   172.25.250.178   <none>        8443/TCP   4s
keycloak-demo-discovery   ClusterIP   None             <none>        8080/TCP   4s

# 1つ目のPod
$ oc logs -f keycloak-demo-1-mvrfg
07:42:50,796 INFO  [org.jgroups.protocols.pbcast.GMS] (ServerService Thread Pool -- 60) keycloak-demo-1-mvrfg: no members discovered after 3037 ms: creating cluster as coordinator
07:42:51,889 INFO  [org.infinispan.CLUSTER] (MSC service thread 1-1) ISPN000078: Starting JGroups channel ejb
07:42:51,898 INFO  [org.infinispan.CLUSTER] (MSC service thread 1-1) ISPN000094: Received new cluster view for channel ejb: [keycloak-demo-1-mvrfg|0] (1) [keycloak-demo-1-mvrfg]

# 2つ目のPod
$ oc logs -f
07:45:02,173 INFO  [org.infinispan.CLUSTER] (MSC service thread 1-1) ISPN000078: Starting JGroups channel ejb
07:45:02,191 INFO  [org.infinispan.CLUSTER] (MSC service thread 1-1) ISPN000094: Received new cluster view for channel ejb: [keycloak-demo-1-mvrfg|1] (2) [keycloak-demo-1-mvrfg, keycloak-demo-1-vv5jn]

1つ目のPodはクラスタを新規で作成していますが、2つ目のPodは無事に既存クラスタに参加した旨のメッセージがログに出力されています。 これにて一件落着です。


以上。