AWS CDK で API Gateway を作った後、テストコードに URL を伝えるのは Parameter Store が便利だよという話

AWS CDK では TypeScript を始めとした様々な言語でインフラコードを書くことができます。つい先日、Java と .NET にも対応しましたね。

私は、サーバーレスのアプリケーションを実装する際、構築した API に対しても実際にリクエストを送り、レスポンスを確認するようなテストも書きます。結合テスト、E2Eテストなどと呼ばれているテストですね。そのとき、構築したAPIのURLがほしい という状況がよくあります。AWS SAM でサーバーレスアプリケーションを構築していたときは CloudFormation の Output からURLを取得したりもしました。

AWS CDK で構築した場合、aws-ssmが便利です。キーだけ決めておけば、人の手を極力少なくテストコードでURLを呼び出すことができます。早速試しましょう。

環境

パッケージ名 バージョン
aws-cdk 1.19.0
jest 24.9.0
ts-jest 24.2.0
ts-node 8.5.4
typescript 3.7.3

やること

  1. AWS CDK で API Gateway (Mock API)をつくる
  2. URLを Parameter Store に登録する
  3. テストコードからURLを取得し、リクエストする

AWS CDK で API Gateway(ダミー)をつくる

サンプルとなる API Gateway を構築します。必要な AWS CDK モジュールをインストールしましょう。

1
> npm install --save-dev @aws-cdk/aws-apigateway @aws-cdk/aws-ssm

次に、CDK のコードを書きます。

lib/eval-cdk19-stack.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import * as cdk from '@aws-cdk/core';
import * as apig from '@aws-cdk/aws-apigateway';
import * as ssm from '@aws-cdk/aws-ssm';
 
// ① function での定義も可能
export function cdkStackEvalCdk(
  scope: cdk.Construct,
  id: string,
): void {
 
  const stack = new cdk.Stack(scope, id);
 
  // ダミーAPI定義
  const api = new apig.RestApi(
    stack,
    'DummyRestApi',
    {
      restApiName: `dummy`,
      endpointTypes: [apig.EndpointType.REGIONAL],
      deployOptions: {
        stageName: 'v1'
      },
    },
  );
 
  // GET /user
  const userResource = api.root.addResource('user');
  resourceIntegrationGetUser(userResource, 'GET');
 
  // ② URLを  Parameter Store に登録
  new ssm.StringParameter(stack, 'UserApiUrl', {
    parameterName: 'UserApiUrl',
    stringValue: api.url,
  });
}
 
 
function resourceIntegrationGetUser(
  self: apig.Resource,
  method: string,
): void {
  const methodOptions: apig.MethodOptions = {
    methodResponses: [
      {
        statusCode: '200',
      },
    ],
  };
 
  const dummyUser = {
    name: 'wada',
    description: 'sorry for late'
  };
 
  const integration = new apig.MockIntegration({
    requestTemplates: {
      'application/json': `{
                "statusCode": 200
            }`,
    },
    integrationResponses: [
      {
        statusCode: '200',
        responseTemplates: {
          'application/json': JSON.stringify(dummyUser),
        },
      },
    ],
  });
 
  self.addMethod(method, integration, methodOptions);
}

ポイントを見ていきます。

1.AWS CDK の Construct 定義は function でもできる

cdk init で出来上がる最初のコードが cdk.Construct をオーバーライドする形になっているので勘違いしがちですが、function でも書けます。そのときは、function内で const stack = new cdk.Stack(scope, id); のようにして Stacks を、 const stackConstruct = new cdk.Construct(scope, id); のようにしてConstructを定義できます。

2. aws-ssm モジュールを使って Parameter Store に登録する

Parameter Store に登録しようとなったとき、私が最初に思いついたのは AWS SDK(CDKではなく)を使って API Gateway のURLを登録することでした。次のようなイメージです:

1
2
3
4
5
6
const params = {
  Name: 'UserApiUrl',
  Type: 'String',
  Value: apig.url
};
ssm.putParameter(params).promsise();

しかしこれではうまくいきません。この処理が呼ばれる(= Parameter Store に登録される)時点では、API Gateway はデプロイが完了しておらず、URLが確定していないためです。それじゃあ一体どんな値が入っているのかというと、仮の値が入っています。 console.log してみると https://${Token[TOKEN.12]}.execute-api.${Token[AWS::Region.4]}.${Token[AWS::URLSuffix.1]}/${Token[TOKEN.18]}/ という値が出力されました。落ちつて考えれば当たり前で、依存関係を解決してデプロイが完了するまでは、API Gateway のURLも確定せず、 Parameter Store に登録することもできませんね。

ではどうするかというと、API Gateway の URLも Parameter Store の "AWSリソース" として構築してしまいます。それをやってくれるのが aws-ssm です。

1
2
3
4
new ssm.StringParameter(stack, 'UserApiUrlParameter', {
    parameterName: 'UserApiUrl',
    stringValue: api.url,
  });

このように StringParameter を登録することで確定した URL をParameter Storeに追加できます。デプロイしましょう。

1
> cdk deploy

Parameter Store に登録されていることを確認してください。

1
2
3
4
5
6
7
8
9
10
11
aws ssm get-parameter --name UserApiUrl
{
    "Parameter": {
        "Name": "UserApiUrl",
        "Type": "String",
        "Version": 1,
        "LastModifiedDate": 1576684620.448,
        "ARN": "arn:aws:ssm:ap-northeast-1:xxxxxxxxxxx:parameter/UserApiUrl"
    }
}

これで準備完了です。

テストコードからURLを取得し、リクエストする

テストコードのほうは AWS SDK を使います。インストールしておきましょう。

1
> npm install --save-dev aws-sdk axios

ダミーAPIに対してリクエストし、レスポンスを確認するようなテストを書きます。

test/eval-cdk19-e2e.test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import * as aws from 'aws-sdk';
import axios from 'axios';
 
const ssm = new aws.SSM({
  region: 'ap-northeast-1'
});
 
test('Dummy API', async () => {
  // ① Parameter Store からURLを取得
  const param = await ssm.getParameter({
    Name: 'UserApiUrl'
  }).promise();
  const url = param.Parameter?.Value!;
  const userEndpoint = `${url}/user`;
 
  // ② URLに対してリクエスト、レスポンスボディを確認する
  const response = await axios.get(userEndpoint);
  expect(response.data).toEqual({
    name: 'wada',
    description: 'sorry for late'
  });
});

デプロイされたダミーAPIのURLを Parameter Store から取得し、リクエストを送ることができました。ここまでURLのコピペは行っていませんし、特別なスクリプトも書いていません。いい感じですね。

まとめ

aws-ssm を使うことで、 AWS CDK デプロイ時に API Gateway の URL も登録できました。その後、テストコードで実際に Parameter Store から取得してテストを行いました。特にサーバーレスアプリケーションなど、多くのサービスが連携するタイプのシステムでは、実際にデプロイしての動作確認が有効です。そのときに、APIを始めとしたアクセス可能なエンドポイントを Parameter Store に登録しておけば捗ると思います。参考になれば幸いです。

PRAWSの技術支援ならクラスメソッド