Sep 29, 2022 15 min read Miniature Jira App with AWS CDK in Python

Introduction

Jira is a popular application among Developers. Jira application helps teams plan, assign, track, report and manage work. Jira brings teams together for agile software development and customer support. Here we are going to create a miniature Jira app with basic functionalities using AWS CDK and python. We will be creating the application using multiple AWS services like,

  • AWS Lambda
  • DynamoDB
  • IAM role
  • API Gateway
  • AWS SES
  • AWS Cloud formation

We will learn how to create, manage, access multiple resources with different permissions using AWS CDK with python. This blog also explains how to select a database, why and when to choose different databases. Here we will be explaining how to design the backend of the application, and give a brief overview of the front end using a block diagram. Using cdk stack construct diagrams various steps involved in the application are explained.

You can add more features to the application, few suggestions are given in the blog itself. All the very best and happy learning.

Development

DATABASE SELECTION

Database selection is the most important part of Application Architecture. We need to choose the right database which suits our project the best.

  1. mysql : SQL has a complex interface that makes it difficult to access it. The programmers who use SQL don’t have full control over the database because of the hidden business rules. Its operating cost is a bit high.

  2. Postgres : Postgresql is a Relational Database Management System, it is an object oriented database. As our project does not have any relational queries we don’t choose postgres.

  3. SQlite : SQLite locks the complete database on access, making it a very good tool for development or applications with no concurrent users, but not a good one for applications that actually have multiple users at the same time. This makes it not a good tool for production web sites.

  4. NoSQL : NOSQL database provides much more flexibility when it comes to handling data. There is no requirement to specify the schema to start working with the application. Also, the NoSQL database doesn’t put a restriction on the types of data you can store together. It allows you to add more new types as your needs change. NoSQL is best suited for agile development which requires fast implementation.

No sql database suits best for our requirements. Dynamodb and Mongodb are the most popular nosql databases.

DynamoDB : Amazon DynamoDB is a fully managed, serverless, key-value NoSQL database designed to run high-performance applications at any scale. DynamoDB offers built-in security, continuous backups, automated multi-Region replication, in-memory caching, and data export tools. Setting up and installation process of DynamoDB is easy compared to other NoSQL databases like MongoDB.

We are going to make use of the property of dynamodb table to split the primary key into partition and sort key. A primary key is used to store, identify and capture data from the table. It is unique for each data. The multiple column-row table structure enables us to store large amounts of information under a single primary key.

DATA MODELLING


We need to create 5 dynamodb tables. Jira Tablesdrawio

Once we decide on our database we can start designing our project.

WORKFLOW


Untitled Diagram drawio

Here when a user signs up his/her credentials are added to the employees table. A login will prompt after every signup. Login will check the credentials of the user and return a user id, which will be retained throughout the process. A verified user can view the dashboard with information about projects and their status. Users can create a new project with a unique projectkey either keeping him/her as the project manager or assign someone . A project manager can add group members from the employee list and send email notification to the selected group members. After project creation, the user will be redirected to the Scrumpage where he/she can view and create stories and tasks, add comments, update status of each task. Only project managers can delete a task. Once the work is done the user can safely sign out from the application.

Building Project


Choosing the right tools to build a project is also crucial. Here we are going to use AWS CDK.

AWS CDK : Is a serverless IaC tool specific to Amazon cloud services alone. In AWS CDK it is easy to define multiple infrastructure resources like DynamoDB tables and multiple IAM roles. To invoke the logs of a serverless function deployed using AWS CDK, you need to use another tool like AWS SAM or AWS CLI.

AWS CDK uses CDK metadata which does not give you access to configure, thereby making it safer from possible threats that may occur through tampering with the configuration. It allows you to use popular programming languages like JavaScript, TypeScript, Python, Java, C#, and .NET to define the resources needed for your serverless application. AWS CDK is excellent for use in deploying small to medium serverless projects . Let’s start building our project.

prerequisites:


setting up your environment

After installing aws-cli, node.js, Open a terminal window and use aws configure to set up your environment. Type the access key ID and secret key and choose a default region

aws configure

Once you configure your environment, run the given command to install aws-cdk.

$ npm install -g aws-cdk

Now we can start creating our project.

cdk project


Open terminal

create a empty directory

$ mkdir minijira_app && cd minijira_app           

Using cdk init create a sample app in python language. This init command will create a virtual environment in your directory. After running the init command open vscode(or any IDE) and open our folder or type code. in the terminal. We can see a few files and folders already created for us.

$ cdk init sample-app --language python          

Activate virtual environment

linux/macos

  source.venv/bin/activate
       

windows

  .venv\Scripts\activate.bat
  

Once the virtual environment is activated install default dependencies specified in default requirement.txt in the sample app. Add boto3 == 1.24.3 to it and run the below command.

pip install -r requirements.txt

In the minijira_app folder we have a python module directory(minijira_app), within a minijira_app_stack.py. This stack.py file contains a custom cdk stack construct for use in our cdk application. A app.py file, this code loads and instantiates an instance of the cdkminijiraStack class from minijira_app/minijira_app_stack.py. Now we can edit these files into our requirements.

The most important Part of our application is in minijira_app_stack.py. This file creates all the resources for our application. This construct class also set the required permissions for our main lambda function. Create a lambda folder in your root directory (MINIJIRA_APP) beside your python directory(minijira_app). Inside lambda create a file called jira.py. This file contains our lambda handler function.

Go to minijira_app_stack.py, here we will specify all the resources, permissions, actions required by the application to work. (we can delete the sample stack,and create a new one)

Resource Creation

CDK_Construct drawio

minijira_app_stack.py

Define our MinijiraAppStack with inherent properties of Parent Class Stack.

class MinijiraAppStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)
       

Constructs are the basic building blocks of AWS CDK apps. A construct represents a “cloud component” and encapsulates everything AWS CloudFormation needs to create the component. AWS Construct library includes constructs that represent all the resources available on AWS.

First we will import necessary dependencies to create our stack.

from attr import Attribute
from constructs import Construct
from aws_cdk import (
    
    Stack,
    aws_lambda as _lambda,
    aws_apigateway as apigw,
    aws_dynamodb as dynamodb,
    aws_ses as _ses,aws_iam as iam,
    
)

Now we need to construct our resources.

Lambda Function

Defining lambda function is pretty straightforward. Give a name to our lambda function. Specify runtime, where to find our lambda code from created assets. Specify lambda handler function is defined in jira.py file. As you know lambda is an inbuilt function in python therefore we can’t use lambda as a variable name. Using _lambda instead.

 my_lambda = _lambda.Function( self, 'JiraHandler',
            runtime = _lambda.Runtime.PYTHON_3_8,
            code = _lambda.Code.from_asset('lambda'),
            handler = 'jira.handler',
            
)

Setting permissions to lambda function : We need our lambda function to send email to selected group members. We need verified employees email ids to send and recieve email. AWS SES verify email id for us and generate unique arn value for each email id. We need to add this verified list of ‘ARN’ values to the ses resources and give access permission to Lambda. Create a file in root directory env.py.

  • IAM role : Give lambda to access ses to send email using IAM policy and SES. The resources parameter in policy statement will take value in ’ resources = [‘arn:aws:ses: : :identity/ ’,‘arn:aws:ses: : :identity/ ‘,etc..,]’ format. For security reasons we add this list of verified arn values in a separate env.py file, and import that file to your stack construct.
 from .env.py import mail_list
 
 my_lambda.add_to_role_policy(iam.PolicyStatement(
            effect = iam.Effect.ALLOW,
            actions = [
          'ses:SendEmail',
          'ses:SendRawEmail',
          'ses:SendTemplatedEmail',
],
        
resources = mail_list
)
)
    
  • API Gateway : create a rest api and specify lambda handler for the api.
 apigw.LambdaRestApi(
            self, 'Endpoint',
            handler = my_lambda,
)
  • DynamoDB table: Generating individual stack for individual dynamodb table. Specifying attributes, granting lambda to read and write permission to each table.Please note each dynamodb table stack should have unique id.
employeetable = dynamodb.Table(self,
        id = 'dynamodbtable',
        table_name = "employee",
        partition_key = dynamodb.Attribute(
            name = 'email',
            type = dynamodb.AttributeType.STRING
)
)
        employeetable.grant_read_write_data(my_lambda)

Similarly create all tables. for complete code refer < github link >

Now we have defined all assets required for our application .We can write our handler code.

The api-gateway we created will give you an endpoint url which supports all methods like GET, POST, PUT, DELETE etc by default. For simplicity we are using the POST method only. The POST request will be in json format. In the request, let the event body contain an action parameter, which decides which function/action to call or perform.

The action parameters are,

  • signup
  • login
  • addproject
  • addstory
  • addtask
  • addcomment
  • employeelist
  • update
  • projectlist
  • delete
  • scrumpage

Instead of using action parameters we can also use multiple lambda functions for different api gateways or create different paths for different functions as well. Here we are using the action Parameter. Go to the lambda folder, open jira.py and we can add our handler function.

jira.py

First we need to import all the necessary python modules.

import json
import boto3
import string
import random
from boto3.dynamodb.conditions import Key

Boto3 is used to access all the services and resources we need for our application. Boto3 is the AWS SDK for Python to create, configure, and manage AWS services. String and Random modules are used to create our unique id for table entries. Key attribute is used for querying dynamodb tables.

Define handler function

def handler(event, context):
      
    event = json.loads(event['body'])
    action = event["action"]

Here according to action we can call different functions. In login action, after verifying credentials we retain email and username as primary_email and primary_user.

if action == "login": 
       email = event["email"]
       password = event["password"]
       response = table_employee.query(KeyConditionExpression=Key('email').eq(email))
       for item in response['Items']:
           if item.get('password') == password :
               primary_user = item.get('username')
               primary_email = email
               message = "successfully logged in"
           else:
               message = "try again wrong credentials" 
               primary_email = "invalid email"
               primary_user = 'Not a registred user'
  

Lets see how to create a project.

when we choose addproject action,event request body will look like this.

  1. { 
            "action" : "addproject",
            "projectkey" : "trialp1",      
            "projectname" : "trialproject1",
            "projectmanager" : "pmuser",
            "groupmembers": "["user1@mail.io","user2@mail.io"]"
     }

Here groupmembers parameter is a list of employee email ids .We need these parameters to send email to selected groupmembers.

if action == "addproject":
        table_projects.put_item(
            Item = {
                'projectkey' :event["projectkey"],      
                'projectname':event["projectname"],
                'projectmanager':event["projectmanager"],
                'groupmembers': event["groupmembers"]
                    }
                )

Once you add the details to the projects table we need to send email to group members. Here is the simplest form of email generation. Make sure you add verified email ids of group members which is in our resources.

        client = boto3.client('ses' ) 
        name = projectmanager
        projectname = projectname    
        source = pm_email   
        subject = 'projectupdate'
        body = f"""<html>
            <head></head>
            <body>
            <h2>'you have been added to project:{projectname}'</var></h2>
            <br/>
             
            </body>
            </html>
                        """
    
        destination = groupmembers
        _message = "Message from: " + name + "\nEmail: " + source + "\nMessage content: " + "added to new project"    
        
        email_message = client.send_email(
            Destination = {
                'ToAddresses': [destination]
                },
            Message = {
                'Body': {
                    'Html': {
                        'Charset': 'UTF-8',
                        'Data': body,
                    },
                },
                'Subject': {
                    'Charset': 'UTF-8',
                    'Data': subject,
                },
            },
            Source = source,
    )
        message = "project added"+_message
  
 

We can add a task to our project . POST request event body will look like this

1. {
        'projectkey' :event["projectkey"] ,      
        'taskid':taskid,
        'createdby':event["createdby"],
        'taskdescription':event["taskdescription"],
        'assignedto':event["assignedto"],
        'startdate':event["startdate"],
        'enddate':event["enddate"],
        'taskstatus':event["taskstatus"]
    }

add task to task table

   if action=="addtask":
      taskid = ''.join(random.choices(string.ascii_uppercase +
                             string.digits, k = 7))
                  
      table_tasks.put_item(
         Item = {
            'projectkey' :event["projectkey"] ,      
            'taskid':taskid,
            'createdby':event["createdby"],
            'taskdescription':event["taskdescription"],
            'assignedto':event["assignedto"],
            'startdate':event["startdate"],
                'enddate':event["enddate"],
                'taskstatus':event["taskstatus"]
        }
        ) 
        message="task added"

Lets see how to delete a task

    if action == "delete":
         projectkey = event["projectkey"]
         taskid = event["taskid"]
         email = event["email"]
         password = event["password"]
         
         response = table_p.query(KeyConditionExpression = Key('projectkey').eq(projectkey))
         for item in response['Items']:
            pm = item.get('projectmanager')
            
         response = table.query(KeyConditionExpression=Key('email').eq(email))
         for item in response['Items']:
            user = item.get('username')
            pw = item.get("password")
            
         if pm == user and password == pw:
              table_t.delete_item(Key = {'projectkey':projectkey, 'taskid': taskid}) 
              
              message = "item deleted by projectmanager" 
              
         else:
         
            message = "please check your credentials,only project managers can delete items from task table."
                

lets update task status

if action == "update":
        change = event["change"]
        projectkey = event["projectkey"]
        taskid = event["taskid"]

According to the value of Change parameter we can change date, status etc . We can use KEY attribute to update expression .

if change == "status":
                            
            table_t.update_item(
                    Key = {    'projectkey':projectkey,
                            'taskid': taskid,
                        },
                    UpdateExpression = "set taskstatus = :g",
                    ExpressionAttributeValues = {
                            ':g': event["taskstatus"]
                        },
                    ReturnValues = "UPDATED_NEW"
                )
            message = "task status changed" 

Similarly you can write up all action codes.

The final piece of code is the scrum page. The scrum page will display all the details of your project. Using the ‘KEY’ attribute we can query any table for any value.

   if action == "scrumpage":
       projectkey = event["projectkey"]
       
       #getting project details
       response = table_p.query(KeyConditionExpression = Key('projectkey').eq(projectkey))
       for item in response['Items']:
           project = item  
           
       #getting story details
       response = table_s.query(KeyConditionExpression = Key('projectkey').eq(projectkey))
       stories = []
       for item in response['Items']:
           stories.append(item) 
           
       #getting tasks details
       tasks = []
       response = table_t.query(KeyConditionExpression = Key('projectkey').eq(projectkey))
       for item in response['Items']:
           tasks.append(item) 
           
       #getting comments
       comments=[]
       response = table_c.query(KeyConditionExpression = Key('projectkey').eq(projectkey))
       for item in response['Items']:
           comments.append(item) 
           
       message = {"project":project,
                "stories":stories,
                "tasks":tasks,
                "comments":comments
              }
                 

Finally we can return our response

    response = {
        "statusCode": 200,
        'headers': {'Content-Type': 'application/json'},
        "body": json.dumps(message)
    }            

    return response

You can refer to https://github.com/Divyapoonthottam/Miniature-JIRA-App-With-AWSCDK-in-Python for complete code .

Once you have saved your updated files.Our project stack needs to be synthesised. AWS CDK apps are effectively only a definition of your infrastructure using code. When CDK apps are executed, they produce (or “synthesise”, in CDK parlance) an AWS CloudFormation template for each stack defined in your application.

To synthesise a CDK app , use the cdk synth command . This will output a CloudFormation template.

cdk synth

To deploy the cloudformation template into your account we need to bootstrap it first. We can use the cdk bootstrap command to install the bootstrap stack into an environment.

cdk bootstrap

On successful bootstrapping, go to your aws console and can see a cdk tool kit in cloudformation service and a new asset bucket in your s3 bucket.

now we can deploy our app

cdk deploy

This will prompt a confirmation,enter Y and you can see the stack created into your account and region as you configured. Go to the AWS console cloudformation service and you can see your mini jira stack. In AWSlambda you can see your lambda function created. In the AWS DynamoDB table, 5 tables are created.

The endpoint url will be displayed in your terminal upon successful deployment.

You can test your api using POSTMAN .


Reference


Author(s)

15 min read

Sep 29, 2022

This website stores cookies on your computer.

These cookies are used to collect information about how you interact with this website and allow us to remember you. We use this information in order to improve and customize your browsing experience and for analytics and metrics about our visitors on this website.

If you decline, your information won’t be tracked when you visit this website. A single cookie will be used in your browser to remember your preference not to be tracked.