RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

CloudFrontの署名付きURLを利用したコンテンツ配信

はじめまして、7月に入社しましたサービス開発チームの越です。

今年は初めて東京で年を越したので、せっかくなら東京の郷土料理を食べようと思い、人生で初めてどじょう鍋を食べました。

どじょう鍋(柳川鍋)では、どじょう特有の泥臭さをごぼうが取り除いているとのことでした。

開発者から泥臭い仕事を取り除いてくれる、どじょう鍋で言うごぼうのようなツールやサービスに私も日々お世話になっています。

今回は業務で使用したCloudFrontの署名付きURLによるコンテンツ配信について書こうと思います。

AWS CloudFrontのドキュメントを読んで自分が理解するときにつまずいたところをまとめてみました。

AWS CloudFrontの署名付きURLをこれからはじめて利用する方の助けになれば幸いです。

AWS CloudFrontとは

以下AWSのドキュメントより引用

Amazon CloudFront は、ユーザーへの静的および動的なウェブコンテンツ (.html、.css、.js、イメージファイルなど) の配信を高速化するウェブサービスです。CloudFront では、エッジロケーションというデータセンターの世界的ネットワークを経由してコンテンツを配信します。...

CloudFront自体の説明はAWSのドキュメントを読むのが確実なので、ここでは割愛しますが、軽く全体の概要だけ知りたい場合は以下のAWS Summit Tokyo 2014のセミナーがまとまっていてわかりやすかったです。

AWS Summit Tokyo 2014

セミナーでは以下の3点について話されていて、署名付きURL(Signed URL)を用いたコンテンツ配信についてCloudFrontの利用をするときの概要やTipsについて説明があり、CloudFrontについて最初に理解するときに参照しました。

  • Amazon CloudFrontとは
  • CloudFrontによるサイト高速化
  • CloudFrontによるセキュア配信

署名付きURL(Signed URL)

署名付きURL(Signed URL)とは

有効期限を指定したワンタイムのデジタル署名付きURLでコンテンツにアクセスできるようにする機能で、主にコンテンツを保護する必要がある場面で利用するものになります。

たとえば、特定の会員やコンテンツを購入された方に対して、ダウンロードを許可するような場面になります。

デジタル署名とは

自分の言葉で簡単に説明できないので、以下より説明を引用させていただきます。 https://wa3.i-3-i.info/word14396.html

デジタル署名とは

コンピュータの世界のハンコ

であり

主に他の人に送るファイルにくっつけるデータで「そのファイルは○○さんが作ったやつですよ~」と「そのファイルは悪い人に改ざんされていませんよ~」を証明するものです。

補足として、AWS CloudFrontの署名付きURLで使用するデジタル署名はRSA署名方式が利用されているので、有効期限の設定には気をつけて利用します。

RSA署名方式の場合、その安全性の基礎となっているのは素因数分解問題の困難性であり、計算量的に困難であった素因数分解ができる確率が高まるという意味で、署名が生成されてから時間が経つほど、安全性が低下します。

署名付きURLの作成

実際に署名付きURLをを利用するときには、まず既定ポリシーとカスタムポリシーのどちらを利用するか選択し、あとは選択したポリシーのフォーマットに合わせて署名を作成し、URLを組み立てるだけになります。

署名付きURLの既定ポリシーとカスタムポリシーの選択

まずはAWSのドキュメントにある比較表を引用して、違いを確認していきます。

説明 既定ポリシー カスタムポリシー
ポリシーステートメントを複数のファイル用に再利用できる。ポリシーステートメントを再利用するには、Resource オブジェクトでワイルドカード文字を使用する必要があります。詳細については、「カスタムポリシーを使用する署名付き URL のポリシーステートメントで指定する値」を参照してください。 いいえ                           はい                          
ユーザーがコンテンツへのアクセスを開始できる日時を指定できる。 いいえ はい
(オプション)
ユーザーがコンテンツにアクセスできなくなる日時を指定できる。 はい はい
コンテンツにアクセスできるユーザーの IP アドレスまたは IP アドレス範囲を指定できる。 いいえ はい
(オプション)
署名付きURLにポリシーのbase64エンコードされたバージョンが含まれているため、URL が長くなる。 いいえ はい

動画の方で説明があったのですが、カスタムポリシー(Custom Policy)は既定ポリシー(Canned Policy)を拡張したものとのことです。

既定ポリシー(Canned Policy)

既定ポリシーでは以下2点の設定を行い、署名を作成します。

  • Resource: コンテンツのURL(フルパス)
  • DateLessThan: URLの有効期限切れ日時

これらの項目は既定ポリシーのポリシーステートメントで設定可能な値がこの2つだからになります。

既定ポリシーのポリシーステートメントの例

{
    "Statement": [
        {
            "Resource": "http://xxxxxxxxxxx.cloudfront.net/resources/horizon.jpg?size=large&license=yes",
            "Condition": {
                "DateLessThan": {
                    "AWS:EpochTime": 1357034400
                }
            }
        }
    ]
}

カスタムポリシー(Custom Policy)

カスタムポリシーでは、既定ポリシーの2点に加えて、オプションで以下の2点が設定可能になります。

  • DateGreaterThan: URLの有効期限の開始日時
  • IpAddress: GETリクエストを実行するクライアントの IP アドレス

つまり、カスタムポリシーのポリシーステートメントで設定可能な値は既定ポリシーで指定可能なものと合わせて4つになります。

さらに、既定ポリシーで指定可能な項目に加えて、カスタムポリシーではURLのPATHに対してワイルドカードが指定できるという特徴があります。

そのため、既定ポリシーではコンテンツ1つに対して1つ署名を必ず付けなければいけないというルールがありますが、カスタムポリシーの方を使用すると、特定のパターンのURLに対してすべて同じ署名を利用できるという特徴があります。

既定ポリシーと比較したときのカスタムポリシーの特徴として、比較表に記載があるようにURLが長くなるというのがあります。

既定ポリシーのポリシーステートメントの例

{
    "Statement": [
        {
            "Resource": "Resource": "http://xxxxxxxxxxx.cloudfront.net/resources/*",
            "Condition": {
                "IpAddress": {
                    "AWS:SourceIp": "192.0.2.10/32"
                },
                "DateGreaterThan": {
                    "AWS:EpochTime": 1357034400
                },
                "DateLessThan": {
                    "AWS:EpochTime": 1357120800
                }
            }
        }
    ]
}

既定ポリシーとカスタムポリシーの選択

選択の基準としては、以下の3点のいずれかの用途がある場合にはカスタムポリシーを利用し、どれも利用しないのであれば既定ポリシーを選択することになると思います。

  • URLの有効期限の開始日時を指定したい
  • GETリクエストを実行するクライアントの IP アドレスを制限したい
  • URLのPATHにワイルドカードを使用して、複数のリソースに対して1つの署名でアクセスしたい

署名付き URL の作成

キーペアの作成

事前準備として、署名を生成する際に必要になる秘密鍵と、CloudFrontに登録する公開鍵を作成します。

AWSのドキュメントを参考に作成していきます。

以下のコマンドで、秘密鍵を生成し、private_key.pem という名前のファイルに保存します。

openssl genrsa -out private_key.pem 2048

以下のコマンドで、上記のコマンドで生成したprivate_key.pemという名前のファイルから公開鍵を抽出します。

openssl rsa -pubout -in private_key.pem -out public_key.pem

パブリックキーをCloudFront にアップロード

パブリックキーを CloudFront にアップロードするには」を参考に、公開鍵をCloudFrontにアップロードします。

このとき生成されるパブリックキーIDをメモしておきます。 このIDは、このあと署名付きURLを作成するときに、Key-Pair-Idフィールドの値として使用します。

CloudFrontの設定

プライベートコンテンツをCloudFrontの署名付きURLで供給するように設定するには、以下のタスクを実行します。

プライベートコンテンツ提供のためのタスクリスト

署名付き URLの作成(既定ポリシー)

awscliのreferenceを参考に署名付きURLを生成します。

https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudfront/sign.html

aws cloudfront sign \
    --url https://xxxxxxx.cloudfront.net/private-content/private-file.html \
    --key-pair-id XXXXXXXXXXX \
    --private-key file:///.../private_key.pem \
    --date-less-than 2020-11-18T19:30:00

生成されたURLのクエリパラメーターに以下の3つが設定されているので、既定ポリシーで作成されているのがわかります。 - Expires - Signature - Key-Pair-Id

https://xxxxxxx.cloudfront.net/private-content/private-file.html?Expires=1605727800&Signature=XXXX...&Key-Pair-Id=XXXXXXXXXXX

署名付きURLの作成(カスタムポリシー)

コマンドのオプションで有効期限の開始日を指定すると、URLにポリシーのbase64エンコードされたバージョンが含まれているので、カスタムポリシーで作成されているのがわかります。

aws cloudfront sign \
    --url https://xxxxxxx.cloudfront.net/private-content/* \
    --key-pair-id XXXXXXXXXXX \
    --private-key file:///Users/.../private_key.pem \
    --date-greater-than 2020-11-1T19:30:00 \
    --date-less-than 2020-11-18T19:30:00

URLのパスにワイルドカードを指定したので、生成された署名を使用して、https://xxxxxxx.cloudfront.net/private-content/*のパターンにマッチする複数のリソースに対してアクセスできます。

https://xxxxxxx.cloudfront.net/private-content/*?Policy=xxxx...&Expires=1605727800&Signature=XXXX...&Key-Pair-Id=XXXXXXXXXXX

確認と理解のために署名の有効性を検証してみる

生成されたURLでコンテンツにアクセスできるかどうか確認したら終わりですが、URLのクエリパラメーターにExpiresとKey-Pair-Idがあり、私は最初なぜこれが必要なのかわからなかったです。

CloudFront はパブリックキーを使用して署名を検証し、URLが改ざんされていないことを確認します。署名が無効である場合、リクエストは拒否されます。

ExpiresとKey-Pair-Idが署名の検証に使われていると分かると疑問が解決したので、署名付きURLを検証を実装してみます。

既定ポリシーで作成した署名付きURLを検証

https://xxxxxxx.cloudfront.net/private-content/private-file.html?Expires=1605727800&Signature=MOEBCVLfRO5CVOglBi9t-dUqI4RZpc5tqOZRwZceD5AHMLTTNFPP2WpXqw~UtpmkB1FF-t8wgiCzIQ~Qvd8hbOIffyb9Dh1I1saxN7HVpoLh99GwwV~LWZdGM3GdO3H6f~IqxLukTqnQf2F5xKrkD-g62Lyc3ShHRnIBOeug7seJpL3~hmFak2mUAa1NaFX2cw-XoZcXYqbi6RbK7hgxliXiLGcTa-D6FO3~r3qxohy5KxLR4A-fW-1gnRblaXUwFVp8eLG9Pvly5D5cH1NQMqgR9CAclJSYNM1ov-wUXQiwbkHMqW1m90KGxdQWluZLfdEHnrFwAUk40EOYfoUHMg__&Key-Pair-Id=XXXXXXXXXXX

既定ポリシーで作成した上記の署名付きURLを検証してみます。

署名のデコードや検証部分を実装するときに、署名生成のサンプルコードを参考にしながら実装しました。

<?php

// 公開鍵のファイルのPATH
$publicKeyFilename = './public_key.pem';
// プレミアムコンテンツのURL
$resource = "https://xxxxxxx.cloudfront.net/private-content/private-file.html";
// 有効期限(EpochTime)
$expiration = 1605727800;
// 署名(URLセーフなbase64でエンコードされてる)
$encoded_signature = "MOEBCVLfRO5CVOglBi9t-dUqI4RZpc5tqOZRwZceD5AHMLTTNFPP2WpXqw~UtpmkB1FF-t8wgiCzIQ~Qvd8hbOIffyb9Dh1I1saxN7HVpoLh99GwwV~LWZdGM3GdO3H6f~IqxLukTqnQf2F5xKrkD-g62Lyc3ShHRnIBOeug7seJpL3~hmFak2mUAa1NaFX2cw-XoZcXYqbi6RbK7hgxliXiLGcTa-D6FO3~r3qxohy5KxLR4A-fW-1gnRblaXUwFVp8eLG9Pvly5D5cH1NQMqgR9CAclJSYNM1ov-wUXQiwbkHMqW1m90KGxdQWluZLfdEHnrFwAUk40EOYfoUHMg__";

function verify($policy, $signature, $publicKeyFilename) {
    $fp = fopen($publicKeyFilename, "r");
    $publicKey = fread($fp, 8192);
    fclose($fp);
    $publicKeyId = openssl_get_publickey($publicKey);

    return openssl_verify($policy, $signature, $publicKeyId);
}

function decode($policy) {
    return base64_decode(strtr($policy, '-_~', '+=/'));
}

function createCannedPolicy($resource, $expiration)
{
    return json_encode([
        'Statement' => [
            [
                'Resource' => $resource,
                'Condition' => [
                    'DateLessThan' => ['AWS:EpochTime' => $expiration],
                ],
            ],
        ],
    ], JSON_UNESCAPED_SLASHES);
}

$signature = decode($encoded_signature);
$policy = createCannedPolicy($resource, $expiration);

$result = verify($policy, $signature, $publicKeyFilename);

if ($result == 1) {
    echo "正しいです";
} elseif ($ok == 0) {
    echo "正しくありません";
} else {
    echo "署名を確認する際にエラーが発生しました";
}
echo PHP_EOL;

公開鍵

手元で確認するので、コードでは秘密鍵から抽出した公開鍵のファイルのpathを使用しています。

AWS上では公開鍵はKey-Pair-Idと紐付いています。

<?php

$publicKeyFilename = './public_key.pem';

URLのクエリパラメータから取得した署名のデコード

URLのクエリパラメータから取得した署名はbase64エンコードされ、一部URLで無効な文字が置き換えられているので、以下の処理でデコードします。

<?php

function decode($policy) {
    return base64_decode(strtr($policy, '-_~', '+=/'));
}

署名を作成するときに使用したポリシーステートメントJSONを生成

既定ポリシーのポリシーステートメントjsonの生成に必要なものは、コンテンツのURLと有効期限の2つでしたので、以下の処理でjsonの文字列を生成します。

<?php

function createCannedPolicy($resource, $expiration)
{
    return json_encode([
        'Statement' => [
            [
                'Resource' => $resource,
                'Condition' => [
                    'DateLessThan' => ['AWS:EpochTime' => $expiration],
                ],
            ],
        ],
    ], JSON_UNESCAPED_SLASHES);
}

既定ポリシーを使用する署名付きURLの作成

署名の検証

PHP を使用したURL署名のサンプルコードでは、openssl_signで署名が作成されていたので、 phpopenssl_verifyで署名の検証を行います。

openssl_verifyでは以下のようなロジックで検証が行われていると思います。

  1. デコードした署名($signature)を、公開鍵を使って復号し、ハッシュ値を取得
  2. 既定ポリシーのポリシーステートメントjsonの文字列($policy)をハッシュ化して、ハッシュ値を生成
  3. 公開鍵で復号したハッシュ値jsonから生成したハッシュ値を比較し、完全に一致することを確認
<?php

function verify($policy, $signature, $publicKeyFilename) {
    $fp = fopen($publicKeyFilename, "r");
    $publicKey = fread($fp, 8192);
    fclose($fp);
    $publicKeyId = openssl_get_publickey($publicKey);

    return openssl_verify($policy, $signature, $publicKeyId);
}

デジタル署名の検証

署名の検証結果では以下のようなことが確認できます。

デジタル署名の検証が成功すると、以下のことが確認できます。
(1) メッセージが改ざんされていないこと
(2) メッセージは、検証に使用した公開鍵と対になる秘密鍵によって署名されたこと

ダイジェストが一致せずに検証が失敗すると、以下のいずれかの事象が発生したことが確認できます。
(1) メッセージが改ざんされたこと
(2) デジタル署名が改ざんされたこと

なお、メッセージとデジタル署名のどちらが改ざんされたかまでは分かりません。

https://www.ipa.go.jp/security/pki/024.html

1点注意点としては、有効期限が改ざんされていないことは検証できますが、有効期限切れかどうかは署名の検証では行っていないので、有効期限切れかどうかの判定については、署名の検証とは別で行います。

有効期限切れかどうかの判定を行うために、クエリパラメータにはExpiresとして有効期限を渡す必要があるのだと理解しました。

まとめ

CloudFrontの署名付きURLの手軽さと便利さがすごい!

参考

年末なのでみんなの質問に答えてみた

特に学びのある記事ではないので暇な人とレアジョブに興味ある方向けです(笑)。

お世話になっております。レアジョブの山田です。CTOをやっています。
今年最後のブログ担当で、連続投稿です。

今年を振り返る

f:id:ymdrock:20201220164823j:plain

今年は、コロナ禍によってニューノーマルと呼ばれる程、社会が一変しました。 企業としても個人としても様々な変化が求められています。

この状況下でレアジョブは、2020/11/20に東証一部へ市場変更を行なうことが出来ました。 会社として、また個人として社会的責任を果たせるように、グループビジョン「Chances for everyone, everywhere」の実現に向けて邁進して行きたいと思います。

東証一部へ市場変更以外ではProgosをリリースしました。ProgosはCEFRで英語スピーキング力を図るサービスで、AIによる自動採点の導入で瞬時の判定とスキルに応じた細かなフィードバックで効率のよい学習に繋げることが出来ます。

また、中長期戦略実現に向けて、様々な市場ニーズ、事業拡張に応えていくために、設立当初から稼働するレアジョブ英会話のシステムを抜本的にリプレイスする意思決定をしました。いくつかのPhaseに分けて進めており、最初のPhaseの着地が見えてきました。引き続き慎重かつ迅速に進めて行きたいと思います。

質問を受け付け、すべてに答える

まだまだ、エンジニアの募集をしていますので、このブログを通じて社内の雰囲気がもっと伝わればなと思いと、また最近全員とコミュニケーションを取る機会が少なかったので、聞いてみたいこと、疑問など質問を募集しました。
思ったより質問が少なったのですが(笑)、来た質問にすべて回答したいと思います。
来年はもっと興味を持って頂けるように努めます。。。

最近 注目している技術について

ノーコード/ローコード開発ツールですね。GoogleのAppSheet買収、AWSAmazon Honeycodeリリースなどの動きでこの領域はさらに加速していくでしょう。 用途に応じた開発ツールが数多くリリースされており、レアジョブでもどう活用するかを慎重に検討をしております。
個人的には、bubbleを触り始めました。

一緒に働きたい(or社内にマッチしそうな)エンジニアはどんな人と考えているか?

スキル的なのは別として、グループビジョン、サービスミッションに共感出来る、Rarejob Wayを体現してくれる人に尽きますね。

www.rarejob.co.jp

最近、読んだ本について(技術書・実用書・小説etc ジャンルは問わず)

実業務でたまにGolangを書いたりしていますので技術書も必要に応じて読みますが、最近は組織論や企業カルチャーに関わる本を読むことが多いです。
最近では、NO RULES(ノー・ルールズ) 世界一「自由」な会社、NETFLIXが非常に興味深かったです。

コロナ禍で変わった身の回りのことなど(はじめたことや止めたことなど)

コロナ以前は、週末は毎週のように家族で遠出していましたが、Go To トラベルも使わなかったですし全く遠出していないです。
落ち着いたら温泉行きたいです。

座右の銘など、大切にしている言葉について(意味も教えて欲しいです)

「常に謙虚でいること」です。
自分自身を常に客観視して、みなから多くのことを学び、意思決定を最善なものにしていきたいと考えています。
今CTOという重責を任せられているからこそより謙虚でいたいです。

きのこの山たけのこの里どっち派か?(理由もあると嬉しいですw)

どっちでもいいです(笑)
敢えていうなら、購入の際に在庫が多い方を買います。

普段、どういうメディアで技術動向などを追っているのか?

全体の流れであれば日経、IT Media、Tech Crunch、CNETなどで、あとはSNSはてブあたりを見ながら気になった情報を調べる感じですね。

好きな曲(アーティスト名などでもOKです)

季節性も加味すると、Awich - Happy X-mas (War Is Over)がよかったです。
MVもかっこいい。

一番好きな言語を教えてください

日本語。。。開発言語と解釈しますね。レアジョブでも導入しましたがGolangです。
煩わしいことも多々ありますが、書いてて楽しいです。

最近、買った高価なもの

スケボーかな。コンプリートでなく自分で組み立てたので23,000円くらいしました。
しかし、うまくならない...

尊敬する人物について

自分では出来ないことをやれる人、自分にない発想を持った人になりますかね。
仕事面では、現レアジョブを含めてこれまでのキャリアでそういった人達と働けたことは運がいいなと思っています。

レアジョブに入って他の会社よりも最も優れていると思ったところはなんですか?

レアジョブ入社まで3社に在籍していましたが、いずれも規模も環境も違うので比較が難しいですし、 他社と比べる必要はないと思うので、技術本部に絞っていいなと思っていることは挙げます。

  • 横の繋がりが強い
    • 私が至らないことがあっても横連携して組織で様々な課題を解決してくれる(感謝)
  • 組織学習の施策が継続出来ている
    • 勉強会
    • 社内情報共有ツールの活用
    • レビュー会
    • ブログ
  • 言語化・ドキュメント化を徹底している
  • 入社時に英語が出来ないくても英語で仕事する機会がある
    • 自社サービスでそれが実現出来ることも証明してくれている
  • みんな勉強家で技術が好き
    • 自分だけでなく周りを巻き込んで学ぼうというも姿勢もいい

他にも色々あるので、興味ある人は遊びに来て下さい(笑)

お子さんへのクリスマスプレゼントを教えてください

こんなの知りたいですか(笑)?

  • 長女(10歳) - ミシン ※最新型を所望されてました
  • 次女(8歳) - Swicthのすみっコぐらしのゲーム

最後に

とりとめない記事を読んで頂きありがとうございました。
メリークリスマス、よいお年を、また来年会いましょう。

Rarejob Tech Blogを見て、興味持って頂いた/頂いている方へ
エンジニア絶賛募集中です。オンラインでのカジュアル面談も実施しているので、お気軽にお問い合わせ下さい。 appeal.rarejob.co.jp

Swaggerで定義したRestAPIをOWASP ZAPでScanする

お世話になっております。
レアジョブの山田です。CTOをしています。

ブログの登場は年1で年度初めにという決まりだったはずですが、 圧力により今年最後のブログを担当させて頂くことになりました。

現在、レアジョブではレアジョブ英会話のシステムリプレイスを行っております。 このリプレイスでマイクロサービスアーキテクチャへ変更していきます。 私自身も移行関連タスクを担っており、RestAPIの脆弱性診断のタスクを割当てられ、Scanning APIs with ZAPに沿ってRest API脆弱性診断を実施したので共有したいと思います。

ZAPとは

ご存知の方も多いかと思いますが、 Zed Attack Proxy(ZAP)は、The Open Web Application Security Project(OWASP)が提供するWeb脆弱性診断のOSSです。

また、OWASPから数年に一度、OWASP Top10として認識すべき重大セキュリティリスクも発表しています。

owasp.org

OpenAPI Support

The API scanning script is an easy way for you to automate security scanning of APIs defined using OpenAPI/Swagger or SOAP.

現在開発中のAPIはSwaggerで定義しており、Scanの自動化のためSwaggerがサポートされているのでシナリオ作成する必要がないのでテストがぐっと楽になります。

実施環境

OWASPからZAPのDockerイメージが提供されているので、クライアントマシンのDockerでZAPを立ち上げ、API ServerへScanを行ないます。 ZAPの起動は、docker-compose.ymlに定義しておきましょう。

version: '3'

services:

  zap:
    image: owasp/zap2docker-weekly
    command: zap.sh -daemon -host 0.0.0.0 -port 8080 -config api.disablekey=true -config api.addrs.addr.name=.\* -config api.addrs.addr.regex=true
    ports:
      - "8080:8080"

準備

Scanの準備として、以下の2つを準備します。 なお、これらのファイルは、/zap/wrkというディレクトリに格納します(scanスクリプトで定義されている)。

リクエスト変更ルールは、例えばAPIサーバにはAPI Keyが必要な場合があります。このような場合、正しいAPI Keyが設定されたリクエストでないとテストが成立しません。 そこで以下のようにScan時に送られるリクエストの特定の値を書き換えます。この例では、HTTP HeaderとPostの一部のフィールドを置換する内容になります。

 replacer.full_list(0).description=Authorization
 replacer.full_list(0).enabled=true
 replacer.full_list(0).matchtype=REQ_HEADER
 replacer.full_list(0).matchstr=Authorization
 replacer.full_list(0).regex=false
 replacer.full_list(0).replacement=Bearer abcdefg
 replacer.full_list(1).description=api key
 replacer.full_list(1).enabled=true
 replacer.full_list(1).matchtype=REQ_HEADER
 replacer.full_list(1).matchstr=X-Api-Key
 replacer.full_list(1).regex=false
 replacer.full_list(1).replacement=1234567890
 replacer.full_list(2).description=Transaction ID
 replacer.full_list(2).enabled=true
 replacer.full_list(2).matchtype=REQ_HEADER
 replacer.full_list(2).matchstr=X-Transaction-ID
 replacer.full_list(2).regex=false
 replacer.full_list(2).replacement=sample
 formhandler.fields.field(0).fieldId=message
 formhandler.fields.field(0).value=this message was changed forcibly
 formhandler.fields.field(0).enabled=true

実施

Scanは、zap-api-scan.pyを実行します。オプションとしてAPIフォーマットとSwaggerファイル名を指定します。 レポートは、定義ファイル同様/zap/wrkに出力されます。 デバッグメッセージ表示、レポートフォーマットなどオプションは他にもあるので、ヘルプをご確認下さい。

docker exec <container id> ./zap-api-scan.py -t <Swaggerファイル名> -f openapi -r <レポートファイル名> -z "-configfile /zap/wrk/<定義ファイル名>"

まとめ

Swaggerで定義されたAPIに対しては、zap-api-scan.pyで簡易にAPI脆弱性診断が出来ることはわかりました。また、定期的に実施することで品質の担保に繋がります。 ただ、ZAPだけでは完全とは言えないので、システム特性に合わせて専門家による第三者診断も必要だと考えています。

では。

Chromebookってどうなの?

お久しぶりです、ディズニーをこよなく愛する岩堀です。みなさん再開後ディズニーには行けていますか? 私は毎月のようにチケット争奪戦をくぐり抜け、つい先日に新アトラクションをすべて体験できました!

そんな体験をレポートしようかと思いましたが、そちらは別の機会で別の体験をレポートしたいと思います。 今回は弊社情シス部門にて導入検討しているChromebookについて、話をしてみたいと思います。

きっかけ

弊社では通常、WindowsMacマシンを基本的に社員に割り当てておりましたが、 これらの端末を社員に配布するまでの準備にかなりの工数を必要としています。 この部分を長年課題として考えておりましたが、具体的な解決ができずにいました。

そのような中で弊社は今年度の初めに全社的にGoogleWorkspace(旧GSuite)の導入を進めていくことになり、徐々にGoogleツールを多用できる状況となってきました。ある程度Googleツールが利用できる環境下の中で、どの程度Chromebookにて業務を行えるのか検証を行いたいと思っていました。 そんなタイミングで私が使用しているMac端末のバッテリー交換が必要となったので、交換完了までの間、Chromebookを使って通常行っている業務がどこまで行えるか検証してみました。

私自身、自宅ではWindowsMacChromebookの3種を用途によって使い分けていたので、Chromebookへ端末を変えることには大きな抵抗もなく臨めましたが、どこまで同じように業務を行えるのかは若干の不安を持ちながら臨んでみました。

参考に私が普段端末を利用して行っている作業は主に以下になります。

  • 各種Webアプリケーションでの対応
  • ZoomやMeetを利用しての会議参加
  • MSOfficeドキュメントの閲覧/編集
  • gitによるソース管理
  • IDEを使ったソースコード閲覧・編集
  • CLIによるサーバ業務
  • Windowsサーバへのリモートアクセス

移行までの時間は?

まず代替え機となるChromebookに移るまでに要した時間ですが、こちらは正直に申しまして0分でした。 メールに関してはGmailを利用しているのと、ローカルファイルに関しては既にGoogleファイルストリームにて GoogleDriveをローカルドライブのように扱っていったことで、端末移行を行う必要がなかったです。

各種Webアプリケーション利用は?

次に業務に欠かせない各種Web アプリケーションですが、弊社ではコミュニケーションツールとしてChatworkとSlack、社内共有ドキュメントツールとしてconfluence等、多くのWebアプリケーションを使っています。基本的にChromeが使えるWebアプリケーションであれば、問題なく利用ができるので、こちらで問題なることはなかったです。また普段アプリベースで使っているもので、通知を表示させていても、WebPush通知が使えるものも多いので、特に違和感なく利用でき、業務で問題となることはなかったです。

Web会議ツールに関しても、MeetやZoomはブラウザのみで利用が可能なので、問題なく会議を行うことができました。

Officeは使えないのでは?

次にMSOfficeドキュメントですが、こちらはMacのときからOffice365を利用しており、引き続きこちらを使いました。ChromebookAndroidベースで作られているため、基本的にPlayストアからアプリを落とすことができます。そのためOffice365をPlay ストアからインストールを行うことで、Chromebook上でもOfficeドキュメントを問題なく扱うことができます。もちろんマクロで作りこんであるExcelも問題なく扱うことができました。

IDEとかはないでしょう?

gitやIDE周りは、Chromebook上でLinuxを起動させて、IDEを起動させることが可能なので、そちらで対応いたしました。 Chromebookの設定の中でLinuxを有効にすることで、Chromebook上でLinuxマシンがVMとして立ち上げることができます。 support.google.com

これによりLinuxを立ち上げることができ、その上でgitやIDEをインストールし、使うことができるようになります。今回は詳細なインストール方法は省略しますが、gitやIDEを使用することができるようになることで、コード管理、修正、レビュー等も問題なく対応が行えました。

CLI操作がしたいときは?

ターミナル操作が必要となるサーバ業務は先程のLinux機能を利用すると合わせてターミナルを使うことができます。こちらの方法でも問題はないのですが、ローカルリソースを上へのファイルダウンロードやアップロードに関してはVM上での作業になり、その後の業務に移る際に若干面倒であるため、私はChromeのウェブストアからインストールできるブラウザアプリのSecure Shell Appを使いました。

chrome.google.com

こちらを使うとブラウザ上でSSH操作が行え、ローカルリソースを扱えるので、ファイルのダウンロードやアップロードにてリソースを直接操作することができるので、とても便利です。

リモートデスクトップは無理でしょう?

最後にWindowsへのリモートデスクトップ操作ですが、こちらもPlay ストアよりMS製のリモートデスクトップアプリをインストールすることができ、使用することが可能となりますので、こちらですべてが対応できるようになります。リモートデスクトップアプリとしてはVNCもあるので、AWSのEC2インスタンスとして立ち上げることができるようになったMacOSにもリモートアクセスが可能になります。

感想

結果として一週間ほどChromebookを業務で使っておりましたが、大きく困ったことは発生しませんでした。逆に起動が早いということもありますし、固まることも殆どないので、業務効率は上がったと思われます。WindowsMac端末が必要な場合にはAWSのWorkspacesやEC2インスタンスで立ち上げたマシンにリモートアクセスすれば対応もできるので、大きく問題になることはないと思われました。

Office製品に関してはほとんどGoogleDocやSpreadSheet等に移すことも可能ですので、ドキュメント周りはそちらに寄せていくことも可能になると考えられます。Excelマクロの互換性の問題に関しても、先日Googleから対応したアドオンの発表もあり、より一層移行が進められると考えられます。 forest.watch.impress.co.jp

弊社では今後Chromebookを徐々に取り入れて行こうと考えておりましたが、今回の検証でより一層、早めに進めていきたいと感じました。 通常業務の影響も少ないことがわかり、逆に業務効率が上がる部分もあることがわかりました。キッティングの面から見ても、ログインするGoogleアカウントの設定の内容が端末に反映されていくので、端末準備にそれほど時間を必要としないことにもメリットを感じました。

みなさんも、一度はChromebookを使ってみて便利さを感じてみてください。ちなみに弊社サービスでありますレアジョブ英会話のレッスンルームも、まだChromebookを推奨環境としていないですけど快適に使うことはできるのを確認しています。私自身レッスンを受けるときは検証も兼ねてChromebookを利用しておりますので、推奨環境に含まれましたらぜひご利用を試してみてください。

スクラムガイド2020が伝えたいこと その1

はじめに

こんにちは。レアジョブのサービス開発チームの三上です。

先日スクラムガイドが3年ぶりにアップデートされました。 私が初めてスクラムマスターを担当したのが2018年だったので、 これまで何度も読み返してきましたし、時には他のスクラムマスターと読みあわせを実施してきたので 自分にとっての教科書が更新されたようなもので、これは個人的には大きな出来事でした。

この記事では、その変更点について自分なりの意見・理解を混じえながら語りたいと思います。

2017年版からの変更点

前回との変更点については公式のスクラムガイドにも記載がありますので、 詳しくはスクラムガイド2020をご確認ください。

いくつかの変更点の中で、この記事では以下の2つの変更点について注目したいと思います。

  • 指示的な部分を削減(デイリースクラムでの質問削除)
  • プロダクトゴールの導入

変更された内容について、公式のガイドではこのように説明されています。

スクラムガイドは時間が経つにつれて少し指示的なものになっていた。2020 年版では、指示的 な表現を削除または緩和して、スクラムを最小限かつ十分なフレームワークに戻すことを目的 としている。たとえば、デイリースクラムの質問の削除、PBI(プロダクトバックログアイテム) の属性に関する記述の緩和、スプリントバックログにあるレトロスペクティブのアイテムに関する記述の緩和、スプリントの中止のセクションの削減などを実施した。

この「デイリースクラムの質問の削除」についてですが、これまでのスクラムガイドでは

  • 開発チームがスプリントゴールを達成するために、私が昨日やったことは何か?
  • 開発チームがスプリントゴールを達成するために、私が今日やることは何か?
  • 私や開発チームがスプリントゴールを達成する上で、障害となる物を目撃したか?

のようにあくまで「例」として説明されていました。

しかし、私自身いくつかのチームを立ち上げる際にはまずはこの例を参考にやってみることが多かったですし、 実際に多くのチームが同じようにデイリースクラムを行っていたのではないでしょうか。

なぜ変更されたのか?

まず私がこのデイリースクラムの質問の削除されたことを目にした時にすぐに「なるほど」と納得しました。

なぜかと言うと私自身も開発メンバーとして、スクラムマスターとしてデイリースクラムに参加した経験の中で、 この3つの質問をすることだけをこなしてしまい、目的を見失ってただただ報告し合うデイリースクラムを何度も目撃してきたからです。

では何が重要かと言うと、 各メンバーがデイリースクラムで達成したい目的を理解して実施することだと考えています。

デイリースクラムの目的を理解する上で重要だと思っているのが「スプリントゴール」の理解です。

今回の変更でもう一点合わせて説明しておきたいのが、 「プロダクトゴールの導入」についてです。

以下のように、ブレイクダウンして順に説明していきます。

プロダクトゴール>スプリントゴール>デイリースクラム(24時間ごとの計画)

まず、スクラムチームを組んでいるからには達成したいプロダクトのゴールがあるはずです。 無いとは思いますが、ゴールを知らないまたは無い場合は今すぐにプロダクトオーナーと会話しましょう!笑

プロダクトゴールを達成するための成果物は「プロダクトバックログ」となります。

スプリントプランニングでは、プロダクトバックログからアイテムを優先度順にスプリントバックログに移してスプリントの計画を立てます。 プランニング時には、そのスプリントが完了した時にどんな状態になっているか、開発チームはそれを説明出来ないといけません。

そして、スプリントプランニングの成果物は「スプリントバックログ」となります。各スプリントではスプリントゴールを設定します。

では改めてデイリースクラムの目的ですが、スクラムガイド2020では

デイリースクラムの目的は、計画された今後の作業を調整しながら、スプリントゴールに対する進捗を検査し、必要に応じてスプリントバックログを適応させることである。

とされています。 スプリントゴール達成のための、再計画の場であると言うことです。

説明が長くなってしまいましたが、 なぜデイリースクラムの質問がなくなったかと言うと、 本来の目的がゴール達成のための検査と適応の場であるはずが 目的を理解せず、具体的な3つの質問だけをする進捗報告の場になってしまうケースが多かったからではないかと考えています。

レアジョブでは?

理想はありつつも、レアジョブのスクラムチームでもTRY&エラーしながらやっています。

デイリースクラムもそうですが、各スクラムイベントにおいて、 HOW(どうやって達成するか)の部分って議論が盛り上がりませんか? やりたい事や課題解決をどう達成するかはエンジニアの得意とする所でもあるが故に、 弊社でもよく議論が盛り上がります。笑

そんな時に重要になってくるのがWHY(なぜそれをやるのか)という観点だと思います。 これを念頭において、どう実現するのがベストなのかやり方について話し合うと答えが見えてくるかも知れません。

最後に

今回はデイリースクラムを中心にお話しましたが、 スクラムにおいて改めて強調したいことは以下の通りです。

  • 軽量なフレームワークである
  • 意図的に不完全であり、まずはそのまま試そう
  • 役割、イベントの目的を理解しよう
  • 詳細な指示はないのでチームで考えよう(自己管理しよう)

スクラムガイド2020が伝えたいこと その1として書かせていただきましたが、 その2があるのか無いのか、乞うご期待です。

以上です。それではまた!

Amazon EC2 Mac Instances が来た!

aws.amazon.com

来ましたね。フォートナイトのチャプター2シーズン5も来たので本当はこの話を6万字書きたいんですが、Amazon EC2 Mac Instance も同じくらい見逃せないのでアプリエンジニア観点で何がこれで解決されるか・難しいかを書いてみようと思います。

難しいと思う点

iPhone/Macアプリ開発においてはMacOSが必須になるため BitriseCircleCI のようなSaasを利用したり、自前でMacPro/Miniなどを用意して Jenkins + xcodebuild コマンドを使ったり Xcode Server でビルドします。

今回出た Amazon EC2 Mac Instance がこれを代替できるかという期待が一つあると思いますが、乗り越えるべきハードルがいくつかあるのではないかと考えています。

Xcodeのバージョン依存

iPhone/Macアプリ開発の課題の一つに「Xcodeのバージョン依存」という課題があります。 Xcodeは毎年アップデートがあり、かつアプリの開発はXcodeへのバージョン依存が強いので、「AのアプリはXcodeのバージョン11.4じゃないと動かない」といったケースが多々あります。XcodeはNodeモジュールのように軽量とは言い難く(17GBとかある)、そのため今回どうプロビジョニングされるかが気になるところです。

Amazon EC2 Mac Instance でこれが解決されているような記述は今の所見られないため、自身でEC2を立ち上げて、最初はリモートデスクトップで入り手動でセットアップが必要に思います。

only pay for actual usage with AWS’s pay-as-you-go pricing

とあるのでそれで実用時は適宜立ち上げるというのもいいと思います。

証明書・プロビジョニングまだまだ面倒問題

fastlaneの登場で、この辺りはAppleIDさえあればアプリ開発に関するセットアップや証明書管理などがCLIからかなり楽に管理できますが、まだまだ詰まり所や理解しにくい設定の多い部分です。この辺りの設定やAppleIDとの結合度の高いCI/CDシステムを自身でメンテしていくのは結構体力を使います。

結局CI/CD便利すぎる問題

Bitrise も Circle CI もできることの割に安いんですよね・・・なので手間を使って乗り換えるかが微妙なところです。Bitrise も Circle CI も必要があればsshもできるし、リモートデスクトップはできないですがCI/CDにおいては必要ないので、十分です。また上記の「Xcodeバージョン問題」も新旧バージョン指定可能になっており解決できます。WordFlowやIntergrationも充実してて自分でやることはリポジトリと接続したり証明書などを設定する程度です。

たとえばセキュリティ都合などでどうしてもこれが導入できないケースやネットワーク上これが難しいようなケースでない限りはこれらを導入することで多くの問題は解決します。

*追記、Bitriseもリモートデスクトップできるようです!

期待したい点

CI/CD以外の用途や、限定的な状況ではこれまでできなかったことができるようになる期待があります。

リモートデスクトップとしての用途

情シス観点で考えると、開発者全員がMacを持たないといけない状況はセキュリティや費用面、セットアップの手間からできれば避けたいところです。もちろん開発的なリターンはありつつも ないに越したことはない・・・

例えば Chromebook + Amazon EC2 Mac Instance + AWS Remote Desktop Gateway で端末や負荷を気にすることなくiOS/MacOSの開発環境が実現できるかもしれません。

aws.amazon.com

また1つiOSアプリの開発可能なインスタンスを用意しておけば、iPhone/iPadのシミュレーターが利用できるので、全員の環境に準備しなくても共有して検証ができます。

利用可能なMac miniのプロセッサは第8世代のCore i7プロセッサ(物理6コア/論理12コア)、3.2GHz(ターボブースト時4.6GHz)32GBメモリとなっており高スペックですし、2021年にはM1チップ搭載の端末も利用可能になるとのことでとても楽しみです。

ネットワーク要件が厳しいUIテストの実施

たとえば「UIテストを実際のAPIを叩きながら実行したい」「APIは本番でなく社内やIPの制限された検証環境のものを使いたい」みたいなケースであれば Amazon EC2 Mac Instance で解決できるかもしれません。あまり多くはないと思いますが、リクエスト元IPや証明書などの制限のあるリクエストをUIテスト中に実施したいケースは解決できそうです。

AWSのネットワーク上でMacOSが動く」のメリットの一つだと思います。

まとめ

今回のリリースで「さぁ!CI/CD入れるぞ!」とまではならないですが、プロビジョニングや連携の改善が今後出てくると乗り換える未来は遠くないように思います。フォートナイトのチャプター3が出ることにはそんな未来になっているかもしれないですね。 緩く触りながら用途を探っていこうと思います。

AWS SAM で Serverless な環境を構築する

どうも、DevOps チームの うすい です。
トトロも鬼滅の刃 無限列車編も見たことがありません。

今回新しいシステムを aws-sam-cli を用いて構築したので簡単にですがそれらの内容を記述したいと思います。AWS SAM 自体の説明は割愛します。

私のマシンの aws cli などのバージョンは下記となります。

$ aws --version
aws-cli/1.16.79 Python/3.7.1 Darwin/19.6.0 botocore/1.12.69

$ sam --version
SAM CLI, version 1.6.2

余談ですが aws-sam-cli のバージョンアップの速度はすごいですね。

AWS SAM では CloudFormation っぽいテンプレートファイルと samconfig.toml ファイルを使用します。samconfig.toml はsam deploy --guided時に生成することもできますが今回は作成しておきます。
あまり情報は見かけませんが、samconfig.toml で ステージング環境 / 本番環境 といった環境に応じたパラメータが適用されるようにします。雰囲気は下記です。

version = 0.1

[default.build.parameters]
profile = "dev"
debug = true
skip_pull_image = false
use_container = true

[default.deploy.parameters]
stack_name = "hogehoge-dev"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-dev"
s3_prefix = "hogehoge"
region = "ap-northeast-1"
profile = "dev"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Env=\"dev\""

[stg.build.parameters]
profile = "stg"
debug = true
skip_pull_image = false
use_container = true

[stg.deploy.parameters]
stack_name = "hogehoge-stg"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-stg"
s3_prefix = "hogehoge-stg"
region = "ap-northeast-1"
profile = "stg"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Env=\"stg\""

[prd.build.parameters]
profile = "prd"
debug = false
skip_pull_image = false
use_container = true

[prd.deploy.parameters]
stack_name = "hogehoge-prd"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-prd"
s3_prefix = "hogehoge-prd"
region = "ap-northeast-1"
profile = "prd"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Env=\"prd\""

一番最後の[prd.deploy.parameters]を例にして説明すると、この部分は[環境.コマンド.aws-sam-cliに渡すパラメータ]となります。このセクションで各パラメータを設定しておくことで、コマンド実行が楽になります(後述)。また、profileAWS CLI の config と同じものを記載してください。parameter_overridesで環境名でEnvパラメータを上書き指定しています。

それでは API Gateway と Lambda Authorizer と DynamoDB を用いた雰囲気tempalte.yamlをご覧ください。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  hoge serverless service

Globals:
  Function:
    Timeout: 3
    Runtime: python3.8
    Environment:
      Variables:
        ENV: !Ref Env
        HOGE_API_HOST: !Ref HogeApiHost
    Layers:
      - !Ref MyLayer
    VpcConfig:
      SecurityGroupIds: !FindInMap [ SecurityGroup, !Ref Env, SecurityGroupIds ]
      SubnetIds: !FindInMap [ SubnetId, !Ref Env, SubnetIds ]        

Parameters:
  Env:
    Type: String
    AllowedValues:
      - prd
      - stg
      - dev
    Default: dev
  HogeApiHost:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /hoge/api/host

Conditions:
  IsDev: !Equals [ !Ref Env, dev ]

Mappings:
  SecurityGroup:
    prd:
      SecurityGroupIds:
        - sg-hoge-prd
    stg:
      SecurityGroupIds: 
        - sg-hoge-stg
    dev:
      SecurityGroupIds:
        - sg-hoge-dev
  SubnetId:
    prd:
      SubnetIds:
        - hoge-prd1
        - hoge-prd2
    stg:
      SubnetIds:
        - hoge-stg1
        - hoge-stg2
    dev:
      SubnetIds:
        - hoge-dev1
        - hoge-dev2

Resources:
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Env
      Auth:
        DefaultAuthorizer: TokenAuth
        AddDefaultAuthorizerToCorsPreflight: False
        Authorizers:
          TokenAuth:
            FunctionPayloadType: TOKEN
            FunctionArn: !GetAtt authorizerFunction.Arn
            Identity:
              Header: Authorization
              ReauthorizeEvery: 0

  authorizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: functions/authorizer/
      Handler: app.lambda_handler
      Description: API Gateway Lambda Authorizer
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref Hogetable
        - AmazonSSMReadOnlyAccess

  # Lambda Layer
  MyLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: hogehoge-service
      Description: ""
      ContentUri: service/
      CompatibleRuntimes:
        - python3.8
    Metadata:
      BuildMethod: python3.8

  # DynamoDB
  HogeTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: Hogetable
      AttributeDefinitions: 
        - AttributeName: id
          AttributeType: N
      KeySchema: 
        - AttributeName: id
          KeyType: HASH
      ProvisionedThroughput: !If [IsDev, { "ReadCapacityUnits": 5, "WriteCapacityUnits": 5 }, !Ref AWS::NoValue]
      BillingMode: !If [IsDev, !Ref AWS::NoValue, PAY_PER_REQUEST]

Outputs:
  WebEndpoint:
    Description: "API Gateway endpoint URL"
    Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/${Env}/"

ほとんど CloudFormation のテンプレートですね。Globals:とか sam 特有に見えますが CloudFormation でも使えるみたいです(未確認)。
雰囲気だけではなんなので、テクニック的なことも書きますと

  • Parameters:のところでType: AWS::SSM::Parameter::Value<String>を使用し Parameter Store の値を取得
  • Conditions:Envdevでないときに DynamoDB のテーブルを Provisioned ではなく OnDemand で作成

といったことをしています。

実際にステージング環境にデプロイするにはまず

$ sam build --config-env stg

とビルドします。samconfig.toml に色々と設定しているのでコマンド自体はシンプルですね。最終的に下記のような出力を確認できます。

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided

Artifacts が出力されているディレクトリの中には CloudFormation のテンプレートも出力されます。
それではデプロイしてみましょう。出力内容は雰囲気です。toml ファイルでconfirm_changeset = trueとしていますので、ChangeSet の確認が途中で入ります。

$ sam deploy --config-env stg
Uploading to hogehoge-stg/xxxxxxxxxxxxxxxxxxxxxxx  5000 / 5000.0  (100.00%)

    Deploying with following values
    ===============================
    Stack name                 : hogehoge-stg
    Region                     : ap-northeast-1
    Confirm changeset          : True
    Deployment s3 bucket       : aws-sam-cli-managed-default-samclisourcebucket-stg
    Capabilities               : ["CAPABILITY_IAM"]
    Parameter overrides        : {'Env': 'stg'}

CloudFormation stack changeset
---------------------------------------------------------------------------------------------------------------------------------------------------------
Operation                              LogicalResourceId                      ResourceType                           Replacement                          
---------------------------------------------------------------------------------------------------------------------------------------------------------
+ Add                                  MyApiDeploymentxxxxxxxxxx              AWS::ApiGateway::Deployment            N/A                                  
+ Add                                  MyApiStage                             AWS::ApiGateway::Stage                 N/A    
---------------------------------------------------------------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:hoge

Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y    <--------------- y を入力すると反映されます

CloudFormation events from changeset
---------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                         ResourceType                           LogicalResourceId                      ResourceStatusReason                 
---------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS                     AWS::ApiGateway::RestApi               MyApi                                  -                                    
CREATE_IN_PROGRESS                     AWS::ApiGateway::RestApi               MyApi                                  Resource creation Initiated          
CREATE_COMPLETE                        AWS::ApiGateway::RestApi               MyApi                                  -                                    
〜〜 省略 〜〜
---------------------------------------------------------------------------------------------------------------------------------------------------------

CloudFormation outputs from deployed stack
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                   
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 WebEndpoint                                                                                                                           
Description         API Gateway endpoint URL                                                                                                              
Value               https://hogehogehoge.execute-api.ap-northeast-1.amazonaws.com/stg/                                                                      
-----------------------------------------------------------------------------------------------------------------------------------------------------------

と分かりにくいですがResourceStatusの値が変化しながら処理が進み、最後に template.yamlOutputsで指定した値が出力されます。
一番右のReplacementの値に注意しながら運用していこうと思います。