A Swift Lift & Shift

Adam Groves
Blue Harvest Tech Blog
8 min readMar 11, 2019

A brief guide to migrating your web app to the cloud.

Migrating apps to the cloud can be a “bear”, but it doesn’t have to be.

It was 5 months ago when I decided to make one of the biggest leaps I’ve ever made in my life. Circumstances at the time steered me towards something different. At the time I was living in the United States, where I’ve lived my whole life. At 37, I was beyond overdue for a change of scenery, culture, and experiences so I made the decision to move to Europe. The reasoning is beyond the scope of this article, so I’ll save the details for another time :)

In short, after rigorously applying for jobs in London, Dublin, and Amsterdam, I landed a job interview with Blue Harvest; a startup within Capgemini, residing in Amsterdam. After a few rounds of interviews, I was tasked with completing a coding challenge. The challenge was to create an app that displays news results from newsapi.org. To get a project up and running quickly with sane defaults, I chose to bootstrap the project using Create React App. I named the app Nuze and the source can be found here. The project is completely front end and makes external API request from the front end. My goal was to learn more about Amazon Web Services by executing a “lift and shift” of a simple app, so I chose to use Nuze.

I won’t go into detail about how the coding challenge was implemented since this article will be concentrating on how to move an existing app to the cloud, in it’s simplest form. I wanted to create a foundational infrastructure that I could expand upon while providing all the essential elements for hosting a web app that utilizes a CDN for the front end, and scalability on the backend. I chose the AWS CloudFront to serve my app via a CDN to ensure low-latency and caching. For the back-end, I chose to utilize AWS Lambda which will be triggered via HTTP requests coming in through API Gateway. CloudFront, Lambda, and API Gateway are all services provided by AWS. They will be explained in the following sections.

AWS

As the leader in cloud services, I chose Amazon Web Services for my cloud platform for which I would migrate Nuze to. Micro-services in AWS can be implemented in a few different ways. In some scenarios, it may make more sense to leverage EC2 containers and Kubernetes clusters (in the form of AWS EKS service) to provide the scalability necessary. However, I chose to use Lambdas due to their innate autoscaling and simplicity. Since Nuze is small and only contains one API call to a 3rd party news API, this would suffice. The following AWS services were used for this migration and you can get more detail on what each service provides in the following links:

Services Used

API Gateway

Lambda

S3

CloudFront

Route53

API Gateway + Lambda

In combination with API Gateway, the Lambda service will take an HTTP request from the API Gateway and trigger a function to run. That function will subsequently fetch news articles from newsapi.org, essentially acting as a proxy. There are benefits to allowing the server (or service) to make requests instead of the client (browser). One benefit is that API keys or any secret used to access data is not exposed to the client. Anyone can open up dev-tools in their browser and extract the API key and use it for themselves (and potentially costing you money if used maliciously). Another reason for making 3rd party API request from the server is that often times, API requests from browsers are prohibited due to CORS restrictions (the server not allowing browser requests by not including an appropriate Access-Control-Allow-Origin header value.)

S3

An S3 bucket is where we will store the static front-end code. What exists in the bucket are the files that are produced when running npm run build from the front-end root directory. This is the build script provided by Create React App. The files will subsequently be available in the <project root>/build directory. The location of your distribution files may differ, but essentially, you should be able to add any front end build to the S3 bucket and make the appropriate back end calls to the API Gateway (which will route your API calls to the appropriate Lambda)

CloudFront

CloudFront will provide us with a CDN which will allow for better security, availability, and performance. Using a CDN, the front end assets will be served from a server in closer proximity, resulting in less latency. Caching is also provided by CloudFront which boosts performance as well.

Route53

Lastly, Route53 will be our DNS service. This will allow us to provide a custom URL to our application. In this case, nooze.blueharvest.io.

Architecture

The architecture necessary to support the migration is displayed in the following diagram. The infrastructure will be built from code opposed to manually configuring AWS via their console. Terraform will be used as the tool to accomplish this. Known as “infrastructure-as-code”, Terraform will be explained in the next section.

AWS Architecture constructed after running `terraform apply` on below Terraform manifest files.

Terraform

Terraform is a tool that enables you to write, plan, and create infrastructure as code. Terraform uses what is called “providers” that can be used along with the provider’s “resources” to set up and configure cloud infrastructure. Since I’m using AWS, the provider is unsurprisingly “aws”. AWS also has a similar tool for describing infrastructure as code, CloudFormation. Although this would have also sufficed, I felt that Terraform was more readable and easier to conceptualize the infrastructure.

I won’t go into detail about how to use Terraform, but if interested, the following files are used and will build the AWS infrastructure upon running terraform apply: You can read more on Terraform here.

variables.tf
This file contains variables that are used throughout the other .tf files

variable "region" {  
default = "eu-west-1"
}

variable "nooze_domain_name" {
default = "nooze.blueharvest.io"
}

// We'll also need the root domain (also known as zone apex or naked domain).
variable "root_domain_name" {
default = "blueharvest.io"
}

variable "blueharvest_zone_id" {
default = "Z31OVNF5EA1VAW"
}

provider.tf
This file contains provider-specific configuration. This is where we define what provider to use and set it up accordingly. In our case, “aws” provider is used. We also set up an archive provider which is responsible for compressing our website files that will be deployed to the S3 bucket.

provider "aws" {  
region = "eu-west-1"
}

provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}

terraform {
backend "s3" {
encrypt = true
bucket = "blueharvest-terraform-state-storage-s3"
region = "eu-west-1"
key = "blueharvest/terraform/nooze"
}
}

provider "archive" {}

data "aws_caller_identity" "current" {}

articles.tf
This file is where most of the infrastructure is defined. IAM, API Gateway, Lambdas, S3, CloudFront and Route53 are all defined and configured in this file.

data "archive_file" "get_articles" {  
type = "zip"
source_dir = "lambda_functions/get_articles"
output_path = "lambda_functions/get_articles.zip"
}

/* IAM */
resource "aws_iam_role" "nooze_lambda_iam" {
name = "nooze_lambda_iam"
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" "nooze_lambda_policy" {
name = "nooze_lambda_policy"
role = "${aws_iam_role.nooze_lambda_iam.id}"

policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stm1",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Sid": "Stm2",
"Effect": "Allow",
"Action": [
"lambda:GetFunction"
],
"Resource": "${aws_lambda_function.get_articles.arn}:*"
}
]
}
EOF
}

resource "aws_iam_policy" "nooze_s3_policy" {
name = "nooze_s3_policy"
description = "Nooze S3 policy"

policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObjectAcl",
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject",
"s3:PutObjectAcl"
],
"Resource": [
"arn:aws:s3:::${var.nooze_domain_name}/"
]
}
]
}
EOF
}

resource "aws_iam_role_policy_attachment" "nooze_lambda_policy_s3_policy_attachment" {
role = "${aws_iam_role.nooze_lambda_iam.name}"
policy_arn = "${aws_iam_policy.nooze_s3_policy.arn}"
}

/* Lambda */
resource "aws_lambda_function" "get_articles" {
filename = "lambda_functions/get_articles.zip"
function_name = "get_articles"
role = "${aws_iam_role.nooze_lambda_iam.arn}"
handler = "lambda_function.handler"
source_code_hash = "${data.archive_file.get_articles.output_base64sha256}"
runtime = "nodejs8.10"
publish = true
tags = {
Project = "articles"
}
}

resource "aws_lambda_permission" "nooze_lambda_permission_get_articles" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = "${aws_lambda_function.get_articles.arn}"
principal = "apigateway.amazonaws.com"
source_arn = "arn:aws:execute-api:${var.region}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.nooze_api_gateway.id}/*/${aws_api_gateway_method.nooze_api_get.http_method}${aws_api_gateway_resource.nooze_api_resource_get_articles.path}"
}


/* API Gateway */
resource "aws_api_gateway_rest_api" "nooze_api_gateway" {
name = "Nooze API"
description = "API for fetching articles"
}

resource "aws_api_gateway_usage_plan" "nooze_admin_api_key_usage_plan" {
name = "Nooze API key usage plan"
description = "Usage plan for the admin API key for articles."
api_stages {
api_id = "${aws_api_gateway_rest_api.nooze_api_gateway.id}"
stage = "${aws_api_gateway_deployment.nooze_api_deployment.stage_name}"
}
}

resource "aws_api_gateway_usage_plan_key" "nooze_admin_api_key_usage_plan_key" {
key_id = "${aws_api_gateway_api_key.nooze_admin_api_key.id}"
key_type = "API_KEY"
usage_plan_id = "${aws_api_gateway_usage_plan.nooze_admin_api_key_usage_plan.id}"
}

resource "aws_api_gateway_api_key" "nooze_admin_api_key" {
name = "Nooze API Key"
}

resource "aws_api_gateway_resource" "nooze_api_resource_get_articles" {
rest_api_id = "${aws_api_gateway_rest_api.nooze_api_gateway.id}"
parent_id = "${aws_api_gateway_rest_api.nooze_api_gateway.root_resource_id}"
path_part = "{proxy+}"
}

resource "aws_api_gateway_method" "nooze_api_get" {
rest_api_id = "${aws_api_gateway_rest_api.nooze_api_gateway.id}"
resource_id = "${aws_api_gateway_resource.nooze_api_resource_get_articles.id}"
http_method = "GET"
authorization = "NONE"
api_key_required = true
}

resource "aws_api_gateway_method_response" "nooze_api_delete_response" {
rest_api_id = "${aws_api_gateway_rest_api.nooze_api_gateway.id}"
resource_id = "${aws_api_gateway_resource.nooze_api_resource_get_articles.id}"
http_method = "${aws_api_gateway_method.nooze_api_get.http_method}"
status_code = "200"
response_models = {
"application/json" = "Empty"
}
}

resource "aws_api_gateway_integration" "nooze_api_get_lambda" {
rest_api_id = "${aws_api_gateway_rest_api.nooze_api_gateway.id}"
resource_id = "${aws_api_gateway_resource.nooze_api_resource_get_articles.id}"
http_method = "${aws_api_gateway_method.nooze_api_get.http_method}"
integration_http_method = "GET"
type = "AWS_PROXY"
uri = "arn:aws:apigateway:${var.region}:lambda:path/2015-03-31/functions/${aws_lambda_function.get_articles.arn}/invocations"
}

resource "aws_api_gateway_deployment" "nooze_api_deployment" {
depends_on = ["aws_api_gateway_integration.nooze_api_get_lambda"]
rest_api_id = "${aws_api_gateway_rest_api.nooze_api_gateway.id}"
stage_name = "Production"
}

/* WWW Site Bucket */

data "aws_iam_policy_document" "blueharvest-terraform-nooze-site-policy" {
statement {
sid = "bucket_policy_site_main"
effect = "Allow"

actions = [
"s3:GetObject",
]

resources = [
"arn:aws:s3:::${var.nooze_domain_name}/*",
]

principals {
type = "*"
identifiers = ["*"]
}
}
}

resource "aws_s3_bucket" "blueharvest-terraform-nooze-site" {
bucket = "${var.nooze_domain_name}"
force_destroy = true
acl = "public-read"

website {
index_document = "index.html"
error_document = "index.html"
}
}


resource "aws_s3_bucket_policy" "blueharvest-terraform-nooze-site-policy" {
bucket = "${aws_s3_bucket.blueharvest-terraform-nooze-site.id}"
policy = "${data.aws_iam_policy_document.blueharvest-terraform-nooze-site-policy.json}"
}

/* AWS Certificate */

resource "aws_acm_certificate" "blueharvest-terraform-site-certificate" {
provider = "aws.us_east_1"
domain_name = "${var.nooze_domain_name}"
validation_method = "DNS"
}

resource "aws_route53_record" "blueharvest-terraform-site-certificate-record" {
name = "${aws_acm_certificate.blueharvest-terraform-site-certificate.domain_validation_options.0.resource_record_name}"
type = "${aws_acm_certificate.blueharvest-terraform-site-certificate.domain_validation_options.0.resource_record_type}"
zone_id = "${var.blueharvest_zone_id}"
records = ["${aws_acm_certificate.blueharvest-terraform-site-certificate.domain_validation_options.0.resource_record_value}"]
ttl = 60
}

resource "aws_acm_certificate_validation" "blueharvest-terraform-site-certificate-validation" {
provider = "aws.us_east_1"
certificate_arn = "${aws_acm_certificate.blueharvest-terraform-site-certificate.arn}"

validation_record_fqdns = ["${aws_route53_record.blueharvest-terraform-site-certificate-record.fqdn}"]
}

/* Cloud Front Distributions */

resource "aws_cloudfront_distribution" "nooze_cloudfront_distribution" {
origin {
custom_origin_config {
// These are all the defaults.
http_port = "80"
https_port = "443"
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1", "TLSv1.1", "TLSv1.2"]
}

domain_name = "${aws_s3_bucket.blueharvest-terraform-nooze-site.website_endpoint}"
origin_id = "${var.nooze_domain_name}"
}

enabled = true
default_root_object = "index.html"

// All values are defaults from the AWS console.
default_cache_behavior {
viewer_protocol_policy = "redirect-to-https"
compress = true
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "${var.nooze_domain_name}"
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000

forwarded_values {
query_string = false

cookies {
forward = "none"
}
}
}

aliases = ["${var.nooze_domain_name}"]

restrictions {
geo_restriction {
restriction_type = "none"
}
}

viewer_certificate {
acm_certificate_arn = "${aws_acm_certificate.blueharvest-terraform-site-certificate.arn}"
ssl_support_method = "sni-only"
}
}

/* AWS 53 Web Site Records */

resource "aws_route53_record" "blueharvest-terraform-nooze-site-record" {
zone_id = "${var.blueharvest_zone_id}"
name = "${var.nooze_domain_name}"
type = "A"

alias {
name = "${aws_cloudfront_distribution.nooze_cloudfront_distribution.domain_name}"
zone_id = "${aws_cloudfront_distribution.nooze_cloudfront_distribution.hosted_zone_id}"
evaluate_target_health = false
}
}

output "Nooze Doamin" {
value = "${var.nooze_domain_name}"
}
output "CloudFront Domain Name" {
value = "${aws_cloudfront_distribution.nooze_cloudfront_distribution.domain_name}"
}
output "Admin API Key" {
value = "${aws_api_gateway_api_key.nooze_admin_api_key.value}"
}

You can read more about each used Terraform resource here:

That’s all for this article. Thanks for reading!

--

--