Showing posts with label AWS. Show all posts
Showing posts with label AWS. Show all posts

Deploying a .NET Core Lambda Project from the .NET Core CLI

In my previous post about "Creating a Lambda function in .NetCore with Visual Studio and AWS Visual Studio Toolkit  we learnt about how to create a lambda function in .net core and deploy it using Visual Studio Toolkit.

The deployment part can also be accomplished from command line using ".NET Core Global Tools for AWS". To install the .Net core global Lambda tools, use the following command in dotnet command line

dotnet tool install --global Amazon.Lambda.Tools --version 3.3.0

It will install the 3.3.0 (current version) of Lambda global tools. If you want to update the global tools you can use it as

dotnet tool update -g Amazon.Lambda.Tools

Deploy Lambda using Global Lambda Tools

Before going to deployment of lambda using Lambda global tools, first you need to understand about the "aws-lambda-tools-defaults.json" file. The file seems as below

{
  "Information": [
    "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI."
  ],

  "profile":"default",
  "region" : "us-east-1",
  "configuration": "Release",
  "framework": "netcoreapp2.1",
  "function-runtime": "dotnetcore2.1",
  "function-memory-size": 256,
  "function-timeout": 30,
  "function-handler": "Lambda::Lambda.Function::FunctionHandler",
  "function-name": "LambdaTest",
  "function-role": "arn:aws:iam::XXXXXXXXXXXX:role/service-role/lambda_basic_execution",
  "environment-variables" : "\"SourceQueue\"=\"source-queue\";\"TargetQueue\"=\"target-queue\";\"ErrorSns\"=\"my-error-sns\""
}
This file has a role for both manual and automated deployments. When you publish with AWS Visual Studio toolkit, the fields are pre-populated with the values from this file.When you use the command line interface, it will use the values specified in this file unless we override those in the command 

Following are some important settings

1) function-handler: This is how you tell AWS Lambda which method to invoke.  It is made up of 3 parts:

  a) Assembly / Namespace name (May be the same as the assembly name if your project isn't complex enough for a namespace heirarchy.)
  b) Class name along with Namespace
  c) Method name

2) function-name: Name of the lambda function in AWS
3) function-role: IAM role arn for executing Lambda.
4) environment-variables: Environment variables which needs to execute the lambda.

Once the settings are correctly placed, open the dotnet command line, navigate to the project folder and run the following command

Note: Your should install & configure the AWS CLI [https://aws.amazon.com/cli/] before running the below commands 

dotnet-lambda deploy-function 
Note: We can use either "dotnet-lambda" or "dotnet lambda"

It will deploy the lambda function to your account

Now you can found the lambda function created in your AWS account using AWS Console

The values are set automatically as we mentioned in the "aws-lambda-tools-defaults.json"


Overriding "aws-lambda-tools-defaults.json" settings

We can override the settings mentioned in "aws-lambda-tools-defaults.json" file while deploying the application by providing some extra parameters to the deploy-function command
The below command will create a function with "LambdaTest2" and 4 environment-variables
dotnet-lambda deploy-function -fn "LambdaTest2" -ev "SourceQueue=sourcequeue;TargetQueue=targetqueue;ErrorSns=myerrorsns;MiddleQueue=middlequeue"
It will create new function in the AWS with name "LambdaTest2" with 4 environment varaiables


You can check the remaining parameters accepted by deploy-function by using command
dotnet-lambda deploy-function --help

You can find more here.

Happy coding 😊!

Creating a Lambda function in .netCore with Visual Studio and AWS Visual Studio Toolkit

AWS allows .net Core framework also while creating Lambda functions. But the AWS console doesn't have any editor for .Net Core framework. It just has the option to upload the code in a zip file. We need to create the Lambda function in our local machine and upload the package file. We can create the Lambda function easily in Visual Studio with the template given in AWS Toolkit for Visual Studio. AWS Toolkit for Visual Studio have some templates for creating AWS Lambda functions easily using .NET Core. This post will guide you to create a Lambda function in .Net core using Visual Studio

Prerequisites

Following are the prerequisites for creating and deploying Lambda functions in Visual Studio

Create a .NET Core Lambda in Visual Studio

Following are the steps to create a Lambda function in Visual Studio
1) Open Visual Studio -> File menu -> New -> Project.
2) In the New Project dialog box, Select "AWS Lambda" under Installed -> Visual C#

     It will show you two types of project.
a) AWS Lambda project: These templates are for creating a project to develop and deploy an                 individual Lambda function.
b) AWS Serverless Application: These templates are for creating Lambda functions with a server-less AWS CloudFormation template. AWS server-less applications enable you to define more than just the function. For example, you can simultaneously create a database, add IAM roles, etc., with server-less deployment. AWS server-less applications also enable you to deploy multiple functions at one time.

3) Select AWS Lambda Project (.NET Core - C#) template.
4) Enter Name and Location for the project.
5) In the next screen, select Blue print for the Lambda you want to develop. For this example, I am selecting Empty Template.
6) It will create the project with the following structure 
Here, we need to examine two files
  • Function.cs
  • aws-lambda-tools-defaults.json

Function.cs 

Following is the sample code which was created automatically in Function.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

using Amazon.Lambda.Core;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace LambdaSample
{
    public class Function
    {
        
        /// 
        /// A simple function that takes a string and does a ToUpper
        /// 
        /// 
        /// 
        /// 
        public string FunctionHandler(string input, ILambdaContext context)
        {
            return input?.ToUpper();
        }
    }
}
Here FunctionHandler is the function where we need to write out Lambda function code. The above code will take a string as parameter and will return the same in UPPER Case. Here ILambdaContext object provides properties with information about the invocation, function, and execution environment(Read more about ILambdaContext here). For the time being, I am not writing any logic in the function, but I just added some logs. My FunctionHandler is as below after adding some logs to it
public string FunctionHandler(string input, ILambdaContext context)
{
 context.Logger.LogLine($"Input string:{input}");
 var upperCaseValue = input?.ToUpper();
 context.Logger.LogLine($"Upper case value:{upperCaseValue}");
 return upperCaseValue;
}

aws-lambda-tools-defaults.json

This is the file from where the Lambda function creator reads the default values while deploying the lambda function. You can set the default values like, framework, run time, memory size, timeout, function-handler etc parameters required for Lambda. Here function-handler defines the starting functionof the Lambda. For our sample it is "LambdaSample::LambdaSample.Function::FunctionHandler". In this, first part indicates the NameSpace(LambdaSample), second part indicates the ClassName with namespace(LambdaSample.Function) and third part indicates the Function name in the class(FunctionHandler).

Following is the aws-lambda-tools-defaults.json file created automatically by Visual Studio
{
  "Information" : [
    "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.",
    "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.",

    "dotnet lambda help",

    "All the command line options for the Lambda command can be specified in this file."
  ],

  "profile":"default",
  "region" : "us-east-1",
  "configuration" : "Release",
  "framework" : "netcoreapp2.1",
  "function-runtime":"dotnetcore2.1",
  "function-memory-size" : 256,
  "function-timeout" : 30,
  "function-handler" : "LambdaSample::LambdaSample.Function::FunctionHandler"
}

Note: If you change the class name or function name, you need to update function-handler in the above file

Publish the Lambda Function

1) In Solution Explorer, right-click the project, and then choose Publish to AWS Lambda.

2) On the Upload Lambda Function page, in Function Name, type a name for the function or select a previously published function to republish -> Choose Next.

3) In the Advanced Function Details page,
  • Select Existing role: Select any role assoicated with your role. The role is used to provide credentials for any AWS service calls made by the code in the function. Your account should have IAM:ListPolicies action, or the Role Name list will be empty and you will be unable to continue.
  • Change Memory & Timeout values if required. 
  • Assign VPC and its Subnets if those are required by your Lambda
  • Add any Environment variable that your Lambda function needs

4) Click on Upload
5) This will open Uploading function page as shown below and the screen will automatically closes after Lambda function uploaded to AWS account

6) After function uploaded successfully, it will opens the following screen which allows you to execute the lambda function, change the Configuration,Event Sources and check the logs from Visual Studio. 

Note: You can also open the above screen for any existing Lambda function by Opening "AWS Explorer" -> AWS Lambda -> Double click on the required function.

Invoke the Function from Visual Studio

We can invoke the Lambda function from Visual Studio directly by the screen shown in the above image

1) Select any Example Request to choose any predefined request. But for our sample, it is just accepting a string as input, I have given "gopiportal_test" in the Request box


2) Click on "Invoke" button
3) This will execute the Lambda function automatically and will show the Response and the function logs

That's it...
Happy Coding !😊

Bitbucket Pipeline to zip the source code and upload to S3 bucket

Bitbucket Pipelines is an integrated CI/CD service, built into Bitbucket. It allows us to automatically build, test and even deploy our code, based on a configuration file in our repository. To set up Pipelines we need to create and configure the bitbucket-pipelines.yml file in the root directory of our repository.

Continuous Delivery Vs Continuous Integration

Continuous delivery is a software development methodology where the release process is automated. Every software change is automatically built, tested, and deployed to production. Before the final push to production, a person, an automated test, or a business rule decides when the final push should occur. Although every successful software change can be immediately released to production with continuous delivery, not all changes need to be released right away.

Continuous integration is a software development practice where members of a team use a version control system and frequently integrate their work to the same location, such as a master branch. Each change is built and verified to detect integration errors as quickly as possible. Continuous integration is focused on automatically building and testing code, as compared to continuous delivery, which automates the entire software release process up to production.

Example: Bitbucket Pipeline to zip the source code and upload to S3 bucket.

Following are the steps for creating a pipeline in Bitbucket. In this example, the pipeline will zip the source code and upload the zip file to AWS S3 bucket.

Note: For creating the code pipeline in Bitbucket, you must be either the owner of the repo or you have the necessary permissions on the repo.
  • Open the repository you want to create a pipeline
  • Click on Settings
  • Under Pipelines Section -> Go to Settings


  • Click on Enable Pipelines

  • Go to Repository variables under Pipelines and enter the following
    • Name: AWS_SECRET_ACCESS_KEY    Value: <Your AWS Secret access key>
    • Name: AWS_ACCESS_KEY_ID               Value: <Your AWS Access key>
    • Name: AWS_DEFAULT_REGION            Value: <Your AWS Region>
  • Come back to Pipelines -> Settings and click on “Configure bitbucket-pipelines.yml” button 
  • Scroll down and Select “Other” in the dropdown

  • Paste the following in the code in the editor

  • Replace the S3 bucket path and click on commit button and watch your pipeline build.
Now every time you push your changes to your repository, BitBucket Pipelines will automatically zip the total source code and upload to Amazon S3 bucket.

A Big Note:-  there are a limited number of free build minutes per month depending on the type of account you have.To check your remaining build minutes, Go to Bitbucket settings -> Select Plan details under PLANS AND BILLING

Happy Coding ! 😊

AWS Aurora Data API Helper Class - C#

As per Amazon documentation, Aurora is a MySQL and PostgreSQL-compatible relational database built for the cloud, that combines the performance and availability of traditional enterprise databases with the simplicity and cost-effectiveness of open source databases.Amazon Aurora is up to five times faster than standard MySQL databases and three times faster than standard PostgreSQL databases. It provides the security, availability, and reliability of commercial databases at 1/10th the cost. Amazon Aurora is fully managed by Amazon Relational Database Service (RDS), which automates time-consuming administration tasks like hardware provisioning, database setup, patching, and backups.

Benefits of Amazon Aurora

  1. High Performance and Scalability
  2. High Availability and Durability
  3. Highly Secure
  4. MySQL and PostgreSQL Compatible
  5. Fully Managed
  6. Migration Support

Connect to Aurora from Client application

We have two options to connect to Aurora database from our application
1) Using connection string with MySql / PostgreSQL libraries
In this we will use a traditional connection string method to connect to the Aurora MySQL / PostgreSQL. In this method, the client application should be in same VPC network where Aurora database exists.
2) Using Aurora data API
Here we will use API to run the SQL statements. In this method, there is no necessary to have the client application in same VPC as Aurora. 

As all are aware of conventional database calling, I am discussing about connecting to the database using Data API in this post

Prerequisites for Working with Data API in Aurora

1) Enable Data API option:
        For using the Aurora data API, we should enable Data API for the Aurora cluster. We can do in through RDS Console. While creating, this option will be available under Connectivity section. While modifying, you can find this option under Network & Security section.
2) Store DB cluster credentials in a secret:
Use AWS Secrets Manager to create a secret that contains credentials for the Aurora DB cluster. For instructions, check Creating a Basic Secret

Helper class

Following is my helper class in .NET / .NET Core using C#. You need to include the AWSSDK.RDSDataService Nuget package for working with it.
using Amazon;
using Amazon.RDSDataService;
using Amazon.RDSDataService.Model;
using Amazon.Runtime;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;

namespace AurorDotnet
{
    public class AuroraHelper
    {
        AmazonRDSDataServiceClient client;
        private readonly string accessKeyId, secretKey, secretArn, auroraServerArn, databaseName;
        private readonly RegionEndpoint region;
        public AuroraHelper(string accessKeyId, string secretKey, string region, string secretArn, string auroraServerArn, string database)
        {
            this.accessKeyId = accessKeyId;
            this.secretKey = secretKey;
            this.region = RegionEndpoint.GetBySystemName(region);
            this.secretArn = secretArn;
            this.auroraServerArn = auroraServerArn;
            this.databaseName = database;
            client = GetClient();
        }

        private AmazonRDSDataServiceClient GetClient()
        {
            if (client == null)
            {
                try
                {
                    client = new AmazonRDSDataServiceClient(accessKeyId, secretKey, region);
                }
                catch (AmazonRDSDataServiceException ex)
                { Console.WriteLine($"Error (AmazonRDSDataServiceException) creating RDS client", ex); }
                catch (AmazonServiceException ex)
                { Console.WriteLine($"Error (AmazonServiceException) creating RDS client", ex); }
                catch (Exception ex)
                { Console.WriteLine($"Error creating AWS S3 client", ex); }
            }
            return client;
        }

        /// <summary>
        /// Runs the command and returns the first record as given object type. If T is string, it return the first row first column data.
        /// </summary>
        /// <typeparam name="T">Object type to which the record to be converted</typeparam>
        /// <param name="sqlCommand">Database command which needs to be executed</param>
        /// <param name="parameters">Prameters for the sql statement</param>
        /// <returns></returns>
        public async Task<T> GetRow<T>(string sqlCommand, Dictionary<string, string> parameters = null)
        {
            var executeSqlRequest = CreateExecuteRequest(sqlCommand, parameters);

            var data = await client.ExecuteStatementAsync(executeSqlRequest);
            if (data.HttpStatusCode == System.Net.HttpStatusCode.OK && data.Records.Count > 0)
            {
                if (typeof(T) == typeof(string))
                    return (T)ParseValue(data.Records[0][0], "varchar");
                else return ParseRecord<T>(data);
            }

            return default(T);
        }

        /// <summary>
        /// Runs the command and returns the list of records of given object type
        /// </summary>
        /// <typeparam name="T">Object type to which the record to be converted</typeparam>
        /// <param name="sqlCommand">Database command which needs to be executed</param>
        /// <param name="parameters">Prameters for the sql statement</param>
        /// <returns></returns>
        public async Task<List<T>> GetRows<T>(string sqlCommand, Dictionary<string, string> parameters = null)
        {
            var executeSqlRequest = CreateExecuteRequest(sqlCommand, parameters);

            var data = await client.ExecuteStatementAsync(executeSqlRequest);
            if (data.HttpStatusCode == System.Net.HttpStatusCode.OK && data.Records.Count > 0)
                return ParseRecordSet<T>(data);

            return default(List<T>);
        }

        /// <summary>
        /// Executes the Sql command and returns number of records effected
        /// </summary>
        /// <param name="sqlCommand"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public async Task<long> ExecuteSql(string sqlCommand, Dictionary<string, string> parameters = null)
        {
            var executeSqlRequest = CreateExecuteRequest(sqlCommand, parameters);

            var data = await client.ExecuteStatementAsync(executeSqlRequest);
            if (data.HttpStatusCode == System.Net.HttpStatusCode.OK)
            {
                if (data.Records.Count > 0)
                    return Convert.ToInt64(data.Records[0][0]);
                else
                    return data.NumberOfRecordsUpdated;
            }

            return 0;
        }

        /// <summary>
        /// Creates the ExecuteStatementRequest for Aurora data api
        /// </summary>
        /// <param name="sqlCommand">Database command which needs to be executed</param>
        /// <param name="parameters">Prameters for the sql statement</param>
        /// <returns></returns>
        private ExecuteStatementRequest CreateExecuteRequest(string sqlCommand, Dictionary<string, string> parameters = null)
        {
            ExecuteStatementRequest executeStatementRequest = new ExecuteStatementRequest()
            {
                SecretArn = secretArn,
                ResourceArn = auroraServerArn,
                IncludeResultMetadata = true,
                ContinueAfterTimeout = true,
                Database = databaseName,
                Sql = sqlCommand
            };
            if (parameters != null && parameters.Count > 0)
                executeStatementRequest.Parameters.AddRange(BuildParams(parameters));
            return executeStatementRequest;
        }

        private List<SqlParameter> BuildParams(Dictionary<string, string> parameters)
        {
            var sqlParameters = parameters.Select(p => new SqlParameter { Name = p.Key, Value = new Field() { StringValue = p.Value, IsNull = string.IsNullOrEmpty(p.Value) } }).ToList();
            return sqlParameters;
        }

        /// <summary>
        /// Converts the Data api response to the given object type and returns the first record
        /// </summary>
        /// <typeparam name="T">Object type to which the record to be converted</typeparam>
        /// <param name="response">Data api response</param>
        /// <returns></returns>
        private T ParseRecord<T>(ExecuteStatementResponse response)
        {
            var records = response.Records[0].Zip(response.ColumnMetadata, (record, col) => new KeyValuePair<string, object>(col.Name, ParseValue(record, col.TypeName))).ToList();
            return ConvertRecordToEntity<T>(records);
        }

        /// <summary>
        /// Converts the Data api response to the given object type and returns the list of records
        /// </summary>
        /// <typeparam name="T">Object type to which the record to be converted</typeparam>
        /// <param name="response">Data api response</param>
        /// <returns></returns>
        private List<T> ParseRecordSet<T>(ExecuteStatementResponse response)
        {
            List<T> data = new List<T>();
            var records = response.Records.Select(x => x.Zip(response.ColumnMetadata, (record, col) => new KeyValuePair<string, object>(col.Name, ParseValue(record, col.TypeName))).ToList()).ToList();
            foreach (var r in records)
            {
                T item = ConvertRecordToEntity<T>(r);
                data.Add(item);
            }
            return data;
        }

        /// <summary>
        /// Maps the KeyValuePair list to object
        /// </summary>
        /// <typeparam name="T">Object type to which the record to be converted</typeparam>
        /// <param name="data">KeyValuePair list which needs to map to the object</param>
        /// <returns></returns>
        private T ConvertRecordToEntity<T>(List<KeyValuePair<string, object>> data)
        {
            Type temp = typeof(T);
            T obj = Activator.CreateInstance<T>();
            PropertyInfo[] propInfo = temp.GetProperties();

            foreach (var item in data)
            {
                foreach (var pro in propInfo)
                {
                    if (pro.Name.ToLower() == item.Key.ToLower())
                    {
                        pro.SetValue(obj, item.Value, null);
                    }
                }
            }
            return obj;
        }

        /// <summary>
        /// Parse the value from Amazon.RDSDataService.Model.Field
        /// </summary>
        /// <param name="field">Amazon.RDSDataService.Model.Field</param>
        /// <returns></returns>
        private object ParseValue(Field field, string colType)
        {
            object value = null;
            if (field.IsNull)
                value = null;
            if (field.ArrayValue != null)
                value = field.ArrayValue;

            switch (colType.ToLower())
            {
                case "int":
                case "smallint":
                    value = Convert.ToInt32(field.LongValue);
                    break;
                case "bigint":
                    value = field.LongValue;
                    break;
                case "tinyint":
                case "boolean":
                case "bit":
                    value = field.BooleanValue;
                    break;
                case "float":
                case "double":
                case "real":
                    value = field.DoubleValue;
                    break;
                case "blob":
                case "binary":
                case "varbinary":
                    value = field.BlobValue;
                    break;
                case "time":
                    if (TimeSpan.TryParse(field.StringValue, out TimeSpan time))
                        value = time;
                    break;
                case "date":
                case "datetime":
                    if (DateTime.TryParse(field.StringValue, out DateTime date))
                        value = date;
                    break;
                default:
                    value = field.StringValue;
                    break;
            }
            return value;
        }

    }
}

Usage:

Create the instance of the helper class as below

AuroraHelper auroraHelper = new AuroraHelper("<AWS Accesskey Id>", "<AWS Secret Key>", "<AWS region>", "<Aurora database secret store Arn>", "<Aurora database cluster Arn>", "<Aurora database name>");

Getting single record from the database

Dictionary<string, string> parameters = new Dictionary<string, string>()
{
 {"accountid","10" }
};
var command = "SELECT account.id,account.name,account.isactive FROM account WHERE account.id=:accountid LIMIT 1";
var data = await auroraHelper.GetRow<Account>(command, parameters);
Console.WriteLine(JsonConvert.SerializeObject(data));

Getting set of record from the database

Dictionary<string, string> parameters = new Dictionary<string, string>()
{
 {"accountname","%test%" }
};
var command = "SELECT account.id,account.accountname,account.isactive FROM account WHERE account.accountname LIKE :accountname";
var data = await auroraHelper.GetRows<Account>(command, parameters);
Console.WriteLine(JsonConvert.SerializeObject(data));

Executing an DDL Command

Dictionary<string, string> parameters = new Dictionary<string, string>()
{
 {"accountname","Gopiportal" },
 {"isactive","1" }
};
var command = "INSERT INTO account(accountname,isactive) VALUES(:accountname,:isactive)";
var data = await auroraHelper.ExecuteSql(command, parameters);
if(data>0)
 Console.WriteLine("Data inserted successfully");
else
 Console.WriteLine("Data insertion failed");

Executing a Stored procedure

Aurora Data API is not built for executing a stored procedure. But we can execute a stored procedure as an SQL command like below

Stored procedure which returns single record

Dictionary<string, string> parameters = new Dictionary<string, string>()
{
 {"accountid","10" }
};
var command = "CALL usp_getAccount(:accountid)";
var data = await auroraHelper.GetRow<Account>(command, parameters);

Stored procedure which returns multiple records

Dictionary<string, string> parameters = new Dictionary<string, string>()
{
 {"accountname","%test%" }
};
var command = "CALL usp_getAccounts(:accountname)";
var data = await auroraHelper.GetRows<Account>(command, parameters);

Stored procedure which deals with DDL command

Dictionary<string, string> parameters = new Dictionary<string, string>()
{
 {"accountname","Gopiportal" },
 {"isactive","1" }
};
var command = "CALL usp_insertAccount(:accountname,:isactive)";
var data = await auroraHelper.ExecuteSql<Account>(command, parameters);

Limitations of Executing Stored procedure using Data API

Following are the limitations using stored procedures with Data API
  1. It will not capture any output variables returned from stored procedure
  2. If the stored procedure returns more than one data set, it will hold the records for the first data set and ignore the remaining
  3. After executing an DDL command in the stored procedure, it will not return the NumberOfRecordsUpdated count. This value will always be zero. 
Read more about Aurora Data API here.

Happy Coding ! 😊

AWS Custom / Lambda Authorizer

A Custom authorizer (also known as a Lambda authorizer) is an AWS API Gateway feature that uses a Lambda function to control access to your API. With Custom authorizer we can implement a custom authorization scheme that uses a bearer token authentication strategy such as OAuth or SAML, or that uses request parameters to determine the caller's identity.

When a client makes a request to one of our API's methods, API Gateway calls our Lambda authorizer, which takes the caller's identity as input and returns an IAM policy as output.


Types of Lambda Authorizer:

There are two types of Lambda autorizers
  1.  Token-based: Token-based authorizer receives the caller's identity in bearer token such as JWT token or oAuth token2
  2. Request-based: Request-parameter-based authorizer receives the caller's identity in combination of headers, query string params, stage variables and $context variables.


Lambda Authorization workflow for API gateway

  1. Client calls the API Gateway method with bearer token
  2. API gateway checks the method configured and Lambda authorizers. If yes, gateway calls the lambda function
  3. Lambda function authenticates the caller
  4. If the call succeeds, the Lambda function grants access by returning an output object containing at least an IAM policy and a principal identifier.
  5. API Gateway evaluates the policy.
    1. If access is denied, API Gateway returns a suitable HTTP status code, such as 403 ACCESS_DENIED.
    2. If access is allowed, API Gateway executes the method. If caching is enabled in the authorizer settings, API Gateway also caches the policy so that the Lambda authorizer function doesn't need to be invoked again. 


Steps to Create Lambda Authorizer

1) Create a lambda authorizer function


You need to first create the lambda function in AWS Console or in your local. In this tutorial I am used a .net core lambda function created in Visual studio. 
Note: You should have AWS Toolkit for visual studio to be installed to create AWS lambda in visual studio.
For creating a lambda function in Visual Studio
  1. Click on File -> New -> Project
  2. Choose "AWS Lambda" under "Visual C#" section and select "AWS Lambda Project(.Net core - C#)" template
  3. Select Empty Lambda template. It will create a skeleton as below
    namespace AWSLambda1
    {
     public class Function
     {
      public string FunctionHandler(string input, ILambdaContext context)
      {
       return input?.ToUpper();
      }
     }
    }
    
  4. Add Amazon.Lambda.APIGatewayEvents nuget package to the project
  5. Following is my code for a request based authorizer. It will read the accountid provider in the method url path and x-api-key from headers and validate those two. So modify the above function as below
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Amazon.Lambda.APIGatewayEvents;
    using Amazon.Lambda.Core;
    
    // Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
    [assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]
    
    namespace AWSLambda1
    {
     public class Function
     {
      
      public async Task FunctionHandler(APIGatewayCustomAuthorizerRequest request, ILambdaContext context)
      {
       var authorized = false;
       int accountId = -1;
       string authToken = string.Empty;
       try
       {
        // Parse the accountId for the parameter values
        if (!string.IsNullOrEmpty(request.PathParameters["accountId"]))
         accountId = int.Parse(request.PathParameters["accountId"]);
    
        // Parse the api key from headers
        if (!string.IsNullOrEmpty(request.Headers["x-api-key"]))
         authToken = request.Headers["x-api-key"];
    
        if (accountId != -1 && !string.IsNullOrWhiteSpace(authToken))
        {
         authorized = (accountId == 1 && authToken == "xxxx-xxxx-xxxx-xxxx") ? true : false;
        }
       }
       catch (Exception ex)
       {
        context.Logger.Log(ex.Message);
       }
       return CreateResponse(request.MethodArn, authorized);
      }
    
      /// 
      /// Create policy and returns the Authorizer response
      /// 
      /// Method Arn
      /// If it is true, it will retun Allow policy otherwise return Deny policy
      /// 
      private static APIGatewayCustomAuthorizerResponse CreateResponse(string methodArn, bool authorized)
      {
       APIGatewayCustomAuthorizerPolicy policy = new APIGatewayCustomAuthorizerPolicy
       {
        Version = "2012-10-17",
        Statement = new List()
       };
    
       policy.Statement.Add(new APIGatewayCustomAuthorizerPolicy.IAMPolicyStatement
       {
        Action = new HashSet(new string[] { "execute-api:Invoke" }),
        Effect = authorized ? "Allow" : "Deny",
        Resource = new HashSet(new string[] { methodArn })
    
       });
    
       APIGatewayCustomAuthorizerContextOutput contextOutput = new APIGatewayCustomAuthorizerContextOutput
       {
        ["User"] = "User",
        ["Path"] = methodArn
       };
    
       return new APIGatewayCustomAuthorizerResponse
       {
        PrincipalID = "User",
        Context = contextOutput,
        PolicyDocument = policy
       };
      }
     }
    }
    
  6. Create a package file with the following command in command prompt in the project folder
    dotnet-lambda package -c Release -o LambdaAuthorizer.zip -f netcoreapp2.1
    
  7. Login to AWS Console
  8. Create a .NET Core Lambda function by uploading the above package and save it.

2) Configure the Lambda function as an API Gateway authorizer

We need to configure the lambda function we created in step 1 in API gateway to use it as a authorizer. 
  1. In AWS Console, go to API gateway and choose your api from the list
  2. Goto Authorizers and Click "Create New Authorizer"
  3. Enter a name for the authorizer.
  4. Choose Type as Lambda.
  5. For Lambda Function, choose the region where you created your Lambda authorizer function and choose the function name from the dropdown list.
  6. Leave Lambda Invoke Role blank.
  7. For Lambda Event Payload, choose Request.
  8. Under Identity Sources, add a Header named "x-api-key"
  9. Choose Authorization Cache if required. 
  10. Click on "Create"
This will create new authorizer in your API Gateway. Now navigate to a api method for which you want to add authorization in the api gateway, Open "Method Request" -> Settings -> Select Your authorizer in Authorization dropdown

3) Test your API

Now test your api from Postman by passing the token in "x-api-keytest" header to the method

Happy Coding 😊!

Reference: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html#api-gateway-lambda-authorizer-lambda-function-create

AWS API Gateway: Use API Keys for authentication

API Gateway supports multiple mechanisms for controlling and managing access to your API. You can find those hereUsage Plans is one of the mechanism which allows you to provide API keys to your customers — and then track and limit usage of your API stages and methods for each API key

In this post, you will learn how to use API keys for authenticating the methods in the API gateway.

To set up API keys for API gateway we need to do the following steps

1) Create an API gateway with desired API methods

I am not going to explain this in this tutorial as it is out of the scope. You can learn how to create methods in API gateway from here 

2) Deploy the API to a stage

  • In the API Gateway main navigation pane, choose "Resources".
  • From the "Actions" drop-down menu, choose "Deploy API".
  • In the "Deploy API", select an existing "Deployment Stage" or create new Stage













  • Click on "Deploy"
  • It will deploy your api and give the api url

3) Create an API key(s) for the API

  • In the API Gateway main navigation pane, choose "API Keys".
  • From the "Actions" drop-down menu, choose "Create API key".





  • In "Create API key", 
    • Type an API key name in the Name input field.
    • Choose Auto Generate to have API Gateway generate the key value, or choose Custom to enter the key manually.
    • Enter description if you want 
    • Choose Save
    • Repeat this  step if you want to add more keys

  • It will create new key and will show as following. You can view the key by clicking on show button

4) Create a new usage plan

 Create a new usage plan. Add the deployed API stage to the usage plan. Attach an API key to the usage plan or choose an existing API key in the plan. Note the chosen API key value.

  • In the API Gateway main navigation pane, choose "Usage Plans".
  • Click on "Create" button
  • In "Create Usage Plan",
    • Type name of the usage plan
    • Enable Throttling, Quota if required (I disabled those in my case)
    • Click on Next 

    • In the next screen, click on "Add API Stage" 

    • Select your API -> Stage -> Click on checkmark
    •  Click on Next
    • In the next screen, "Create API Key and add to Usage Plan" if you want to add new api key or click on "Add API Key to Usage Plan" to add existing API key. Here I am selecting existing one

    •  Enter the name of the api key (created in Step 3) and click on checkmark. Repeat this if you want to attach another key to this Usage Plan
    • Click on "Done"

5) Configure API methods to require an API key

  • In the API Gateway main navigation pane, choose "Resources".
  • Under "Resources", choose an existing method.
  • Choose "Method Request".
  • Under the "Authorization Settings" section, choose true for "API Key Required".
  • Save the settings by clicking on checkmark icon.










6) Redeploy the API 

Redeploy the API to the same stage (Check Step 2 for deployment process)

The client can now call the API methods while supplying the x-api-key header with the chosen API key as the header value.

Happy Coding 😊!

AWS CloudFormation script for Lambda and SNS integration

AWS CloudFormation is a service that helps us to set up your Amazon Web Services. We just need to create a template that describes all the AWS resources that we want (like SNS, Lambda), and AWS CloudFormation takes care of provisioning and configuring those resources for us. We don't need to individually create and configure AWS resources and figure out what's dependent on what.

This post decribes how to write cloud formation script for creating a Lambda function, SNS topic and integrating those two to trigger the Lambda function from SNS.

We can write cloud formation scripts in Visual studio directly if we installed AWS Toolkit for Visual studio.
Once you installed the AWS toolkit, open Visualstudio -> Click on Solution -> Add -> New Project -> AWS -> Select AWS Cloud Formation Project.



Select "Create with Empty Template" in the next screen. It will create a project with empty template. The template will look like as follows
{
 "AWSTemplateFormatVersion" : "2010-09-09",

 "Description" : "",

 "Parameters" : {
 },

 "Resources" : {
 },

 "Outputs" : {
 }
}
It contains four sections
  1. Description: Decription about the cloud formation template
  2. Parameters: Any dynamic values requires while creating the resources like environment name, existing resources ars etc.
  3. Resources: AWS resourcs which needs to be created
  4. Outputs: Output values like the resource arn etc.

Now the first step for our cloud formation template is we need to create a Exectuiont role for Lambda. 

Lambda Execution Role

Before we can build a Lambda Function, we need to create some permissions for it to assume at runtime. The below role is a minimal role suitable for a basic Lambda Function with no external integration points. Additional permissions (e.g. reading from an S3 Bucket) can be added to the list of Statements in the PolicyDocument. 
"LambdaExecutionRole" : {
 "Type" : "AWS::IAM::Role",
 "Properties" : {
  "AssumeRolePolicyDocument" : {
   "Version" : "2012-10-17",
   "Statement" : [
    {
     "Action" : [
      "sts:AssumeRole"
     ],
     "Effect" : "Allow",
     "Principal" : {
      "Service" : [
       "lambda.amazonaws.com"
      ]
     }
    }
   ]
  },
  "Policies"                 : [
   {
    "PolicyName" : "lambda-test-policy",
    "PolicyDocument" : {
     "Version" : "2012-10-17",
     "Statement" : [
      {
       "Effect" : "Allow",
       "Action" : [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:GetLogEvents",
        "logs:PutLogEvents"
       ],
       "Resource" : "*"
      }
     ]
    }
   }
  ]
 }
}

Lambda Function

Following is the template for creating a lambda function. My lambda function will get the code from S3 bucket.
"Lambda"              : {
 "Type" : "AWS::Lambda::Function",
 "Properties" : {
  "FunctionName" : {
   "Fn::Sub" : "lbd-example-${Environment}"
  },
  "Description"  : "Lambda function triggered by SNS",
  "Handler"      : "Lambda.Test::Lambda.Test.Function::FunctionHandler",
  "Role"         : {
   "Fn::GetAtt" : [
    "LambdaExecutionRole",
    "Arn"
   ]
  },
  "Runtime"      : "dotnetcore2.1",
  "MemorySize"   : 256,
  "Timeout"      : 600,
  "Tags"         : [
   {
    "Key" : {
     "Fn::Sub" : "${Environment}"
    },
    "Value" : ""
   }
  ],
  "Code"         : {
   "S3Bucket" : "my-bucket",
   "S3Key"    : "deployments/dev/LambdaTest.zip"
  }
 },
 "DependsOn"  : [
  "LambdaExecutionRole"
 ]
}
Note: ${Environment} is reference to my parameter

SNS Topic

Following is the template for creating a SNS Topic. SNS Topic is very simple, just the topic name and it subscriptions.
"SNSTopic" : {
 "Type" : "AWS::SNS::Topic",
 "Properties" : {
  "DisplayName" : {
   "Fn::Sub" : "test-sns-${Environment}"
  },
  "TopicName"   : {
   "Fn::Sub" : "test-sns-${Environment}"
  },
  "Subscription" : [
   {
    "Endpoint" : {
     "Fn::GetAtt" : [
      "Lambda",
      "Arn"
     ]
    },
    "Protocol" : "lambda"
   }
  ]
 },
 "DependsOn"  : "Lambda"
}

Lambda invoke Permission for the Topic

The important thing here is to give Lambda invoke permission to SNS.In cloud formation, creating Lambda, SNS topic resource will not work. We need to grant permission to SNS topic to invoke Lambda function directly. Following is my template for lambda permission.
"LambdaInvokePermission" : {
 "Type" : "AWS::Lambda::Permission",
 "Properties" : {
  "Action" : "lambda:InvokeFunction",
  "Principal" : "sns.amazonaws.com",
  "SourceArn" : {
   "Ref" : "SNSTopic"
  },
  "FunctionName" : {
   "Fn::GetAtt" : [
    "Lambda",
    "Arn"
   ]
  }
 }
}
Note: Here "SourceArn" property refer to the SNS Topic we created. Visual studio shows "SNSTopic is invalid type for this reference" for this property. But no worries. It is in the correct format and will deploy. 

Following is my complete cloud formation template
{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description"              : "Test Lambda SNS integration",
    "Parameters"               : {
        "Environment" : {
            "Type" : "String",
            "Default" : "dev",
            "Description" : "Environment name for which the components are deploying",
            "AllowedPattern" : "^[a-z]*$"
        }
    },
    "Resources"                : {
        "LambdaExecutionRole" : {
            "Type" : "AWS::IAM::Role",
            "Properties" : {
                "AssumeRolePolicyDocument" : {
                    "Version" : "2012-10-17",
                    "Statement" : [
                        {
                            "Action" : [
                                "sts:AssumeRole"
                            ],
                            "Effect" : "Allow",
                            "Principal" : {
                                "Service" : [
                                    "lambda.amazonaws.com"
                                ]
                            }
                        }
                    ]
                },
                "Policies"                 : [
                    {
                        "PolicyName" : "lambda-test-policy",
                        "PolicyDocument" : {
                            "Version" : "2012-10-17",
                            "Statement" : [
                                {
                                    "Effect" : "Allow",
                                    "Action" : [
                                        "logs:CreateLogGroup",
                                        "logs:CreateLogStream",
                                        "logs:GetLogEvents",
                                        "logs:PutLogEvents"
                                    ],
                                    "Resource" : "*"
                                }
                            ]
                        }
                    }
                ]
            }
        },
        "Lambda"              : {
            "Type" : "AWS::Lambda::Function",
            "Properties" : {
                "FunctionName" : {
                    "Fn::Sub" : "lbd-example-${Environment}"
                },
                "Description"  : "Lambda function triggered by SNS",
                "Handler"      : "Lambda.Test::Lambda.Test.Function::FunctionHandler",
                "Role"         : {
                    "Fn::GetAtt" : [
                        "LambdaExecutionRole",
                        "Arn"
                    ]
                },
                "Runtime"      : "dotnetcore2.1",
                "MemorySize"   : 256,
                "Timeout"      : 600,
                "Tags"         : [
                    {
                        "Key" : {
                            "Fn::Sub" : "${Environment}"
                        },
                        "Value" : ""
                    }
                ],
                "Code"         : {
                    "S3Bucket" : "my-bucket",
                    "S3Key"    : "deployments/dev/LambdaTest.zip"
                }
            },
            "DependsOn"  : [
                "LambdaExecutionRole"
            ]
        },
        "SNSTopic"            : {
            "Type" : "AWS::SNS::Topic",
            "Properties" : {
                "DisplayName" : {
                    "Fn::Sub" : "test-sns-${Environment}"
                },
                "TopicName"   : {
                    "Fn::Sub" : "test-sns-${Environment}"
                },
                "Subscription" : [
                    {
                        "Endpoint" : {
                            "Fn::GetAtt" : [
                                "Lambda",
                                "Arn"
                            ]
                        },
                        "Protocol" : "lambda"
                    }
                ]
            },
            "DependsOn"  : "Lambda"
        },
        "LambdaInvokePermission" : {
            "Type" : "AWS::Lambda::Permission",
            "Properties" : {
                "Action" : "lambda:InvokeFunction",
                "Principal" : "sns.amazonaws.com",
                "SourceArn" : {
                    "Ref" : "SNSTopic"
                },
                "FunctionName" : {
                    "Fn::GetAtt" : [
                        "Lambda",
                        "Arn"
                    ]
                }
            }
        }
    },
    "Outputs"                  : {
        "Topic" : {
            "Description" : "Test Topic",
            "Value"       : {
                "Ref" : "SNSTopic"
            }
        },
        "Lambda" : {
            "Description" : "Test Lambda",
            "Value"       : {
                "Fn::GetAtt" : [
                    "Lambda",
                    "Arn"
                ]
            }
        }
    }
}
Happy Coding 😊!

AWS: Simple Storage Service (S3) Helper Class - TransferUtility - C#

In my previous post, we learnt about Amazon S3 and upload / download / copy / delete the files(or objects) of an S3 bucket using C#. The methods given in the previous post uses a Single GET / PUT operations, so that it will upload / download upto 5 GB (Refer here) and we need to use Multipart Upload API. So for implementing the Multipart Upload API concept .NET uses TransferUtility class.

TransferUtility

TransferUtility is a high level utility for managing transfers to and from Amazon S3. It provides a simple API for uploading  and downloading content to/from Amazon S3. It uses Amazon S3 multipart upload API, so you can upload large objects, up to 5 TB. 

It uses multiple threads to upload multiple parts of a single file at once which increase throughput, when dealing with large content sizes and high bandwidth.

Configure the TransferUtility

There are three optional properties that you can configure:

ConcurrentServiceRequests 

Determines how many active threads or the number of concurrent asynchronous web requests will be used to upload/download the file. The default value is 10.

MinSizeBeforePartUpload

Gets or sets the minimum part size for upload parts in bytes. The default is 16 MB. Decreasing the minimum part size causes multipart uploads to be split into a larger number of smaller parts. Setting this value too low has a negative effect on transfer speeds, causing extra latency and network communication for each part.

NumberOfUploadThreads 

Gets or sets the number of executing threads. This property determines how many active threads will be used to upload the file. The default value is 10 threads.

Following is the C#.NET helper method for Upload / Download files using TransferUtility 
public class AmazonS3TransferHelper
{
 AmazonS3Client client;

 private static readonly ILog _logger = LogManager.GetLogger(typeof(AmazonS3TransferHelper));
 private readonly string accessKeyId, secretKey, serviceUrl;
 public AmazonS3TransferHelper(string accessKeyId, string secretKey, string serviceUrl)
 {
  this.accessKeyId = accessKeyId;
  this.secretKey = secretKey;
  this.serviceUrl = serviceUrl;
  client = GetClient();
 }

 /// <summary>
 /// Initializes and returns the AmazonS3 object
 /// </summary>
 /// <returns></returns>
 private AmazonS3Client GetClient()
 {
  if (client == null)
  {
   try
   {
    // S3 config object
    AmazonS3Config clientConfig = new AmazonS3Config
    {
     // Set the endpoint URL
     ServiceURL = serviceUrl
    };
    client = new AmazonS3Client(accessKeyId, secretKey, clientConfig);
   }
   catch (AmazonS3Exception ex)
   { _logger.Error($"Error (AmazonS3Exception) creating S3 client", ex); }
   catch (AmazonServiceException ex)
   { _logger.Error($"Error (AmazonServiceException) creating S3 client", ex); }
   catch (Exception ex)
   { _logger.Error($"Error creating AWS S3 client", ex); }
  }
  return client;
 }

 private TransferUtility GetTransferUtility()
 {
  var config = new TransferUtilityConfig()
  {
   ConcurrentServiceRequests = 10,
   MinSizeBeforePartUpload = 16 * 1024 * 1024
  };

  return new TransferUtility(GetClient(), config);
 }

 /// <summary>
 /// Uploads the file to the S3 bucket. 
 /// </summary>
 /// <param name="bucketNameWithPath">S3 bucket name along with the subfolders. Ex. If you are using a 'dev' folder under bucket 'myBucket', this value should be myBucket/dev</param>
 /// <param name="fileNameInS3">File name used to store the content in the bucket</param>
 /// <param name="fileContent">String content which needs to be stored in the file</param>
 /// <returns></returns>
 public async Task Upload(string bucketNameWithPath, string fileNameInS3, string fileContent)
 {
  _logger.Info("Entering AmazonS3TransferHelper.Upload");
  try
  {
   byte[] byteArray = Encoding.ASCII.GetBytes(fileContent);
   MemoryStream stream = new MemoryStream(byteArray);

   var tranferUtility = GetTransferUtility();
   var transferUploadRequest = new TransferUtilityUploadRequest
   {
    BucketName = bucketNameWithPath,
    Key = fileNameInS3,
    InputStream = stream
   };

   await tranferUtility.UploadAsync(transferUploadRequest); //commensing the transfer  
  }
  catch (Exception ex)
  {
   _logger.Error("Error in uploading file to s3 bucket", ex);
  }

  _logger.Info("Leaving AmazonS3TransferHelper.Upload");
 }

 /// <summary>
 /// Downloads the file from S3 bucket
 /// </summary>
 /// <param name="bucketNameWithPath">S3 bucket name along with the subfolders. Ex. If you are using a 'dev' folder under bucket 'myBucket', this value should be myBucket/dev</param>
 /// <param name="fileNameInS3">File which needs to be get from the bucket</param>
 /// <param name="localFilePath">Local folder path to store the file downloaded from S3</param>
 /// <returns></returns>
 public async Task Download(string bucketNameWithPath, string fileNameInS3, string localFilePath)
 {
  _logger.Info("Entering AmazonS3TransferHelper.Download");
  try
  {
   var tranferUtility = GetTransferUtility();
   var transferDownloadRequest = new TransferUtilityDownloadRequest
   {
    BucketName = bucketNameWithPath,
    Key = fileNameInS3,
    FilePath = localFilePath + "/" + fileNameInS3
   };

   await tranferUtility.DownloadAsync(transferDownloadRequest);
  }
  catch (Exception ex)
  {
   _logger.Error("Error in downloading file to s3 bucket", ex);
  }

  _logger.Info("Leaving AmazonS3TransferHelper.Download");
 }
}

Usage:

Create the helper class object
AmazonS3TransferHelper transferHelper = new AmazonS3TransferHelper(<accesskey>, <secret key>, <AWS S3 endpoint url>);
Upload:
Here, I am uploading S3.txt file to the dev folder in my S3 bucket named gopiBucket with name "S3Sample_1.txt".
string fileContent = File.ReadAllText("S3.txt");
await transferHelper.Upload("gopiBucket/dev", "S3Sample_1.txt", fileContent);
Download a file:
Downloading the file from dev folder to the "Downloads" folder in my solution. If the "Downloads" folder not exits, it will create new one.
await transferHelper.Download("gopiBucket/dev", "S3Sample_1.txt", "Downloads");
Happy Coding  😊!!