Uniforceのニシです。

この記事を書いている頃は、ヤマザキパンのベイクワンシリーズのたまごぱんにハマっています(甘い飲み物、食べ物、好き🤤)。

少量で腹持ちがいい感じです。

さて、今回はローカルでのテストは成功するのにタイムゾーンの違いでGitHub Actionsではテストが失敗したお話です。

きっかけ

AWS CDKでAPI Gateway+Lambdaの構成を実装していました。ディレクトリ構成のイメージとファイルの説明は次の通りです。

lib
├── api-deployment-stack.ts
├── func-stack.ts
└── api-stack.ts

ファイル

メモ

api-stack.ts

RestAPIの実装

func-stack.ts

Lambdaプロキシ統合の実装

api-deployment-stack.ts

DeploymentとStageの実装

RestAPI、Lambdaプロキシ統合、APIのDeployment周りの実装をそれぞれ別のStackとして実装しています。AWS CDKのドキュメントにも記載がありますが、1つのCloudFormatinに定義できるリソースには制限がありますので、それを見越して予めStackを分けておくことは良くあることだと思っています。

aws-cdk-lib.aws_apigateway module · AWS CDK

Breaking up Methods and Resources across Stacks It is fairly common for REST APIs with a large number of Resources and Methods to hit the CloudFormation limit of 500 resources per stack.

To help with this, Resources and Methods for the same REST API can be re-organized across multiple stacks. A common way to do this is to have a stack per Resource or groups of Resources, but this is not the only possible way. The following example uses sets up two Resources '/pets' and '/books' in separate stacks using nested stacks:

また、上記のようにStackを分けて実装した場合、初回はAPI Gatewayのデプロイが成功しますが、2回目以降のデプロイではリースを変更したとしても変更されたことをキャッチできず、変更内容が反映されません。ドキュメントにも警告メッセージとして記載されています。

Warning: In the code above, an API Gateway deployment is created during the initial CDK deployment. However, if there are changes to the resources in subsequent CDK deployments, a new API Gateway deployment is not automatically created. As a result, the latest state of the resources is not reflected. To ensure the latest state of the resources is reflected, a manual deployment of the API Gateway is required after the CDK deployment. See Controlled triggering of deployments for more info.

2回目以降のデプロイで変更内容を反映するためにドキュメントではコンストラクタIDを適宜変更する方法が紹介されています。

Controlled triggering of deployments By default, the RestApi construct deploys changes immediately. If you want to control when deployments happen, set { deploy: false } and create a Deployment construct yourself. Add a revision counter to the construct ID, and update it in your source code whenever you want to trigger a new deployment:

ドキュメントで紹介されているようにコンストラクタIDを変更する方法でも良いのですが、書き換えるのがメンドウに感じたためaws-cdkのissueに記載されていたようにaddToLogicalIdに現在時刻を利用する方法を使用しました。

https://github.com/aws/aws-cdk/issues/13526#issuecomment-796414134

const apiDeployment = new cdkApiGateway.Deployment(this, 'api-deployment', { api: <importedApi>, description: 'maybe put datetime or artifact version here', retainDeployments: true, }); apiDeployment.addToLogicalId( new Date().toISOString() ); //need to change deployment hash to force new deployment

addToLogicalIdに現在時刻を利用する方法で2回目以降のデプロイでも問題なく変更内容が反映されていることを確認できましたので、スナップショットテストを実装しました。addToLogicalIdで現在時刻を利用していますので、スナップショットテストでは次のように時刻を固定するための処理を入れました。

jest.useFakeTimers().setSystemTime(new Date(2021, 0, 1, 1, 1, 1));

ローカルでスナップショットテストを複数回実行してみてPASSすることを確認しました。

PASS test/api.test.ts (5.009 s)
api test
dev
apiStack
✓ Snapshot test (622 ms)
apiFuncStack
✓ Snapshot test (3 ms)
apiDeploymentStack
✓ Snapshot test (5 ms)

次にGitHub Actionsのワークフローとしてスナップショットテストを実行してみたところ、FAILとなってしまいました …ナンデェェ。( ;∀;)・。ェ

FAIL test/api.test.ts (7.009 s)
api test
dev
apiStack
✓ Snapshot test (802 ms)
apiFuncStack
✓ Snapshot test (4 ms)
apiDeploymentStack
✕ Snapshot test (7 ms)

● api test › dev › apiDeploymentStack › Snapshot test

expect(received).toMatchSnapshot()

Snapshot name: `api test dev apiDeploymentStack Snapshot test 1`

- Snapshot - 3
+ Received + 3

@@ -5,11 +5,11 @@
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
"Type": "AWS::SSM::Parameter::Value<String>",
},
},
"Resources": {
- "ApiDeploymentAA93AD2Dc0ba51c8d0b765ab2f1572c511ea860a": {
+ "ApiDeploymentAA93AD2De43b6cf92ea5ec5e9288e68193d51828": {
"DeletionPolicy": "Retain",
"Properties": {
"RestApiId": {
"Fn::ImportValue": "ApiDevStack:ExportsOutputRefApirestApiEE5B197C3A4A91C1",
},
@@ -58,11 +58,11 @@
},
"Type": "AWS::ApiGateway::ApiKey",
},
"ApiDeploymentStageCA7354C5": {
"DependsOn": [
- "ApiDeploymentAA93AD2Dc0ba51c8d0b765ab2f1572c511ea860a",
+ "ApiDeploymentAA93AD2De43b6cf92ea5ec5e9288e68193d51828",
],
"Metadata": {
"cdk_nag": {
"rules_to_suppress": [
{
@@ -86,11 +86,11 @@
"Fn::ImportValue": "amazon-apigateway-api-logs-an1-dev-arn",
},
"Format": "{"requestId":"$context.requestId","sourceIp":"$context.identity.sourceIp","caller":"$context.identity.caller","userAgent":"$context.identity.userAgent","user":"$context.identity.user","requestTime":"$context.requestTime","requestTimeEpoch":"$context.requestTimeEpoch","httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath","path":"$context.path","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength","accounId":"$context.identity.accountId","apiId":"$context.apiId","stage":"$context.stage","awsEndpointRequestId":"$context.awsEndpointRequestId","domainName":"$context.domainName","domainPrefix":"$context.domainPrefix","extendedRequestId":"$context.extendedRequestId","webaclArn":"$context.webaclArn","wafResponseCode":"$context.wafResponseCode","wafStatus":"$context.wafStatus"}",
},
"DeploymentId": {
- "Ref": "ApiDeploymentAA93AD2Dc0ba51c8d0b765ab2f1572c511ea860a",
+ "Ref": "ApiDeploymentAA93AD2De43b6cf92ea5ec5e9288e68193d51828",
},
"RestApiId": {
"Fn::ImportValue": "ApiDevStack:ExportsOutputRefApirestApiEE5B197C3A4A91C1",
},
"StageName": "api",

209 | describe('apiDeploymentStack', () => {
210 | test('Snapshot test', () => {
> 211 | expect(Template.fromStack(apiDeploymentDevStack)).toMatchSnapshot();
| ^
212 | });
213 |
214 | describe('cdk-nag AwsSolutions Pack', () => {

at Object.<anonymous> (test/api.test.ts:211:67)

原因はタイムゾーンの違いだった

何が原因でGitHub Actionsでのスナップショットテストが失敗していたかというと、タイムゾーンでした。スナップショットテスト失敗時の出力結果をしっかり見ていればあまり時間はかからなかったと思うのですが、そのときは何とかテストをPASSしたい気持ちで試行錯誤して2時間ぐらい経っていました…

さて、スナップショットテストの出力結果で注目すべきポイントはこちらです。

- "ApiDeploymentAA93AD2Dc0ba51c8d0b765ab2f1572c511ea860a"

+ "ApiDeploymentAA93AD2De43b6cf92ea5ec5e9288e68193d51828"

スナップショットテストでは前回との差分があるのかをチェックするのですが、DeploymentのIDに差分が発生していることが指摘されています。DeploymentのIDはaddToLogicalIdで現在時刻を使用するように設定しているものの、スナップショットテストにおいてはDateをmock化することで時刻が固定されるようにしていました。

実際、ローカルでスナップショットテストを複数回実行してもテストは問題なくPASSしていたため、Dateのmock化による時刻の固定はなされているはずです。ではなぜ差分が発生しているのか考えたときに、ローカルとGitHub Actionsのランナーでタイムゾーンが異なるからではないかと思いました。

そこでスナップショットテストのときにタイムゾーンの違いが出ないようにするためにJestコマンドの実行時にタイムゾーンを設定するようにしました。具体的には次のとおりです。

TZ=Asia/Tokyo jest

再度GitHub Actionsでスナップショットテストを実行してみると、、、今度はPASSしました!

PASS test/api.test.ts (6.011 s)
api test
dev
apiStack
✓ Snapshot test (533 ms)
apiFuncStack
✓ Snapshot test (3 ms)
apiDeploymentStack
✓ Snapshot test (7 ms)

念のため何度かリランしてみましたが、複数回実行しても結果は変わりませんでしたので、やはりタイムゾーンが原因でした。

まとめ

今回はローカルでのテストは成功するのにタイムゾーンの違いでGitHub Actionsではテストが失敗したときのお話を記事にしてみました。

調べてみるとGitHub Actions ワークフローのenvでタイムゾーンを設定することもできるようですが、今回はJestコマンド実行時のみタイムゾーンを設定することにより、ローカルとGitHub Actionsのスナップショットテストにおいて違いがでないようにしました。

本日の記事はここまでとなります。

最後まで読んでいただきありがとうございました🙌