Showing posts with label Websites. Show all posts
Showing posts with label Websites. Show all posts

Pythonizing our Web App!

Ok, you asked for it! You all wanted an update for the lambda functions to use another language and
luckily, my favourite one! Python!

So let's get started!

Update the Lambda function to fetch and store data

The first step is to update the Lambda function that fetches data from an external website and stores it in a serverless database. Here is an example of the same function written in Python:

import boto3
import requests
dynamo = boto3.client('dynamodb')
def lambda_handler(event, context):
    # Fetch data from external website
    response = requests.get('https://example.com/data')
    data = response.json()
    
    # Store data in DynamoDB table
    item = {
        'id': {'S': data['id']},
        'data': {'S': data['data']}
    }
    dynamo.put_item(TableName='your-table-name', Item=item)
    return {
        'statusCode': 200,
        'body': json.dumps({'message': 'Data stored in DynamoDB'})
    }

This example uses the boto3 package to interact with DynamoDB and the requests package to fetch data from an external website, similar to the previous example. Note that the function uses python json module to parse data instead of JSON.parse() function in node.js

Update the Lambda function to retrieve data

The next step is to update the Lambda function that retrieves data from the serverless database and makes it accessible through the API Gateway. Here is an example of the same function written in Python:


import boto3
import json
dynamo = boto3.client('dynamodb')
def lambda_handler(event, context):
    # Retrieve data from DynamoDB table
    data = dynamo.scan(TableName='your-table-name')
    # Return data to user
    return {
        'statusCode': 200,
        'body': json.dumps(data)
    }

This function uses the boto3 package to interact with DynamoDB and returns the data to the user as a JSON string using json.dumps()

Re-Creating a Lambda Authorizer

A Lambda authorizer is a Lambda function that you build to authorize access to your API Gateway APIs using bearer token authentication, such as OAuth or JWT.

Here's an example of a simple Lambda authorizer function that can be used to authenticate a user using a bearer token. This function will use the Okta library to validate the token and return the appropriate policy allowing or denying access to the API Gateway endpoint.

import json
from okta.framework import OktaAuth
def lambda_handler(event, context):
    auth = OktaAuth(event)
    claims = auth.verify_token()
    if claims:
        auth_response = auth.get_auth_response(claims)
    else:
        auth_response = auth.get_auth_response(None, 'Unauthorized')
    return auth_response

This example uses the okta library to validate the token passed in the Authorization header and return the appropriate policy allowing or denying access to the API Gateway endpoint.

Updating Terraform Configuration

For the terraform configuration to use the python function in the Lambda function resource the runtime attribute needs to be updated from nodejs14.x to python3.8. Additionally, in order to link the newly created Lambda authorizer function to the API Gateway, we need to update the authorization attribute of aws_api_gateway_method resource to CUSTOM and add authorizer_id to it pointing to our newly created Lambda function.

resource "aws_api_gateway_method" "example" {
  rest_api_id = aws_api_gateway_rest_api.api_name.id
  resource_id = aws_api_gateway_resource.example.id
  http_method = "GET"
  authorization = "CUSTOM"
  authorizer_id = aws_lambda_authorizer.authorizer_lambda.id

  request_parameters = {
    "method.request.header.Authorization": true
  }
  integration {
    type = "AWS_PROXY"
    uri = aws_lambda_function.data_lambda.invoke_arn
    http_method = "POST"
    request_templates = {
      "application/json" = "{\"statusCode\": 200}"
    }
  }
}

Even Re-Create a Hello World Lambda Function

To create a simple "Hello World" Lambda function, you can use the following code:

def lambda_handler(event, context):
    message = 'Hello World!'
    return {
        'statusCode': 200,
        'body': json.dumps(message)
    }

This function returns a JSON object containing the message "Hello World!"

Verify the Deployment

To verify that everything is set up correctly, you can test the API Gateway endpoint using a tool like Postman and check the logs of all Lambda functions and the DynamoDB table to ensure that the data is being fetched and stored correctly, accessible when the user is logged in, and that the authorizer is properly handling token validation.

Additionally, you can test the "Hello World" lambda function by invoking it from the AWS Lambda console or by making a request to it via an API Gateway endpoint.

Wrap Up!

There you have it! You wanted it you got it, but by updating these Lambda functions and the Terraform configuration to use Python instead of Node.js, you can continue to use the same functionality as before but with a different programming language.

Despite updating the dependencies and language, the function code and dependencies together, remains ready for deployment with Terraform to create the same product demonstrating how modular we can make these websites!

I got ID! Adding Authorisation to our App!

Why not show off a bit of my fondness towards Pearl Jam?

In our previous chapters of this series we have:

That's right! Today we will add an additional layer of security to our Angular application by integrating an AWS Lambda Authorizer as an authentication and authorization method, using either AWS Cognito or Okta as a third-party provider.

Create a Lambda Authorizer function

The first step in creating a Lambda Authorizer is to create the Lambda function itself. The function will handle the authentication and authorization process by validating the incoming token and determining if the user is authorized to access the resources.

Here's an example of a simple Lambda Authorizer function written in Node.js that uses AWS Cognito as the third-party provider:

const AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider();
exports.handler = async (event) => {
    // Extract the token from the request
    const token = event.authorizationToken;
    
    // Verify the token using the Cognito provider
    const params = {
        AccessToken: token
    };
    const data = await cognito.getUser(params).promise();
    
    // Determine if the user is authorized
    const user = data.UserAttributes;
    const isAuthorized = user.some(attr => attr.Name === 'custom:groups' && attr.Value === 'admin');
    
    // Return the policy statement
    if (isAuthorized) {
        return {
            principalId: user.find(attr => attr.Name === 'sub').Value,
            policyDocument: {
                Version: '2012-10-17',
                Statement: [
                    {
                        Action: 'execute-api:Invoke',
                        Effect: 'Allow',
                        Resource: event.methodArn
                    }
                ]
            }
        };
    } else {
        return {
            principalId: 'user',
            policyDocument: {
                Version: '2012-10-17',
                Statement: [
                    {
                        Action: 'execute-api:Invoke',
                        Effect: 'Deny',
                        Resource: event.methodArn
                    }
                ]
            }
        };
    }
};

If you are using Okta as the provider, you should use Okta's SDK and API to verify the token and determine if the user is authorized.

const OktaJwtVerifier = require('@okta/jwt-verifier');
const oktaJwtVerifier = new OktaJwtVerifier({
  issuer: 'https://{yourOktaDomain}.com/oauth2/default',
  clientId: '{clientId}'
});
exports.handler = async (event) => {
    // Extract the token from the request
    const token = event.authorizationToken;
    
    // Verify the token using the Okta provider
    try {
        const jwt = await oktaJwtVerifier.verifyAccessToken(token);
        // Verify that the user has the required scope
        if (jwt.claims.scp.split(" ").includes("admin")) {
            return {
                principalId: jwt.claims.sub,
                policyDocument: {
                    Version: '2012-10-17',
                    Statement: [
                        {
                            Action: 'execute-api:Invoke',
                            Effect: 'Allow',
                            Resource: event.methodArn
                        }
                    ]
                }
            };
        }
    } catch (err) {
        console.log(err);
    }
    // Return a policy statement denying access
    return {
        principalId: 'user',
        policyDocument: {
            Version: '2012-10-17',
            Statement: [
                {
                    Action: 'execute-api:Invoke',
                    Effect: 'Deny',
                    Resource: event.methodArn
                }
            ]
        }
    };
};

This example uses the @okta/jwt-verifier package to verify the access token and check for the required scope(in this case "admin").

Create the Lambda Authorizer on API Gateway

Once the Lambda Authorizer function is created, it needs to be added as an Authorizer on the API Gateway. This can be done using the AWS Management Console or using Terraform. 

resource "aws_api_gateway_authorizer" "authorizer_name" {
  name = "your-authorizer-name"
  rest_api_id = aws_api_gateway_rest_api.api_name.id
  type = "TOKEN"
  identity_source = "method.request.header.Authorization"
  authorizer_uri = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${aws_lambda_function.authorizer_name.arn}/invocations"
}

The above code creates an Authorizer on the API Gateway and associates it with the Lambda function that we created at the start.

Update the Angular application

Once the Lambda Authorizer is set up, the Angular application needs to be updated to send the token in the Authorization header with each request. This can be done by adding an interceptor that adds the token to the headers before sending the request.

Here's an example of an Angular interceptor that adds the token to the headers:

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { TokenService } from './token.service';
import { Observable } from 'rxjs';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
    constructor(private tokenService: TokenService) {}
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        request = request.clone({
            setHeaders: {
                Authorization: `Bearer ${this.tokenService.getToken()}`
            }
        });
        return next.handle(request);
    }
}

This example assumes that you have a TokenService that retrieves the token from storage.

You will also need to register this interceptor in the app.module.ts file.

import { TokenInterceptor } from './token.interceptor';
providers:
...
@NgModule({
  ...
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptor,
      multi: true
    }
  ],
  ...
})
export class AppModule { }

This registers the interceptor in the AppModule, ensuring that the token is added to the headers of all HTTP requests made by the application.

Deploy the Terraform Configuration

After updating the Angular application, the Terraform configuration can be deployed to create and configure the necessary resources on AWS.
$ terraform init
$ terraform apply

Verify the Deployment

Now we can test to make sure that the Lambda Authorizer has been set up correctly, you can test the API Gateway endpoint using a tool like Postman and check the logs of the Lambda function to ensure that it is being invoked correctly.

Wrap Up!

By now you should be starting to see how we are building up this application and how you can start to implement your own logic into this magical paradigm. By using a Lambda Authorizer and integrating with a third-party provider like AWS Cognito, Okta or even many others! you can add an additional layer of security to your Angular application.

Getting some Dynamic Data with help from Lambda

In our previous blog posts we have seen how we can reduce costs by growing our web presence using very cheap tooling such as S3 and Javascript to do more with our websites, in reality we could use any Javascript framework to borrow our users CPU to run the website but in this guide we have selected Angular.

Create an AWS Lambda function

The first step in adding a serverless backend to your Angular application is to create an AWS Lambda function. Lambda is a serverless compute service that allows you to run code without provisioning or managing servers.

resource "aws_lambda_function" "function_name" {
  function_name = "your-function-name"
  runtime = "nodejs12.x"
  handler = "index.handler"
  role = aws_iam_role.iam_role.arn
  environment {
    variables = {
      your_env_variable = "your-value"
    }
  }
  code {
    zip_file = file("path/to/your-lambda-function.zip")
  }
}

And inside this zip file we might want to hold some code to run...

exports.handler = async (event) => {
    return {
        statusCode: 200,
        body: JSON.stringify({
            message: 'Hello World',
        }),
    };
};


This function simply returns an object containing a statusCode of 200 and a message body containing a JSON string of the message "Hello World" but it would be SO much more! 

Create an IAM Role

In order to execute the Lambda function, an IAM role with the necessary permissions must be created. This role can be created using Terraform.

resource "aws_iam_role" "iam_role" {
  name = "your-role-name"
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}
resource "aws_iam_role_policy" "iam_role_policy" {
  name = "your-role-policy"
  role = aws_iam_role.iam_role.id
  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:*"
      ],
      "Resource": "*"
    }
  ]
}
EOF
}

Create a Lambda API Gateway

Once the Lambda function is created, it needs to be exposed through an API Gateway. The API Gateway will handle the incoming requests and invoke the Lambda function.

resource "aws_api_gateway_rest_api" "api_name" {
  name = "your-api-name"
}
resource "aws_api_gateway_resource" "resource_name" {
  rest_api_id = aws_api_gateway_rest_api.api_name.id
  parent_id = aws_api_gateway_rest_api.api_name.root_resource_id
  path_part = "your-resource-path"

resource "aws_api_gateway_method" "method_name" {
  rest_api_id = aws_api_gateway_rest_api.api_name.id
  resource_id = aws_api_gateway_resource.resource_name.id
  http_method = "GET"
  authorization = "NONE"
}
resource "aws_api_gateway_integration" "integration_name" {
  rest_api_id = aws_api_gateway_rest_api.api_name.id
  resource_id = aws_api_gateway_resource.resource_name.id
  http_method = aws_api_gateway_method.method_name.http_method
  type = "AWS"
  uri = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${aws_lambda_function.function_name.arn}/invocations"
  integration_http_method = "POST"
}
resource "aws_api_gateway_deployment" "deployment_name" {
  rest_api_id = aws_api_gateway_rest_api.api_name.id
  stage_name = "prod"
}

Deploy the Terraform Configuration

After updating the Angular application, the Terraform configuration can be deployed to create and configure the necessary resources on AWS.

terraform init

terraform apply

Verify the Deployment

As always we should verify what we deploy, to make sure that the serverless backend has been set up correctly, you can test the API Gateway endpoint using a tool like Postman, and check the logs of the Lambda function to ensure that it is being invoked correctly.

Updating the Angular Application

Now that the Lambda function and API Gateway have been created, the Angular application needs to be updated to call the API Gateway endpoint. This can be done by adding an HTTP service in the Angular application, and making a GET request to the API Gateway endpoint.

In order to consume this data in your Angular application, you will need to create an Angular module that makes an HTTP request to the API Gateway endpoint and handle the response data. You can use the HttpClient module from @angular/common/http to make the request.

Here's an example of an Angular service that makes a GET request to the API Gateway endpoint and returns the data:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
  providedIn: 'root'
})
export class HelloWorldService {
  constructor(private http: HttpClient) { }
  getHelloWorld() {
    return this.http.get<any>('https://your-api-gateway-endpoint.execute-api.us-east-1.amazonaws.com/prod/your-resource-path');
  }
}

You can then import this service in your component class and call the getHelloWorld() method to retrieve the data like in the following example:

import { Component, OnInit } from '@angular/core';
import { HelloWorldService } from './hello-world.service';
@Component({
  selector: 'app-root',
  template: '<p> {{ message }} </p>'
})
export class AppComponent implements OnInit {
  message: string;
  constructor(private helloWorldService: HelloWorldService) {}
  ngOnInit() {
    this.helloWorldService.getHelloWorld().subscribe((data) => {
        this.message = data.message;
    });
  }
}

This component will now display the message "Hello World" on the webpage when implemented.

It's important to notice that you should replace the url of the API Gateway on the service file with the url of your API Gateway after you deploy it with terraform, also you will need to import HttpClientModule in the root module of your Angular application and import the service in the components that you want to consume the data.

Wrap Up!

As you can see, integrating a serverless backend using AWS Lambda and API Gateway is a great way to add dynamic functionality to your Angular application while also taking advantage of the scalability and cost-efficiency of serverless compute.

By using Terraform, we can automate the process of creating and configuring the necessary resources, making the deployment process faster and more efficient. Remember to verify the backend and the frontend are correctly deployed, and test the API Gateway endpoint to check the Lambda function is being invoked correctly.

Upgrading your S3 SPA with Angular

Hey! I am a poet! In our previous blog post we built a very cheap and cost effective website on S3 using Terraform, if you
haven't seen it you can find it here. Going a Step further we can do some clever things with this rather cheap concept. For example, why don't we upgrade this setup to add some more complex logic and turn it into a Web Application?

To accomplish this, let's look at some Javascript frameworks. We sadly need to use Javascript as S3 has no server side processing capabilities, in the future we will look at how we can accomplish tasks that require this but for now, let's built a WebApp with Angular, (not to be confused with AngularJS which uses much older concepts and beyond he scope of this post).

Deploying an Angular application to Amazon Web Services (AWS) can be a great way to scale and distribute your app to a global audience. In this blog post, we'll walk through the process of deploying an Angular application to AWS S3 with CloudFront and Route53 using Terraform.

Things we will need! 

Before getting started, it's important to make sure that you have the following prerequisites:

  • An AWS account
  • A running Angular application (or you can use ng new <APP NAME> to get you started)
  • Terraform installed on your local machine
  • AWS CLI installed on your local machine
  • AWS Access and Secret key
  • Basic knowledge of Angular and AWS services

Create a CloudFront Distribution

You should have already created the S3 bucket in the previous post so, the next step is to create a CloudFront distribution to handle the distribution of your app's files to users. CloudFront is a content delivery network (CDN) that can be used to distribute your app's files to a global audience.

resource "aws_cloudfront_distribution" "distribution_name" {
  origin {
    domain_name = aws_s3_bucket.bucket_name.bucket
    origin_id   = "S3-${aws_s3_bucket.bucket_name.bucket}"
  }
  enabled = true
  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "S3-${aws_s3_bucket.bucket_name.bucket}"
    forwarded_values {
      query_string = false
    }
  }
}


 

Adding a DNS with Route53

The next step in deploying your Angular application to AWS is to create a Route53 record to handle the routing of traffic to your app. Route53 is a scalable and highly available Domain Name System (DNS) service that can be used to route traffic to your app.

resource "aws_route53_record" "route53_record" {
  zone_id = "your-route53-zone-id"
  name    = "your-domain-name"
  type    = "A"
  alias {
    name                   = aws_cloudfront_distribution.distribution_name.domain_name
    zone_id                = aws_cloudfront_distribution.distribution_name.hosted_zone_id
    evaluate_target_health = true
  }
}

Build and Deploy the Application

Now that all the necessary resources are created for our Angular application, we can build and deploy the application on the S3 bucket.

To build the application you can use the command:

npm run build --prod

Then you can use the AWS CLI command to sync the built files into your S3 Bucket:

aws s3 sync build/ s3://your-bucket-name/

Deploy the Terraform Configuration

Once the application is built, you can deploy the Terraform configuration to create and configure the necessary resources for your Angular application on AWS.

$ terraform init

$ terraform apply

The terraform init command is used to initialise the Terraform configuration, while the terraform apply command is used to create and configure the resources defined in the configuration.

Verify the Deployment

To verify that your Angular application has been deployed correctly, you can visit the URL of your application in a web browser. This URL can be found in the AWS S3 console or in the Route53 record created earlier.

You can also check the CloudFront distribution status by visiting the CloudFront Management Console, ensuring the status is "Deployed".

Maintenance and Updating

In order to update the Angular application, you will have to rebuild the application and sync the new build with the S3 bucket. Then, you will have to run the terraform apply command to update the Cloudfront distribution.

It's also important to keep an eye on the costs associated with your AWS resources. You can use the AWS Cost Explorer to view and manage your AWS costs.

Wrap Up!

As you can see, deploying an Angular application to AWS S3 with CloudFront and Route53 can be a great way to scale and distribute your app to a global audience. By using Terraform, you can automate the process of creating and configuring the necessary resources, making the deployment process faster and more efficient. Remember to keep an eye on costs and always verify the application is deployed correctly.

Creating a Simple Application on S3

As I mentioned in a previous blog post, I have been looking at the most effecient way of publishing web content with infrastructure built on the cloud. So far S3 has provided one of the cheapest solutions in getting basic web content out to our users.

Let's walk through the very basic steps of deploying a static website to AWS S3 using Terraform. We will cover the basics of creating an S3 bucket and configuring it for static website hosting, as well as some best practices for managing your static website on AWS.

Before we get started, make sure you have the following prerequisites:

  • An AWS account set up and you are logged in to the AWS Management Console.
  • Terraform installed on your local machine.
  • The static website files that you want to host on S3.

Creating an S3 Bucket

The first step in hosting a static website on AWS S3 is to create an S3 bucket. An S3 bucket is a logical container for storing objects in S3, and is the root namespace for your objects.

To create an S3 bucket with Terraform, create a new configuration file called main.tf and add the following code:

resource "aws_s3_bucket" "example" {
  bucket = "example.com"
  acl    = "public-read"
}

This code creates an S3 bucket called "example.com" with a public read ACL, which allows anyone to read the objects in the bucket.

Enable Static Website Hosting

Now that you have created an S3 bucket, the next step is to enable static website hosting for it. To do this, add the following code to your configuration file:

resource "aws_s3_bucket_policy" "example" {
  bucket = aws_s3_bucket.example.id
  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::example.com/*"
    }
  ]
}
EOF
}

This code creates an S3 bucket policy that allows public read access to all objects in the bucket. This is required for static website hosting to work.

Configure Static Website Hosting

Now that static website hosting is enabled for your S3 bucket, the next step is to configure it. To do this, add the following code to your configuration file:

resource "aws_s3_bucket_website" "example" {
  bucket = aws_s3_bucket.example.id
  index_document = "index.html"
  error_document = "error.html"
}

This code config will create us a very simple AWS S3 Bucket website setup, pointing the root page to a static html file called index.html and posting users to error.html should there be a problem.

Upload the Static Website Files

Now that your S3 bucket is configured for static website hosting, the next step is to upload your static website files to it. To do this, use the aws s3 cp command to upload the files to the bucket:

aws s3 cp index.html s3://example.com/index.html
aws s3 cp error.html s3://example.com/error.html
aws s3 cp styles.css s3://example.com/styles.css

Replace "index.html", "error.html", and "styles.css" with the names of your own static website files.

Deploy the Resources

Now that you have defined all the resources you want to create on AWS and uploaded your static website files to S3, the final step is to deploy them. To do this, run the following command from the command line:

terraform apply

This will create the S3 bucket and configure it for static website hosting, and upload your static website files to the bucket.

index.html:

<html>
  <head>
    <title>My Static Website</title>
    <link rel="stylesheet" href="styles.css">
  </head>
  <body>
    <h1>Welcome to My Static Website!</h1>
    <p>This website is hosted on AWS S3 using Terraform.</p>
  </body>
</html>

error.html:

<html>
  <head>
    <title>Error</title>
  </head>
  <body>
    <h1>An error has occurred.</h1>
  </body>
</html>

styles.css:

body {
  font-family: Arial, sans-serif;
  color: #333;
}
h1 {
  color: #0070c9;
}

To upload these files to your S3 bucket, you can use the aws s3 cp command like this:

aws s3 cp index.html s3://example.com/index.html
aws s3 cp error.html s3://example.com/error.html
aws s3 cp styles.css s3://example.com/styles.css

This will upload the "index.html", "error.html", and "styles.css" files to the "example.com" S3 bucket.

Wrap Up!

That's how to host a static website on AWS S3 using Terraform. We walked through the steps of creating an S3 bucket, enabling static website hosting, and configuring it to meet your specific needs. We also demonstrated how to upload your static website files to the bucket. We hope this tutorial was helpful and gives you a good starting point for hosting static websites on AWS S3 using Terraform.

In our next post lets look at what more we can do with this, what technologies we can add to make our website do more things and even use our users computer cycles to power our website with JavaScript.