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

2019.12.19

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 モジュールをインストールしましょう。

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

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

lib/eval-cdk19-stack.ts

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を登録することでした。次のようなイメージです:

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 です。

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

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

> cdk deploy

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

aws ssm get-parameter --name UserApiUrl
{
    "Parameter": {
        "Name": "UserApiUrl",
        "Type": "String",
        "Value": "https://xxxxyyyzzz.execute-api.ap-northeast-1.amazonaws.com/v1/",
        "Version": 1,
        "LastModifiedDate": 1576684620.448,
        "ARN": "arn:aws:ssm:ap-northeast-1:xxxxxxxxxxx:parameter/UserApiUrl"
    }
}

これで準備完了です。

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

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

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

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

test/eval-cdk19-e2e.test.ts

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 に登録しておけば捗ると思います。参考になれば幸いです。