echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Serverless Framework】IAM Roleを共有し複数プロジェクトでリソース毎に割り当てする

概要

AWS のIAM Role(≒実行権限)を共通プロジェクトでまとめて作成しておき、後で別の複数プロジェクトで使用する(=共有する)方法です。

経緯

先日Twitterをしていたら、こんなツイートを見つけました。

一つのterraformプロジェクトで一元管理してたリソースを、要望に合わせてIAMとそれ以外のterraformプロジェクトに分けて同じ名前のIAMを作ってから各リソースでPassRoleするようにしたらハマった arnが同じでも内部的には別のKeyを持っていて、destroyされたIAMを参照しようとしてるっぽい

で、それについて僕が書きのRTをしました。

これ、Serverless Frameworkならどうやるかな。親プロジェクトでIAMRole作って、それをOutputに設定して、子プロジェクトでそれをcf:stack変数で参照する感じかな?

そうしたら、元ツイのツイート主の方と色々お話が弾みまして、元ツイのツイート主の方から、下記疑問をいただきました。

今回の要望だと、IAMとそれ以外のプロジェクトに関してapplyできる組織を完全に分けたいという話だったので、slsでやるとしたらIAM用とそれ以外で完全にプロジェクト分けて、それ以外の方はarnをベタで書く感じになりますかね・・(slsでIAMだけdeployできるのかどうかは知らない)

(中略)

slsのIAMって、providerで指定するときにfunctionsにアタッチするpolicyを記述するイメージなので、functionsがない状態でのslsってどういう挙動になるのかは興味ありましね・・知人がslsで実装してるのですが、functionに権限渡してるだけですね・・

このツイートに対して、下記リプを返しました。

多分functionsがない場合、resourcesで普通にCloudFormationみたいに定義する感じですかね。てかslsでIAMだけってそういややったことないですね。たいてい一つはLambdaをデプロイするので…(最悪、ダミーLambdaを作る手はあるかもしれませんが…)

が、上記リプを返した所で、

  • 「そういやfunctionsを一つも作成しないパターン、やったことないな」
  • 「このやり方も実際に動かしたわけじゃないから、動く確証がないな...」
  • 「なら、実際に確かめてみるか!」

というわけで、実際に確かめてみることにしました。

参考

【Serverless Framework】API Gatewayを複数プロジェクト(serverless.yml)で共有する(Share API Gateway and API Resources)

※前回の記事ですが、前回と類似した処理を行いますので、こちらもあわせて参考にされるとよいと思います。

今回作成する成果物構成

プロジェクト

下記2つのプロジェクトを作成します。

プロジェクト名 説明 備考
iam-share-first IAM関連をまとめて作成するプロジェクト 初めにデプロイしておく必要がある
iam-share-second iam-share-firstで作成したIAMを使用するリソースを割り当てるプロジェクト iam-share-firstのデプロイ後にデプロイする必要がある
IAM Roleについて

「iam-share-first」で、下記2つのIAM Roleを作成します。

定義名 説明 備考
IamRoles3GetObject S3のGet系APIのみ実行を許可するIAM Role List系は不可
IamRoles3ListObjects S3のList系APIのみ実行を許可するIAM Role Get系は不可
Lambdaについて

「iam-share-second」で、下記2つのLambdaを作成します。

Lambda名 説明 割り当てるIAM Role 備考
GetObject S3.GetObjectのみ実行される(はず)のLambda IamRoles3GetObject S3.ListObjectsは権限がないからエラーになる(はず)
ListObjects S3.ListObjectsのみ実行される(はず)のLambda IamRoles3ListObject S3.GetObjectは権限がないからエラーになる(はず)

ちなみに、上記2つのLambdaはソースコード自体は全く同じで、その内容は下記になります。

※S3のGetObjectとListObjectsを実施するソースになります。ただ付与しているIAM Roleはそれぞれ異なるので、IAM Roleを付与していない方の処理は権限がなくてエラーになる...という確認です。

※「suzukima-iam-share-test-bucket」には、あらかじめ「I am iamTest.txt」という内容の「iamTest.txt」というテキストファイルを格納しておきます。

'use strict';  
  
const AWS = require("aws-sdk");
const S3 = new AWS.S3();
const BUCKET_NAME = "suzukima-iam-share-test-bucket";
  
module.exports.index = async event => {
  
  console.log(`[event] ${JSON.stringify(event)}`);
  
  try{
    await getObject();
    console.info('GetObject Succeeded');
  } catch(e) {
    console.warn(`[GetObject Error] ${e.message}`);
    console.warn('GetObject Failed');
  }
  
  try{
    await listObjects();
    console.info('listObjects Succeeded');
  } catch(ex) {
    console.warn(`[listObjects Error] ${ex.message}`);
    console.warn('listObjects Failed');
  }
  
  return;  
};
  
async function getObject() {
  const params = {
    Bucket: BUCKET_NAME,
    Key: "iamTest.txt"
  };
  
  const data = await S3.getObject(params).promise();
  console.info(`[GetObject Body] ${data.Body.toString()}`);
  
  return;
}
  
async function listObjects() {
  const params = {
    Bucket: BUCKET_NAME,
  };
  
  const lists = await S3.listObjects(params).promise();
  for(const content of lists.Contents) {
    console.info(`[listObjects Key] ${content.Key}`);
  }
  
  return;
}

iam-share-firstについて

※各プロジェクトのserverless.ymlについては、ブログの最後に載せておきます。
※検証当時はAPI Gatewayも共有すると勘違いして、API Gateway関連のリソースも定義していますが、IAM Roleの共有だけなら不要です。

ただし前回記事と異なり、iam-share-firstではLambdaを一切作成していませんので、その点では参考になると思います。)

resources.Resources

[resources.Resources]内で、先程の「IamRoles3GetObject」及び「IamRoles3ListObjects」の2つのIAM Roleを作成します。

そしてそれぞれに、(今回の対象バケットである)「suzukima-iam-share-test-bucket」に対する「S3.Getxxx」及び「S3.Listxxx」系のアクションを許可します。

またこの2つのIAM Roleは共にLambda関数に割り当てますが、そのLambda関数ではログを出力したいので、CloudWatch Logsに関するすべてのアクションも許可しています。

resources.Outputs

[resources.Outputs]では、[resources.Resources]で定義した2つのIAM Roleについて、「出力」タブに出力するように設定を行っています。
前回同様、こうすることによって、この2つのIAM RoleのARNをiam-share-secondで参照できるようにしています。

なお、 iam-share-first内で使用している「serverless-pseudo-parameters」プラグインですが、これはServerless FrameworkにてCloudFormationの(「AWS::AccountId」などの)疑似パラメータ参照を可能にするプラグインです。

www.serverless.com

CloudFormationの疑似パラメータ参照については、こちらを参照。

docs.aws.amazon.com

iam-share-secondについて

functions

iam-share-secondでは、[functions]で定義するLambda関数(「GetObject」及び「ListtObjects」)の「role」に、iam-share-firstで作成したIAM RoleのARNを指定するだけでOKです。

また、ここではServerless Frameworkの「${cf:[スタック名].[キー名]}」変数を使用して、iam-share-firstスタックの「出力」タブから各IAM RoleのARNを取得しています。

動作確認

実際に動作確認した結果(ログ)が、下記2つの画像になります。

GetObject関数では、S3.getObject()は正常に実施され、「iamTest.txt」の中身の「I am iamTest.txt」というログが表示されていますが、S3.listObjects()は実行権限がないため「Access Denied」となっています。

逆にListObjects関数では、S3.listObjects()は正常に実施され、バケット内の「iamTest.txt」がログに表示されていますが、S3.getObjects()は実行権限がないため「Access Denied」となっています。

このことからも、IAM Role、およびそのLambda関数への割り当てが正常に動作しているといえます。

GetObject

f:id:Makky12:20201006194537p:plain

ListObjects

f:id:Makky12:20201006194555p:plain

まとめ

以上が「IAM Roleを共有し複数プロジェクトでリソース毎に割り当てする」方法になります。

「なんでそんな面倒くさい事するの?」と思われるかもしれませんが、前回の記事でも書いた通り、プロジェクト自体が大きくなって、リソース管理やライフサイクルなどでのリソース分割を本格的に考えなければならなくなった際、リソース共有やプロジェクト分割は非常に重要になってきます。

というか、それらを本格的に考えなければならなくなった時には、すでに手遅れだったりする場合もあるので、リソース共有やプロジェクト分割は余裕のあるうちから考慮・対処をしておくのがおすすめです。(自分も痛い目にあっていますので...)

それでは、今回はこの辺で。

serverless.ymlについて

iam-share-first
  
service: iam-share-first  
provider:
  name: aws
  runtime: nodejs12.x
  
  apiGateway:  
    restApiResources:
      /s3:
        Ref: SuzukimaRestApiResourceS3
  
  stage: dev
  region: ap-northeast-1
  profile: default
  
plugins:
  - serverless-pseudo-parameters
  
custom:
  projectName: iam-share-test
  
package:  
  exclude:
    - node_modules
    - node_modules/**
  
# you can add CloudFormation resource templates here
resources:
  Resources:
    SuzukimaRestApi:
      Type: AWS::ApiGateway::RestApi
      Properties:
        Name: ${self:custom.projectName}-RestApi
    SuzukimaRestApiResourceS3:
      Type: AWS::ApiGateway::Resource
      Properties:
        RestApiId: !Ref SuzukimaRestApi
        ParentId:
          'Fn::GetAtt': [ SuzukimaRestApi, RootResourceId ]
        PathPart: 's3'
      DependsOn:
        - SuzukimaRestApi
    SuzukimaBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: suzukima-${self:custom.projectName}-bucket
    IamRoles3ListObjects:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            -
              Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action:
                - sts:AssumeRole
        Path: "/"
        Policies:
          -
            PolicyName: ${self:custom.projectName}-s3ListObjectsPolicy
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                -
                  Effect: Allow
                  Action:
                    - s3:List*
                  Resource:
                    'Fn::GetAtt': [SuzukimaBucket, Arn]
                -
                  Effect: Allow
                  Action:
                    - logs:*
                  Resource:
                    'Fn::Sub': arn:aws:logs:ap-northeast-1:#{AWS::AccountId}:log-group:/aws/lambda/*
        RoleName: ${self:custom.projectName}-s3ListObjectsRole
      DependsOn:
        - SuzukimaBucket
    IamRoles3GetObject:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            -
              Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action:
                - sts:AssumeRole
        Path: "/"
        Policies:
          -
            PolicyName: ${self:custom.projectName}-s3GetObjectPolicy
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                -
                  Effect: Allow
                  Action:
                    - s3:Get*
                  Resource:
                    'Fn::Join':
                      - "/"
                      - - 'Fn::GetAtt': [SuzukimaBucket, Arn]
                        - "*"
                -
                  Effect: Allow
                  Action:
                    - logs:*
                  Resource:
                    'Fn::Sub': arn:aws:logs:ap-northeast-1:#{AWS::AccountId}:log-group:/aws/lambda/*
        RoleName: ${self:custom.projectName}-s3GetObjectRole
      DependsOn:
        - SuzukimaBucket  
  
  Outputs:
    apiGatewayRestApiId:
      Value:
        Ref: SuzukimaRestApi
      Export:
        Name: ${self:custom.projectName}-restApiId-${self:provider.stage}
    apiGatewayRestApiRootResourceId:
      Value:
        'Fn::GetAtt': [SuzukimaRestApi, RootResourceId]
      Export:
        Name: ${self:custom.projectName}-rootResourceId-${self:provider.stage}
    apiGatewayResourcesS3:
      Value:
        Ref: SuzukimaRestApiResourceS3
      Export:
        Name: ${self:custom.projectName}-ResourcesS3-${self:provider.stage}
    iamRoleS3ListObjectsArn:
      Value:
        'Fn::GetAtt': [IamRoles3ListObjects, Arn]
      Export:
        Name: ${self:custom.projectName}-iamRoleS3ListObjectsArn-${self:provider.stage}
    iamRoleS3GetObjectArn:
      Value:
        'Fn::GetAtt': [IamRoles3GetObject, Arn]
      Export:
        Name: ${self:custom.projectName}-iamRoleS3GetObjectArn-${self:provider.stage}
iam-share-second
service: iam-share-second  
  
provider:
  name: aws
  runtime: nodejs12.x
  
  stage: dev
  region: ap-northeast-1
  profile: default
  
  apiGateway:
    restApiId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiId}  
    restApiRootResourceId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiRootResourceId}  
    restApiResources:
      /s3: ${cf:${self:custom.parentStackName}.apiGatewayResourcesS3}
  
custom:
  parentStackName: iam-share-first-dev
  projectName: iam-share-test

package:
  exclude:
    - node_modules
    - node_modules/**
    - restClient.txt

functions:
  GetObject:
    handler: iamTest.index
    name: GetObject
    role: ${cf:${self:custom.parentStackName}.iamRoleS3GetObjectArn}
    events:
      - http:
          path: /s3/get
          method: get
  
  ListObjects:
    handler: iamTest.index
    name: ListObjects
    role: ${cf:${self:custom.parentStackName}.iamRoleS3ListObjectsArn}
    events:
      - http:
          path: /s3/list
          method: get