Hello!
I have been receiving the error in the title of this post repeatedly, despite having scoured my serverless.yml
file for hours, spending all night tweaking things, researching, tweaking again, consulting documentation, etc, etc. I cannot, for the life of me, figure out what I’ve got wrong and why this isn’t working.
The Stack
My stack is the photos backend for a mobile app I am developing. Initially, I had it set up to accept requests through an AWS HTTP 2.0 API Gateway and forward them to a one of three Lambda functions, one to return the result of a createPresignedPost
request, one for deleting a single object, and one for deleting multiple objects. I also implemented a custom authorizer as a Lambda function, which receives a JSON web token generated when my users authenticate and passes it in each of the above three requests as an Authorization
header, which is decoded using a secret stored in AWS secrets in order to authenticate the above CRUD-style requests. Users upload files using the createPresignedPost
API call, which returns a URL and some headers, which are used to construct a POST
request to that url with the image file attached, and, volia! the image is in my AWS bucket.
All of this was working wonderfully.
I then moved on to upgrade the system, first by adding an s3 event trigger on the aforementioned bucket, for the s3:ObjectCreated:*
action, which runs a Lambda function that compresses uploaded images as JPGs. Initially, I followed the guidance to set up the event entirely under the the lambda function in my serverless.yml
, and the first problem I ran into was I was getting the ‘bucket already exists’ error, because my event trigger was creating the bucket I had already specified in my resources
section. First I marked it with existing: true
, but this did nothing, and my assumption is this only works for buckets that exist already in the stack on AWS, not potentially in the serverless deployment. I had to remove the duplicate bucket from my resources
section, and because I need additional configuration options, such as LifecycleConfiguration
, CorsConfiguration
, and PublicAccessBlockConfiguration
, I followed guidance in the docs to move the s3 bucket and event trigger to the provider
section of serverless.yml
. I was still having problems and realized I had to define my s3 event trigger under the providers.s3
definition, as part of notificationConfiguration.LambdaConfigurations
. This solved that error. Now I am stuck on the error in the title, something seems to be wrong with my iAmRoleStatements
section, but I can’t figure out what it is.
The compression Lambda function gets files from the original bucket, sourceBucket
, and after compressing them, places them into targetBucket
. I created a resource for the targetBucket
, but not the sourceBucket
, because that is created in the definition under provider
. I also have BucketPolicy
resources for both buckets and I’ve given them minimum needed iAmRoleStatements
permissions under provider
. I also added a third bucket and corresponding BucketPolicy
and iAMRoleStatement
for a future publicBucket
. Basically, users select or take photos in my app and they are immediately added to sourceBucket
, then compressed and copied to targetBucket
. Only after a user either finishes creating a profile or presses ‘Save Changes’ on their ‘edit profile’ page, will those photos in targetBucket
be moved to publicBucket
, using a batch action to limit API calls, where they can be consumed by the public in the app as profile photos. The photos in sourceBucket
and targetBucket
act as temporary files and have an ExpirationInDays
LifecycleConfiguration
rule that periodically cleans out any images left behind after users either successfully create/edit their profiles, or abandon the process.
serverless.yml
Below is my serverless.yml
in full:
org: *redacted*
app: *redacted*
service: *redacted*
frameworkVersion: "3"
custom:
region: us-east-1
sourceBucketName: *redacted*
targetBucketName: *redacted*
publicBucketName: *redacted*
jpgCompressionQuality: 90
apiSecret: ${ssm:/aws/reference/secretsmanager/*redacted*}
provider:
region: ${self:custom.region}
name: aws
runtime: nodejs14.x
httpApi:
cors: true
authorizers:
customAuthorizer:
type: request
functionName: tokenAuthorizer
payloadVersion: '2.0'
identitySource:
- $request.header.Authorization
s3:
sourceBucket:
name: ${self:custom.sourceBucketName}
accessControl: Private
lifecycleConfiguration:
Rules:
- Id: TempDeletionRule
Status: Enabled
ExpirationInDays: 7
corsConfiguration:
CorsRules:
- AllowedMethods:
- GET
- POST
AllowedOrigins:
- "*"
AllowedHeaders:
- "*"
publicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
notificationConfiguration:
LambdaConfigurations:
- Event: "s3:ObjectCreated:*"
Function:
"Fn::GetAtt":
- CompressImagesHandlerLambdaFunction
- Arn
iamRoleStatements:
- Effect: "Allow"
Action:
- "s3:GetObject"
- "s3:GetObjectAcl"
- "s3:PutObject"
- "s3:PutObjectAcl"
- "s3:DeleteObject"
- "s3:PutAccelerateConfiguration"
Resource:
- arn:aws:s3:::${self:custom.sourceBucketName}
- arn:aws:s3:::${self:custom.publicBucketName}
- Effect: "Allow"
Action:
- "s3:GetObject"
- "s3:ListBucket"
- "s3:GetBucketLocation"
- "s3:GetObjectVersion"
- "s3:PutObject"
- "s3:PutObjectAcl"
- "s3:GetLifecycleConfiguration"
- "s3:PutLifecycleConfiguration"
- "s3:DeleteObject"
- "s3:PutAccelerateConfiguration"
Resource: arn:aws:s3:::${self:custom.targetBucketName}
- Effect: "Allow"
Action:
- "lambda:InvokeFunction"
Resource:
- arn:aws:s3:::${self:custom.sourceBucketName}
- Effect: "Allow"
Action:
- "ssm:GetParameter*"
- "kms:Decrypt"
Resource:
- arn:aws:secretsmanager:${self:custom.region}:${AWS::AccountId}:secret:*
- arn:aws:ssm:${self:custom.region}:${AWS::AccountId}:parameter/*
package:
individually: true
exclude:
- ".serverless/**"
- "*.*"
functions:
tokenAuthorizer:
handler: src/auth.handler
environment:
API_SECRET: ${self:custom.apiSecret}
createPresignedPostHandler:
handler: src/api.createPresignedPost
events:
- httpApi:
method: POST
path: /createUpload
authorizer:
name: customAuthorizer
type: request
environment:
S3_BUCKET_NAME: ${self:custom.sourceBucketName}
deleteObjectHandler:
handler: src/api.deleteObject
events:
- httpApi:
method: POST
path: /deleteUpload
authorizer:
name: customAuthorizer
type: request
environment:
S3_BUCKET_NAME: ${self:custom.sourceBucketName}
deleteObjectsHandler:
handler: src/api.deleteObjects
events:
- httpApi:
method: POST
path: /deleteUploads
authorizer:
name: customAuthorizer
type: request
environment:
S3_BUCKET_NAME: ${self:custom.sourceBucketName}
compressImagesHandler:
handler: src/transform.compress
events:
- s3: sourceBucket
environment:
S3_TARGET_BUCKET_NAME: ${self:custom.targetBucketName}
JPG_COMPRESSION_QUALITY: ${self:custom.jpgCompressionQuality}
resources:
Resources:
TargetBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:custom.targetBucketName}
AccessControl: Private
LifecycleConfiguration:
Rules:
- Id: TempCompressDeletionRule
Status: Enabled
ExpirationInDays: 7
CorsConfiguration:
CorsRules:
- AllowedMethods:
- GET
- POST
AllowedOrigins:
- "*"
AllowedHeaders:
- "*"
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
PublicBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:custom.publicBucketName}
AccessControl: Private
CorsConfiguration:
CorsRules:
- AllowedMethods:
- GET
- POST
AllowedOrigins:
- "*"
AllowedHeaders:
- "*"
SourceBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Statement:
- Sid: AdminActions
Effect: Allow
Principal:
AWS: arn:aws:iam::${AWS::AccountId}:root
Action:
- "s3:GetObject"
- "s3:GetObjectAcl"
- "s3:PutObject"
- "s3:PutObjectAcl"
- "s3:DeleteObject"
Resource: arn:aws:s3:::${self:custom.sourceBucketName}/*
Bucket: ${self:custom.sourceBucketName}
TargetBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Statement:
- Sid: AdminActions
Effect: Allow
Principal:
AWS: arn:aws:iam::${AWS::AccountId}:root
Action:
- "s3:GetObject"
- "s3:GetObjectAcl"
- "s3:PutObject"
- "s3:PutObjectAcl"
- "s3:DeleteObject"
Resource: arn:aws:s3:::${self:custom.targetBucketName}/*
Bucket:
Ref: TargetBucket
PublicBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Statement:
- Sid: PublicObjectRead
Effect: Allow
Principal: "*"
Action:
- "s3:GetObject"
Resource: arn:aws:s3:::${self:custom.publicBucketName}/*
- Sid: AdminActions
Effect: Allow
Principal:
AWS: arn:aws:iam::${AWS::AccountId}:root
Action:
- "s3:GetObject"
- "s3:GetObjectAcl"
- "s3:PutObject"
- "s3:PutObjectAcl"
- "s3:DeleteObject"
Resource: arn:aws:s3:::${self:custom.publicBucketName}/*
Bucket:
Ref: PublicBucket
CompressImagesHandlerLambdaPermissionSourceBucketS3:
Type: "AWS::Lambda::Permission"
Properties:
FunctionName:
"Fn::GetAtt":
- CompressImagesHandlerLambdaFunction
- Arn
Principal: "s3.amazonaws.com"
Action: "lambda:InvokeFunction"
SourceAccount:
Ref: AWS::AccountId
SourceArn: arn:aws:s3:::${self:custom.sourceBucketName}
Error
The full error I am getting is:
Error:
UPDATE_FAILED: IamRoleLambdaExecution (AWS::IAM::Role)
The policy failed legacy parsing (Service: AmazonIdentityManagement; Status Code: 400; Error Code: MalformedPolicyDocument; Request ID: *redacted*; Proxy: null)
If anyone can identify why I am receiving this error, I would really, really appreciate the help. I have been banging my head against the wall for way too long on this with no progress, and I am up against the wall with a deadline. My backup plan is to revert to what I had working before and try to install the compression event trigger manually on the AWS web console, but I would really prefer the whole thing be written out using serverless/IaC. Hopefully it’s something stupid that my tired eyes just can’t see anymore.
Thanks,
Avana
Update:
For some reason this forum’s spam bot hid my reply to this post, so I’m adding it here.
I solved the malformed policy issue. For reference, I had to use ${aws:accountId}
in string replacement contexts but AWS::AccountId
in Ref:
contexts. I was switching between either format for all in my attempts,
A new problem rears its ugly head
Anyway, now I’m getting a new error that has me stumped.
Error:
CREATE_FAILED: S3BucketSourceBucket (AWS::S3::Bucket)
*redacted* already exists in stack arn:aws:cloudformation:us-east-1:*redacted*:stack/*redacted*/*redacted*
Somehow cloud formation is trying to create my sourceBucket
twice, even though I specifically removed it from the resources
section because it’s defined with the event trigger under provider
. I also tried removing the associated BucketPolicy
to no avail.
I followed the instructions here to a tee, for configuring an event trigger to work with a custom bucket configuration.
What’s weird is in my attempts to troubleshoot this earlier, I removed all of the event trigger stuff, and just tried to have the original code that was working, plus create another two buckets as resources
and I was getting this same error. As soon as I removed the other two buckets it worked again. Yet there was no naming confusion or mixed up variables.
So I am thinking either there is something going on with the fact that I’m trying to crate multiple buckets, or the guidance for referencing custom buckets in the serverless docs is wrong.