serverless frameworkでfunction定義を別ファイルに書く場合でもパス指定はserverless.ymlからでよい

###はじめに
[serverless framework](https://www.serverless.com/)を利用してクラウドリソースを管理する場合、小規模なうちはserverless.ymlにすべての内容を記載すると思います。
しかし対象リソースが増えてきた場合、一つのymlにすべての設定を記述するととても見づらくなるため、リソース種類ごと等の単位でymlファイルを分割し、serverless.ymlから該当ファイルを参照することで見通しの良いIaCが実現できます。

ただ、ファイルを分割してディレクトリを分ける場合、変数や外部ファイルへの参照パスも変えなきゃいけないのか…と思う方もおられるかもしれません。
自分自身疑問に思いましたので、実際に検証してみました。

###結論
結論から申し上げると、分割したymlテンプレート側であっても、パスの指定は`読込先のserverless.ymlから見たパス`を指定します。

###検証
以下のような、AWSのLambda関数を管理するプロジェクトがあるとします。
`env`ディレクトリにLambdaの環境変数を管理するyml、`functions`ディレクトリにLambda関数のtypescriptファイル、`slsTemplates`ディレクトリにserverless.ymlから読み込むテンプレートを配置します。
例えばIAMロールなど、別のAWSリソースをslsで管理したくなった際も、`slsTemplates`下に`iam.yml`等のテンプレートファイルを追加することで管理がしやすくなります。

```
├─serverless.yml
├─src
│  ├─env
│  │  ├─dev.yml
│  │  └─prod.yml
│  ├─functions
│  │  ├─foo.ts
│  │  └─bar.ts
└─slsTemplates
    └─lambdaFunctions.yml
```

このプロジェクトの場合、serverless.ymlとlambdaFunctions.ymlはそれぞれ以下のように定義することになります。

```serverless.yml
service:
  name: test-service

custom:
  webpack:
    webpackConfig: ./webpack.config.js
    includeModules: true
  defaultStage: dev
  environment:
    dev: ${file(./src/env/dev.yml)}
    prod: ${file(./src/env/prod.yml)}
  dynamoStreamArn:
    dev: arn:aws:dynamodb:ap-northeast-1:999999999999:table/User/stream/2020-06-23T07:49:58.352
    prod: arn:aws:dynamodb:ap-northeast-1:999999999999:table/User/stream/2020-06-23T07:49:58.352

provider:
  name: aws
  runtime: nodejs12.x
  stage: ${opt:stage, self:custom.defaultStage}
  region: ap-northeast-1
  environment: ${self:custom.environment.${self:provider.stage}}
  apiName: ${self:service}-${self:provider.stage}

functions:
  - ${file(slsTemplates/lambdaFunctions.yml)}
```

```slsTemplates\lambdaFunctions.yml
foo:
  handler: src/functions/foo.handler
  timeout: 30
  memorySize: 256
  role: arn:aws:iam::999999999999:role/test-role
  events:
    - stream:
        type: dynamodb
        arn: ${self:custom.dynamoStreamArn.${self:provider.stage}}
        parallelizationFactor: 10
        batchSize: 1000
        maximumRetryAttempts: 10
bar:
  handler: src/functions/bar.handler
  timeout: 30
  memorySize: 256
  role: arn:aws:iam::999999999999:role/test-role
```

ご覧の通り、lambdaFunctions.ymlにおいて、関数fooのhandlerの指定は`src/functions/foo.handler`として、serverless.ymlからのパスで指定できています。
また、関数fooでeventsに指定しているDynamoDBのstream arnをserverless.ymlのcustomから参照する部分も、serverless.yml内と同様の指定方法で参照できることが分かります。

なぜこのような指定が可能かというと[serverless print](https://www.serverless.com/framework/docs/providers/google/cli-reference/print/)をしてみると分かります。
`serverless print`は、変数等で指定した部分を解決した状態でコンソール上にserverless構成を出力するコマンドです。
上記プロジェクトでは、出力は以下のようになります。

```yml
service:
  name: test-service

custom:
  webpack:
    webpackConfig: ./webpack.config.js
    includeModules: true
  defaultStage: dev
  environment:
    dev: 
      STAGE: dev
      APPSYNC_ENDPOINT: https://xxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
    prod:
      STAGE: prod
      APPSYNC_ENDPOINT: https://xxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
  dynamoStreamArn:
    dev: arn:aws:dynamodb:ap-northeast-1:999999999999:table/User/stream/2020-06-23T07:49:58.352
    prod: arn:aws:dynamodb:ap-northeast-1:999999999999:table/User/stream/2020-06-23T07:49:58.352

provider:
  name: aws
  runtime: nodejs12.x
  stage: dev
  region: ap-northeast-1
  environment: 
    STAGE: dev
    APPSYNC_ENDPOINT: https://xxxxxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-northeast-1.amazonaws.com/graphql
  apiName: test-service-dev

functions:
  - foo:
      handler: src/functions/foo.handler
      timeout: 30
      memorySize: 256
      role: arn:aws:iam::999999999999:role/test-role
      events:
        - stream:
            type: dynamodb
            arn: arn:aws:dynamodb:ap-northeast-1:999999999999:table/User/stream/2020-06-23T07:49:58.352
            parallelizationFactor: 10
            batchSize: 1000
            maximumRetryAttempts: 10
    bar:
      handler: src/functions/bar.handler
      timeout: 30
      memorySize: 256
      role: arn:aws:iam::999999999999:role/test-role
```

このように、serverless frameworkでは、serverless.ymlから外部のテンプレートファイルを読み込む場合、各テンプレートファイル側でパスや変数の解決をするのではなく、serverless.yml上で一つのテンプレートとして統合したうえで変数やパスの解決を行います。
そのため、変数やパスはserverless.yml上から解決できるものを指定することになります。

###最後に
今回はserverless frameworkでテンプレートファイルを分割する際に引っかかりがちなパスの指定について見てきました。
IaCの目的の一つは、インフラの状態を人間がぱっと見で把握しやすくすることであるので、テンプレートファイルの見易さは重要な要素になります。
serverless.ymlは知らず知らずのうちに膨れ上がってしまいがちで、パスを書き換えるのもめんどくさいからディレクトリの整理は後回しでいいか…と思われている方もおられるかと思いますが、パスの書き換えが不要であると分かっていただけたことで、整理へのハードルが一つ下がったんじゃないかな、と思います。