CDKコントリビュートことはじめ

はじめに

年末に息子が産まれ、4ヶ月の育休に突入しました。

運よく寝ている間のフリータイムがそこそこ生まれたので、大変お世話になっているOSSAWS CDKにコントリビュートしてみようと思い立ちました。

結果として大いにハマり、現在Star Contributorまでなれています。

ただし、最初の1PRのハードルが高かったので、同様の境遇の方に向けて具体的な手順などお伝えできればと思い、記事にしてみました。

こちらの記事(大変お世話になりました)などのN番煎じ感が否めませんが、 統合テスト周りの情報があまりなかったので、そのあたりを重点的に補完できればと思っています。

speakerdeck.com

基本的にはcontributiong guideを参考にしてください。 (といいつつ、これが分かりづらく辛かったのが記事作成のモチベーションです) github.com

前提知識

L1はCloudformationから自動生成されるため、我々の方でいじることはありません。 CDKでの実装内容としてはL2上でのL1操作が基本になります。

L2の新規作成も出来ますが、RFCに則る必要があるなどハードルが高いので割愛します。 (私もやったことはありません)

github.com

手順

環境構築

開発環境を構築します。私はlocalで実施しますが、gitpodでやれたりもします。お好みで。

まずはclone & install

git clone https://github.com/{your-account}/aws-cdk.git
cd aws-cdk
yarn install

その後、aws-cdk-libをビルドします。結構時間かかるので気長に待ちましょう。

npx lerna run build --scope=aws-cdk-lib

issue探索

まずは取り組む課題の明確化です。どうにか自分で対応出来そうなissueを見つけてみます。

effort/small or effort/medium ラベルでフィルタしつつ、自分の詳しいサービスに関するものを眺めて見るのが良いと思います。 ラベリングは割と適当なので、smallでも難しかったりmediumでも簡単だったりします。

取り組みやすいパターン

  1. エンジンやインスタンスのバージョン追加

enumに新バージョンを追加するだけです。一番カンタン。後述する統合テストも多分不要です。

github.com

  1. Cloudformation(L1)で渡せるPropertyをL2の引数に追加

L1はCloudformationから自動生成されるため全てのpropsに対応していますが、L2は都度都度手動で追加していく必要があるため、結構多くのpropsが未対応だったりします。特に、新規でL1に追加されたものが狙い目です。

github.com

  1. deploy時にエラーとなるパラメータをsynth時にチェック

Cloudformationのdocumentに許容されるpropsの条件が明記されています。これを満たしていないとdeploy時にエラーとなりますが、synthの段階でthrow ErrorしてあげるとUXが良くなります。 こちらも統合テストを書かなくてもOKなので、取り組みやすさ◎です。

github.com

もちろん、自分で新規issueを発行してもOKです。既存issueに取り組むと「あれっ?ここの実装、修正したほうが良さそう..??」と気づくことが多々あります。そんなときはissue発行して取り組んでみましょう

特にissueへのコメントは不要です。ガンガン進めちゃいましょう。

今回は例として以下のissueを題材に進めてみたいと思います。

github.com

設計

整理

まずはissue内容を整理します。 L1コンストラクトであるCfnTopicにはArchivePolicyという引数がありますが、これはL2コンストラクトであるTopicからは設定できないようです。そこで、新しくこれを設定できるような引数を追加すれば良さそうです。

これはパターン2の「Cloudformation(L1)で渡せるPropertyをL2の引数に追加」ですね。

仕様調査

Cloudformationのドキュメントには、ArchivePolicyについて以下のように記述されていました。

パラメータはJsonを受け取れるとしか書いてありません。。。不親切ですね。(あるある)

Json形式の情報が見当たらないので、マネコンでSNSトピックを作りArchivePolicyを設定してみます。すると、以下のように丁寧にJSON形式まで表示されていました。

これに則り、以下のようにパラメータを渡してあげれば良さそうです。

// L1コンストラクタでの設定例
const topic = new CfnTopic(this, 'Topic', {
      archivePolicy: {
        MessageRetentionPeriod: 10,
      },
}

また、archivePolicyFIFOトピックのみで有効なようです。 これは後ほどバリデーションします。

L2仕様検討

Cfnに沿って愚直に作ると、以下のようなInterfaceを定義しTopicPropsに追加してあげれば良さそうです。

interface ArchivePolicy {
  MessageRetentionPeriod: number
}

export interface TopicProps {
  ... // 既存の引数群
   archivePolicy?: ArchivePolicy // 新しいL2の引数として追加
}

const resource = new CfnTopic(this, 'Resource', {
    ... // 既存のパラメータ
    archivePolicy: props.archivePolicy, // CfnTopicへ渡すパラメータを追加
    });

しかしこの場合、ユーザは以下のようなオブジェクトを自作してL2を呼び出す必要があり、あまり直感的とは言えません。

// これを自作する
const archivePolicy = {
    messageRetentionPeriod: 10 // day単位
}

const myTopic = sns.Topic(this, 'MyTopic', {
  archivePolicy,
})

そこで、よりユーザフレンドリーとなるように、messageRetentionPeriodInDaysという引数を受け取るようにしてみましょう。

export interface TopicProps {
  ... // 既存の引数群
   messageRetentionPeriodInDays?: 10 // 新しいL2の引数として追加
}

const period = props.messageRetentionPeriodInDays

const resource = new CfnTopic(this, 'Resource', {
    ... // 既存のパラメータ
    // CfnTopicへ渡すパラメータをL2内で作成
    archivePolicy: period !== undefined ? {
        messageRetentionPeriod: period,
    } : undefined 
    });

これにより、ユーザは日数をL2へ引数として渡すだけで設定が完了します。さっきより使い勝手が良さそうですね。

実装

コードを実装していきます。上記に沿って、aws-cdk/packages/aws-cdk-lib/aws-sns/lib/topic.tsをどんどん修正していきます。

バリデーション

ついでに、パラメータのバリデーションも追加します。 今回はmessageRetentionPeriodInDaysが以下の条件を満たすかチェックします。 - FIFOトピックに適用されているか - 1~365の整数

    // FIFOトピックに適用されているか
    if (props.messageRetentionPeriodInDays && !props.fifo) {
      throw new Error('`messageRetentionPeriodInDays` is only valid for FIFO SNS topics.');
    }
    // 1-365の整数か?
    if (
      props.messageRetentionPeriodInDays !== undefined
      && !Token.isUnresolved(props.messageRetentionPeriodInDays)
      && (!Number.isInteger(props.messageRetentionPeriodInDays) || props.messageRetentionPeriodInDays > 365 || props.messageRetentionPeriodInDays < 1)
    ) {
      throw new Error('`messageRetentionPeriodInDays` must be an integer between 1 and 365');
    }

Token

CDKではデプロイ後まで確定しない値(ex. ARN)をTokenという形で扱います。

docs.aws.amazon.com

引数としてstring or numberを受け取るとき、念のためTokenの可能性も考慮します。 といいつつ、Tokenに対して値のチェックは実行できないため、あくまでTokenでないことを確認すればOKです

// before
if (props.someValue > 10) { ... }

// after
import { Token } from '../../core';
if (!Token.isUnresolved(props.someValue) && props.someValue) { ... }

テスト

テストには単体テスト結合テストの2種類が存在します。基本的にはどちらも作成してからPRを提出します。

単体テスト

cdk synthにより生成されるCloudformationテンプレートが望み通りのものとなっているかをチェックします。 aws-cdk/packages/aws-sns/test/topic.test.tsに以下のようなテストを追記していきます。

    test('specify message retention period in days', () => {
      // GIVEN
      const app = new cdk.App();
      const stack = new cdk.Stack(app);

      // WHEN
      new sns.Topic(stack, 'MyTopic', {
        fifo: true,
        messageRetentionPeriodInDays: 10,
      });

      // THEN
      Template.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', {
        ArchivePolicy: {
          MessageRetentionPeriod: 10,
        },
        FifoTopic: true,
      });
    });

直感的でわかりやすいですね!

同様に、バリデーションについてもテストを追加していきます。 少しバリエーションを加えて、test.eachを活用してみます。これは、配列の要素を順番にtestしていくものです。 全ての要素でちゃんとErrorがthrowされればOKです。

    test.each([0, 366, 12.3, NaN])('throw error if message retention period is invalid value "%s"', (days) => {
      // GIVEN
      const app = new cdk.App();
      const stack = new cdk.Stack(app);

      // THEN
      expect(() => new sns.Topic(stack, 'MyTopic', {
        fifo: true,
        messageRetentionPeriodInDays: days,
      })).toThrow(/`messageRetentionPeriodInDays` must be an integer between 1 and 365/);
    });

test実行は以下のコマンドで行えます。

yarn test topic.test.ts

しっかりテスト通過すればOKです!

結合テスト

実際にCDKテンプレートを自アカウントにデプロイできるかをチェックします。 公式の手順はこちら。

github.com

  1. CDKの環境設定 自分のアカウントへCDK deployできるようにします。AWS CLIの設定及びCDK bootstrapを行えばOKです。

  2. 結合テストファイルの作成

packages/@aws-cdk-testing/framework-integ/test/aws-sns/testに新規の結合テストファイルを作成します。 今回はinteg.sns-fifo-archive-policy.tsを作成します。

import { App, Stack, StackProps } from 'aws-cdk-lib';
import { Topic } from 'aws-cdk-lib/aws-sns';
import * as integ from '@aws-cdk/integ-tests-alpha';

// デプロイするスタックの定義
class SNSFifoArchivePolicyStack extends Stack {
  constructor(scope: App, id: string, props?: StackProps) {
    super(scope, id, props);

    // 早速、作成した引数を使ってみます
    new Topic(this, 'MyTopic', {
      fifo: true,
      messageRetentionPeriodInDays: 12,
    });
  }
}

const app = new App();
const stack = new SNSFifoArchivePolicyStack(app, 'SNSFifoArchivePolicyStack');

// おまじない
new integ.IntegTest(app, 'SqsTest', {
  testCases: [stack],
});
  1. @aws-cdk-testing/framework-integ をビルド
npx lerna run build --scope=@aws-cdk-testing/framework-integ
  1. 生成されたJSファイルを使ってyarn integを実行 TSファイルではないので注意してください。 これにより、cdk deploy → スタックデプロイ完了 → snapshotファイル生成 → スタック削除 → 完了 を自動で実行してくれます。
$ yarn integ ./packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-fifo-archive-policy.js --update-on-failed
yarn run v1.22.21
integ-runner --language javascript test/aws-sns/test/integ.sns-fifo-archive-policy.js --update-on-failed

Verifying integration test snapshots...

  NEW        aws-sns/test/integ.sns-fifo-archive-policy 1.95s

Snapshot Results: 

Tests:    1 failed, 1 total
Failed: /Users/kazuhoshinozuka/git/aws-cdk/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-fifo-archive-policy.js

Running integration tests for failed tests...

Running in parallel across regions: us-east-1, us-east-2, us-west-2
Running test /Users/kazuhoshinozuka/git/aws-cdk/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-fifo-archive-policy.js in us-east-1
  SUCCESS    aws-sns/test/integ.sns-fifo-archive-policy.js/DefaultTest 165.908s
       NO ASSERTIONS

Test Results: 

Tests:    1 passed, 1 total
✨  Done in 168.56s.

これで生成されたpackages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-fifo-archive-policy.js.snapshotフォルダをまるっとgit add し、ブランチに追加します。

README作成

feature実装の際はREADMEの作成が必須です。 既存のREADMEに追記し、ブランチにaddします

PR作成

一般的な流れでPRを作成すればOKです。テンプレが設定されているので、いい感じに埋めればOKです。

レビュー

以上を満たしてPRを作成すると、自動的にpr/needs-community-reviewラベルが付与されます。いつかcommunity reviewerがレビューしてくれますので、レビュー内容に適宜対応していきましょう。 community reviewerがapproveすると、今度は自動的にpr/needs-maintainer-reviewラベルが付与されるので、maintainerのレビューを待ちます。 maintainerにもapproveされれば自動的にマージされます!お疲れ様でした。

概ね1週間くらいはレビュー待ちの期間があります。 簡易な修正なら即日マージされますし、大きなものだと数ヶ月単位でかかると思います。 気長に待ちましょう。

最後に

困ったときはdistinguish contributorの@go-to-kさんがとても優しく質問に対応していただけると思います。(勝手に宣伝) ハードルが高ければ私でも大丈夫です (@nixieminton)

CDKのissueは2000個ほど積まれていますし、issue化されていない問題は星の数ほどあると思います! ぜひみんなで良いものにしていきましょう!

Apple Silicon MacbookでSteam版パルワールドを遊ぶ(crossover無し)

要約

Apple純正のgame porting toolkitを用いて、パルワールド(Steam)をM1 mac上で遊ぶ方法です。crossoverは使わないので、ずーっと無料です!

背景

こちらのブログを見て感動してしまい、全然やったことのないPCゲームに手を出してみようと思いました。

note.com

そうです、噂のパルワールドです。

ただし、あまりに愚かな僕を待ち受けていたのは非情な現実でした。

Windows限定なんかーい...

 

〜〜〜2週間後〜〜〜

 

どうやらApple Silicon macでもプレイできるという情報を入手しました。

なかなか大変だったので、同様な状況の方のために手順メモを残しておきます。

 

手順

基本的にはこちらのgithub リポジトリに則ります。

github.com

 

環境

MacOS Sonoma 14.3

M1 16GB memory

 

x86版 Homebrewセットアップ

Rossetaのインストール & x86でのsh起動

$ softwareupdate --install-rosetta
$ arch -x86_64 zsh

x86版homebrewのinstall

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

architectureに応じたhomebrewのPATH設定 (~/.zshrcに追記)

if [ "$(arch)" = "arm64" ]; then
    eval "$(/opt/homebrew/bin/brew shellenv)"
else
    eval "$(/usr/local/bin/brew shellenv)"
fi

再びx86 shを起動 (3のif文でいい感じにPATH設定が行われる)

$ arch -x86_64 zsh

homebrewのPATH設定を確認

$ which brew
/usr/local/bin/brew

 

Build

Apple tap のダウンロード

$ brew tap apple/apple http://github.com/apple/homebrew-apple

game-porting-toolkit(GPTK)のインストール (死ぬほど時間かかる。M1で約75分, M2 maxで36分。下記のprebuiild版もあるのでそっちが良いかも。)

$ brew -v install apple/apple/game-porting-toolkit

x-code CLIがv15.1でない場合、エラーで落ちる。そのときは以下コマンドを実行してから再度GPTKのインストールを実行する。

$ sudo rm -rf /Library/Developer/CommandLineTools
$ sudo xcode-select --install

Prebuild版のインストール

あとから気づいたが、prebuildされたGPTKもインストール可能らしい。先に書いてくれ。。

参考までに手順を載せますが、buildを実行した方はスキップでOK。

$ brew tap gcenx/homebrew-apple
$ brew install gcenx/wine/game-porting-toolkit

GPTKのインストール

2024/2/3現在、v1.1がインストールされる。

$ arch -x86_64 zsh
$ brew update && brew upgrade apple/apple/game-porting-toolkit

$ ditto /Volumes/Game\ Porting\ Toolkit-1.1/redist/lib/ `brew --prefix game-porting-toolkit`/lib/


Wineのsetup

$ WINEPREFIX=~/Win10 `brew --prefix game-porting-toolkit`/bin/wine64 winecfg

wineのconfigが表示されるので、Windows10 > apply > OK

(applyを押し忘れない!!!これで1h溶かしました...)

 

SteamのWindowsインストーラをダウンロード

store.steampowered.com

 Windows版をダウンロードします。Not mac

 

インストーラのコピー

$ cp ~/Downloads/SteamSetup.exe ~/Win10/drive_c

 

SteamのSetup

$ cd ~/Win10/drive_c
$ MTL_HUD_ENABLED=0 WINEESYNC=1 WINEPREFIX=~/Win10 /usr/local/opt/game-porting-toolkit/bin/wine64 SteamSetup.exe

もろもろ完了し、FinishでSteamが自動起動する。

ただし、おそらく一度失敗するので、「Restart Steam?」 を選択

そして、おそらく再度失敗するので、一度PCを再起動する。

再起動後、直接Steamを起動する。先程とは実行するexeが異なっているので注意。

$ cd ~/Win10/drive_c
$ MTL_HUD_ENABLED=0 WINEESYNC=1 WINEPREFIX=~/Win10 /usr/local/opt/game-porting-toolkit/bin/wine64 Program\ Files\ \(x86\)/Steam/Steam.exe

 

これでログイン画面が出ればOK!!!!!

お疲れ様でした。