概要
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」などの)疑似パラメータ参照を可能にするプラグインです。
CloudFormationの疑似パラメータ参照については、こちらを参照。
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
ListObjects
まとめ
以上が「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