Serverless way of uploading pictures to an S3 Bucket
Tags: devops, aws, cloud, serverless
Published: 2022-06-06 16:49:40

In creating full-stack applications, there will come a time when a developer undergoes R&D of how to store pictures and files and dynamically allocate them to a specified user. Cases such as profile pictures, photo uploads and other dynamic files that can be transmitted as binary data that are unique to each user are some of the entry-level challenges a developer will experience. And one of the most efficient way of handling these things are through cloud services such as Amazon S3.


Uploading a file or photo to an Amazon S3 bucket is easy in a server-full architect-ed platform. In the case of hosting a backend service in an EC2 platform, one way to upload a file or photo is to upload the image to the server and then the server pushes the image to the S3 bucket. The client then have to call a get request to the server if it wishes to get a resource from the S3 bucket. Another way of uploading is to immediately upload the the image from the frontend to the S3 bucket. This way, the server does not have to handle the image data being passed on from backend to the S3 bucket.


However, if your choice of architecture is a serverless one and that you want the uploading of an image to an S3 bucket be handled by your lambda functions then there is a way for that too! This article would be a step by step guide of how to upload images through lambda functions! 


For this example, we would be using AWS CDK as our IaC of choice.

_**Overall Process:**_
1. Configure the infrastructure by creating a lambda function and an S3 bucket construct.
2. Create an IAM policy statement for the said lambda function that has get and put access to the S3 bucket.
3. Edit the lambda function handler where it returns a signed URL from the S3 bucket.
4. Invoke the lambda function.
5. Upload image using the signed URL (this is a PUT request) from the client.

First is to create an S3 bucket construct.
```ts
import * as s3 from 'aws-cdk-lib/aws-s3'

new s3.Bucket(this, 'bucketID', {
  bucketName: 'bucket name'
  publicReadAccess: true,
  cors: [{
    allowedMethods:[ // Allow methods
      s3.HttpMethods.GET,
      s3.HttpMethods.POST,
      s3.HttpMethods.PUT,
    ],
    allowedOrigins:['*'] // Just for the sake of local and dev development
    allowedHeaders:['*']
  }]
})
```
Next is to create a lambda function construct. This lambda function returns a signed URL from Amazon S3.
```ts
import * as lambda from 'aws-cdk-lib/aws-lambda'

const getsignedurl = new lambda.Function(this, 'function', {
  runtime: lambda.Runtime.NODEJS_14_X,
  code: lambda.Code.fromAsset('path/to/function'),
  handler: 'function.handler',
  environment: {
    'UploadBucket': BUCKET_NAME // 'bucket name'
  },
})
```
Add a Amazon S3 policy statement to be granted to the newly created lambda function.
```ts
// s3Bucket Policy for profileImageUpload lambda function
const s3BucketPolicy = new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,  
  actions: ['s3:getObject','s3:putObjectAcl', 's3:putObject'],  
  resources: [`arn:aws:s3:::${BUCKET_NAME}/*`]
})

// Added S3 policy to image upload lambda function
getsignedurl.role?.attachInlinePolicy(
  new iam.Policy(this, 'uploadPolicy', {
    statements: [s3BucketPolicy]
  })
)
```
Also, _**don't forget to integrate the lambda function in its corresponding API gateway**_ to serve as a trigger!

The code below will be the logic inside your newly created lambda function.
```ts
const AWS = require('aws-sdk')

const s3 = new AWS.S3()
const URL_EXPIRATION_SECONDS = 300

exports.handler = async function (event: any) {
  return await profileImageUpload(event)
}

export const profileImageUpload = async (event: any) => {
  try{
    const Key = `${userID}.jpg`
        
    const s3Params = {
      Bucket: process.env.UploadBucket,     
      Key,      
      Expires: URL_EXPIRATION_SECONDS,      
      ContentType: 'image/jpeg',      
      ACL: 'public-read'    
    }

    const uploadURL = await s3.getSignedUrlPromise('putObject', s3Params)

    return {
      body: JSON.stringify({
        uploadURL: uploadURL,        
        Key
      }),      
      headers: { "Content-Type": "application/json" },      
      statusCode: 200    
    }
  }catch (error){
    return error
  }
}
```
The userID will serve as a unique key for the name of the object uploaded to the S3 bucket. This lambda function will return an upload URL and a Key. This upload URL will be used to send a PUT request with the request body having a binary file which is the picture or object to be uploaded.

The example below is a Postman call of our newly created lambda function which returns an upload URL and a Key. The Key is part of the object URL in the cloud which makes it accessible if you stitch together your own bucket URL and Key!

![Postman Picture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iushyj80jnbraw4vfd89.png)

Now call a PUT request to the returned upload URL and attach the picture in the request body. It shown in the example below. 

![Postman Picture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aztujbi8qubwtfe66xe3.png)

Click send and go check your newly uploaded photo in the AWS console!