deploymentRoleを設定したServerless FrameworkのスタックでのLambdaイベントの罠

わかりにくいタイトルですが・・・


まとめを 1行 1文で

Serverless Frameworkでは、deploymentRoleを指定して、S3などの既存リソースへのLambdaイベント設定を行なった場合、内部で作成されたカスタムリソースLambdaのIAMロールにdeploymentRoleがアタッチされる(長い・・・)

もう少し掘り下げて

Serverless Frameworkでは・・・

  • CloudFormation APIでのサービスロールオプション--role-arnは、provider:iam:deploymentRoleにあたる
  • S3などの既存リソースへのLambdaイベント設定では、内部でカスタムリソース(Lambda-backed)が作成される
  • deploymentRoleが設定されているときは、作成されたカスタムリソースLambdaのIAMロールにはdeploymentRoleがアタッチされる

つまり?

Serverless Frameworkで使用するdeploymentRole(CloudFormationサービスロール)には、CloudFormationサービスだけでなくLambdaサービスもAssumeできるようにしておかないといけない



経緯

Servlerless Frameworkでよくあるスタックを作ろうとしたらハマった(?)ので書いてみた。

やりたかったこと

  • 下記をServlerless Frameworkで実現する

    • ①スタック作成をCloudFormationサービスロールをアタッチして行いたい
    • ②S3へのLambdaイベント設定で、Lambdaを作成したときに「勝手に」S3が作成されないようにしたい

①スタック実行をCloudFormationサービスロールをアタッチして行いたい

CloudFormationサービスロールが何かという点は、こちらの記事を参照してください。

go-to-k.hatenablog.com
上記のようなCloudFormationサービスロール--role-arnをServlerless Frameworkでやる場合、provider:iam:deploymentRoleというオプションになります。

www.serverless.com

そのため、あらかじめCloudFormationのサービスロール用のIAMロールを作成しておき、serverless.yml内に定義する必要があります。

provider:
  iam:
    role:
      name: custom-role-name
      statements:
        - Effect: 'Allow'
          Resource: '*'
          Action: 'iam:DeleteUser'
    deploymentRole: arn:aws:iam::123456789012:role/Test-Service-Role # <-これ

このようにdeploymentRoleによって、Servlerless FrameworkでCloudFormationサービスロールを使用してデプロイすることが可能となります。


ちなみに作成するサービスロール用のIAMロールはこんなものです。

  DeployRoleForCloudFormation:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "Test-Service-Role"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service:
                - "cloudformation.amazonaws.com"
      Policies:
        - PolicyName: "Test-Service-Policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "ecs:*"
                Resource:
                  - "*"


②S3へのLambdaイベント設定で、Lambdaを作成したときに「勝手に」S3が作成されないようにしたい

まず、「S3へのLambdaイベント設定」とはどんなものかというと、こんなものです。

resources:
  LambdaResource:
    runtime: nodejs12.x
    handler: functions/sample.handler
    events:
      - s3:
          bucket: bucket-name
          event: s3:ObjectCreated:*

これは、「bucket-nameというS3バケットにオブジェクトが作成されたらLambdaResourceというLambdaが起動する」というLambdaリソース作成のコードです。


そして、イベント発火元となるS3(bucket-name)を同じスタックで定義したいですよね。

ところが、このLambdaイベントの書き方の場合、Lambdaリソース作成と同時に「Lambdaの発火元のbucket-nameというバケットを新規で作成」しようとするのです。


そのため、同じスタック内で発火元のS3を定義していると、「Already Exist」のようなエラーが出て失敗してしまうのです。

つまり、これだと、Lambdaと同じスタックで発火元のS3バケットを作ることができません。


そこで、Servlerless Frameworkでは以下のような書き方(existing: true)でこの問題を解消する方法を提供してくれています。(もともとはプラグインだった)

resources:
  LambdaResource:
    runtime: nodejs12.x
    handler: functions/sample.handler
    events:
      - s3:
          bucket: bucket-name
          event: s3:ObjectCreated:*
          existing: true ### <- ここを追加!!


これによって、Lambdaの定義と同じスタックでイベント発火元のS3バケットも定義できるのです。


ここで終わりではないのです

...

...

...

ところが・・・!

ここでまた問題があります。


実は上記のexisting: trueでは、Servlerless Frameworkがスタック内部にカスタムリソース(Lambda-backed)を(勝手に)作成するのです。

CustomDashresourceDashexistingDashs3LambdaFunctionという論理名


つまり、既存のS3などのイベントで発火するLambdaを作成すると、もう一つ余計にLambdaがスタック内部に作成される、ということです。


※カスタムリソースが何かというのはこちらをご覧ください。

(ざっくり言うと、CloudFormationで提供されていないリソース定義をLambdaなどを定義することで何でも作れるようにする、みたいなものです)

docs.aws.amazon.com


これの何が問題かということなのですが、別にカスタムリソースLambdaの1つや2つ、勝手に作られることは何の問題でもないのです。

ここで、この記事の一番言いたいことに繋がるのですが、、、


Servlerless Frameworkでは、deploymentRoleが設定されているときに、カスタムリソースLambdaにdeploymentRoleをアタッチする仕様なのです!


deploymentRole + カスタムリソースの問題

Servlerless Frameworkでは、deploymentRoleが設定されているときに、カスタムリソースLambdaにdeploymentRoleをアタッチする仕様なのです!

ここでまたdeploymentRoleが出てきました。①とようやく繋がりました。


ちなみにソースコードや公式のdocなども見てみました。

確かにソースコードを見ても、deploymentRoleがあるときは、カスタムリソースLambdaのIAMロールにセットしていました。

serverless/lib/plugins/aws/customResources/index.js at 8d56d0e520db8068e89fa8d4d2bf4e64b0acd97b · serverless/serverless · GitHub


ユニットテストもあります。

serverless/test/unit/lib/plugins/aws/customResources/index.test.js at c97ffcc0b976a4efe67b14858ac96580d7b9d4a9 · serverless/serverless · GitHub


API GatewayのCloudWatch Logsにもカスタムリソースが作られるそうですが、そこにもdeploymentRoleの記載が。

Note: Serverless configures the API Gateway CloudWatch role setting using a custom resource lambda function. If you're using iam.deploymentRole to specify a limited-access IAM role for your serverless deployment, the custom resource lambda will assume this role during execution.

serverless/docs/providers/aws/events/apigateway.md at ec909452b5167e05d892d2c44bc46b4ff7d7470a · serverless/serverless · GitHub



そして、「カスタムリソースLambdaにdeploymentRoleがアタッチされる」ことの何が問題なのかというと、、、

deploymentRole、つまりCloudFormationサービスロールは、その名の通り「CloudFormationのためのロール」として作っています。

なので、CloudFormationサービスからしか、AssumeRoleできないようにしているのです!


①で作成したIAMロールを再掲

  DeployRoleForCloudFormation:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "Test-Service-Role"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service:
                - "cloudformation.amazonaws.com"
      Policies:
        - PolicyName: "Test-Service-Policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "ecs:*"
                Resource:
                  - "*"

こちらのAssumeRolePolicyDocumentには、

  • cloudformation.amazonaws.com
  • このロールをsts:AssumeRoleできる

ようなロールの内容になっています。

          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service:
                - "cloudformation.amazonaws.com" ### <- これ!!!


つまりこの状態でデプロイしようとすると、LambdaがAssumeRoleできる設定にはしていないので当然エラーが出ます。

An error occurred: CustomDashresourceDashexistingDashs3LambdaFunction - Resource handler returned message: "The role defined for the function cannot be assumed by Lambda.


だから何なのかと、じゃあLambdaサービスがこのロールを使える(Assumeできる)ようにすればいいんじゃ?(以下例)と思われる方もいるかもしれませんがそうもいかないのです。

          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service:
                - "cloudformation.amazonaws.com"
                - "lambda.amazonaws.com" ### <- これ!!!


ここからは完全に個人の主観になるのですが・・・


こういったIAMロール、特にサービスロールは1つのサービス用に作ることが多いので、そもそも複数のサービスがAssumeRoleできるような作り方がしてあると「う〜ん?」となります。(もちろん複数がAssumeRoleして良い場合もあるかとは思います)

特に今回の場合、Lambdaというサービスが、CloudFormation用に作成しているロールを使えてしまうというのが問題なのです。


「CloudFormation用に作成しているロール」とはすなわち、システム・アプリを構成するリソースを作成・変更・削除する権限を全て持っている強力なロールになります。

特にサーバーレス開発などでは、絞ったとしても、かなりAdministratorやPowerUserに近い権限になってしまう場合もあります。

ただそれは、「CloudFormationだからしょうがない、他にどうしようもないよね」みたいなバックグラウンドがあります。


しかし今回、この強力なロールをLambdaが使えるようになってしまうのです。

これは何を意味するのかというと、そもそもLambdaというのはソースコードを実行するサービスです。つまりLambdaというのは、コードさえ書けば何でも自動で行えてしまいます。

そんな何でもできるサービスに、何でもできるIAM権限をアタッチしたら・・・

怖いですよね。適切にポリシー制御や監視をしていても不安や恐怖は残ってしまいます。


最後に

と、いうように、一番最初のまとめに書いた


「Serverless Frameworkでは、deploymentRoleを指定して、S3などの既存リソースへのLambdaイベント設定を行なった場合、内部で作成されたカスタムリソースLambdaのIAMロールにdeploymentRoleがアタッチされる


「Serverless Frameworkで使用するdeploymentRole(CloudFormationサービスロール)には、CloudFormationサービスだけでなくLambdaサービスもAssumeできるようにしておかないといけない


というのが個人的に問題と感じ、この記事を書くに至ったわけです。

ただ、Serverless Frameworkという非常に有名なサービスの仕様だし、特にこのような内容の記事も見たことがないので、実は勘違いとかそんなに問題ではないとか考えすぎとか回避策があるとか...って話なのかもしれません。


長くなりましたが、読んでくださった方、ありがとうございました。



追記

Serverless FrameworkのGitHubリポジトリのissueにて議論されていました(議論が終わってクローズもされていました)。

github.com


ここでのカスタムリソースLambdaはデプロイの一環であるからdeploymentRoleを使用する、セキュリティ的にも脅威はない、というような旨でした。

う〜む・・・