<file_sep>security:
	@gosec --exclude-dir=node_modules ./...
	@echo "[OK] Go security check was completed!"

.PHONY: clean
clean: 
	@echo Cleaning project ...
	@go clean
	@rm -rf build/cmd
	@echo [OK] Done!

.PHONY: build
build: security build-binary

build-binary: ./cmd/*
	@echo Building lambdas ...
	@for dir in $^; do \
		echo Building $${dir}; \
		go build -o build/$${dir} $${dir}/main.go ; \
	done
	@echo [OK] Done!<file_sep># Test

1. Run [deploy](DEPLOY.md)
2. Copy the cloudfront url in the console
2. Open browser
  - https://[cloudfront]/
  - https://[cloudfront]/error (should throw 500 error)
  - https://[cloudfront]/upsert (shold throw 404 error, GET method is not supported)
  - https://[cloudfront]/show (should show 404 error for now)
  - Run `curl -X POST https://[cloudfront]/upsert` (should show 403 error, curl was blocked by waf)
  - Run `curl -X POST -A "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0" https://[cloudfront]/upsert` (should show Added!)
  - https://[cloudfront]/show (should show 200 now with movie information)
<file_sep>#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from '@aws-cdk/core'
import { name, version, author } from '../../package.json'

import { requiredEnv, fromEnv, ifEmtpy } from './lib/utils'
import { InfraStack, WafStack, ServiceStack } from './stacks'

const environment = requiredEnv('ENVIRONMENT')
const region = requiredEnv('AWS_REGION')
const cloudfrontRegion = 'us-east-1'

const configFromEnv = fromEnv({
  application: 'APPLICATION',
  createdBy: 'CREATED_BY',
  version: 'VERSION',
})

const commonParams = {
  environment,
  application: ifEmtpy(configFromEnv.application, name),
  version: ifEmtpy(configFromEnv.version, version),
  createdBy: ifEmtpy(configFromEnv.createdBy, author),
}

const app = new cdk.App()

const infraStack = new InfraStack(app, `${environment}-custom-resource`, {
  env: {
    region,
  },
  ...commonParams,
})

const wafStack = new WafStack(app, `${environment}-${configFromEnv.application}-waf`, {
  env: {
    region: cloudfrontRegion,
  },
  ...commonParams,
  scope: 'CLOUDFRONT',
})

const serviceStack = new ServiceStack(app, `${environment}-${configFromEnv.application}-service`, {
  env: {
    region,
  },
  ...commonParams,
  dynamoDB: {
    readMaxRCU: 500,
    writeMaxRCU: 1000,
    readMinRCU: 1,
    writeMinRCU: 1,
  },
  gateway: {
    throttlingBurstLimit: 500,
    throttlingRateLimit: 1000,
  },
  crossImportFuncArnExportName: `${infraStack.stackName}:CrossImportFuncArn`,
  webAclImport: {
    region: cloudfrontRegion,
    stackName: wafStack.stackName,
    outputKey: 'WebAclArn',
  }
})

serviceStack.addDependency(wafStack, 'WebAcl Arn')
serviceStack.addDependency(infraStack, 'Cross import function')
<file_sep>import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'

import { BaseStack, IBaseStackProps } from '../lib/stack'
import {
  Lambdas,
  Cloudfront,
  Dashbaord,
  Alarm,
  Table,
  Gateway,
  IDynamoDBThrotllingProps,
  IGatewayThrottling,
} from '../constructs'

export interface IServiceStackProps extends IBaseStackProps {
  dynamoDB: IDynamoDBThrotllingProps
  gateway: IGatewayThrottling
  crossImportFuncArnExportName: string
  webAclImport: {
    stackName: string,
    region: string,
    outputKey: string,
  }
}

export class ServiceStack extends BaseStack {
  constructor (scope: cdk.Construct, id: string, props: IServiceStackProps) {
    super(scope, id, props)

    const crossImportFunction = lambda.Function.fromFunctionArn(this, 'CrossImportFunction', cdk.Fn.importValue(props.crossImportFuncArnExportName))

    const webAcl = new cdk.CustomResource(this, 'CloudfrontResource', {
      serviceToken: crossImportFunction.functionArn,
      properties: {
        StackName: props.webAclImport.stackName,
        Region: props.webAclImport.region,
        OutputKey: props.webAclImport.outputKey,
      }
    })

    const tableName = `${props.environment}-${props.application}`
    const dynamodb = new Table(this, 'DynamoDB', {
      ...props.dynamoDB,
      tableName,
    })
    const table = dynamodb.table

    const apiName = `${props.environment}-${props.application}`
    const stageName = props.environment
    const api = new Gateway(this, 'Gateway', {
      apiName,
      stageName,
      ...props.gateway,
    })
    const gateway = api.gateway

    new Lambdas(this, 'Lambdas', {
      gateway,
      table,
    })

    const cf = new Cloudfront(this, 'Cloudfront', {
      gateway,
      stage: stageName,
      webAclArn: webAcl.getAtt('OutputValue').toString()
    })

    new Dashbaord(this, 'Dashboard', {
      gateway,
      table,
    })

    new Alarm(this, 'Alarm', {
      gateway,
      table,
    })

    new cdk.CfnOutput(this, 'DistributionID', {
      description: 'DistributionID of Cloudfront',
      value: cf.cloudfront.distributionId,
      exportName: `${this.stackName}:DistributionID`,
    })

    new cdk.CfnOutput(this, 'DistributionDomainName', {
      description: 'DistributionDomainName of Cloudfront',
      value: cf.cloudfront.distributionDomainName,
      exportName: `${this.stackName}:DistributionDomainName`,
    })
  }
}
<file_sep>package main

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"strings"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
	internal "github.com/singlewind/sampleweb/internal/aws"
	"github.com/singlewind/sampleweb/pkg"
)

func ShowHandler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
	client, err := internal.NewDynamoDBClient()

	if err != nil {
		response := events.APIGatewayV2HTTPResponse{
			StatusCode: http.StatusInternalServerError,
		}
		fmt.Println("Fail to start session")
		fmt.Println(err.Error())
		return response, err
	}

	tableName, _ := os.LookupEnv("TABLE_NAME")
	movieName := "The Big New Movie"
	movieYear := "2015"
	result, err := client.GetItem(context.TODO(), &dynamodb.GetItemInput{
		TableName: aws.String(tableName),
		Key: map[string]types.AttributeValue{
			"Year": &types.AttributeValueMemberN{
				Value: movieYear,
			},
			"Title": &types.AttributeValueMemberS{
				Value: movieName,
			},
		},
	})

	if err != nil {
		response := events.APIGatewayV2HTTPResponse{
			StatusCode: http.StatusInternalServerError,
		}
		fmt.Println(err.Error())
		return response, err
	}

	if result.Item == nil {
		response := events.APIGatewayV2HTTPResponse{
			StatusCode: http.StatusNotFound,
		}
		fmt.Printf("Could not find '%v'", movieName)
		return response, nil
	}

	var item pkg.Item
	err = attributevalue.UnmarshalMap(result.Item, &item)
	if err != nil {
		response := events.APIGatewayV2HTTPResponse{
			StatusCode: http.StatusInternalServerError,
		}
		fmt.Printf("Failed to unmarshal Record, '%v'", err)
		return response, err
	}

	var body strings.Builder
	fmt.Fprintln(&body, "Found item:")
	fmt.Fprintln(&body, "Year: ", item.Year)
	fmt.Fprintln(&body, "Title: ", item.Title)
	fmt.Fprintln(&body, "Plot: ", item.Plot)
	fmt.Fprintln(&body, "Rating: ", item.Rating)
	return events.APIGatewayV2HTTPResponse{
		StatusCode: http.StatusOK,
		Body:       body.String(),
	}, nil
}

func main() {
	lambda.Start(ShowHandler)
}
<file_sep>import * as cdk from '@aws-cdk/core'
import * as api from '@aws-cdk/aws-apigatewayv2'
import * as log from '@aws-cdk/aws-logs'

export interface IGatewayThrottling {
  throttlingBurstLimit: number
  throttlingRateLimit: number
}

export interface IGatewayProps extends IGatewayThrottling {
  stageName: string
  apiName: string
}

export class Gateway extends cdk.Construct {
  readonly gateway: api.HttpApi
  constructor (scope: cdk.Construct, id: string, props: IGatewayProps) {
    super(scope, id)

    const gateway = new api.HttpApi(this, 'Gateway', {
      disableExecuteApiEndpoint: false,
      createDefaultStage: false,
      corsPreflight: {
        allowHeaders: [
          'Content-Type',
          'X-Amz-Date',
          'Authorization',
          'X-Api-Key',
        ],
        allowMethods: [
          api.CorsHttpMethod.OPTIONS,
          api.CorsHttpMethod.GET,
          api.CorsHttpMethod.POST,
          api.CorsHttpMethod.PUT,
          api.CorsHttpMethod.PATCH,
          api.CorsHttpMethod.DELETE,
        ],
        allowCredentials: true,
      },
      apiName: props.apiName
    })

    const stage = new api.HttpStage(this, 'GatewayStage', {
      httpApi: gateway,
      stageName: props.stageName,
      autoDeploy: true,
    })

    const cfnStage = stage.node.defaultChild as api.CfnStage
    cfnStage.defaultRouteSettings = {
      throttlingBurstLimit: props.throttlingBurstLimit,
      throttlingRateLimit: props.throttlingRateLimit,
    }

    const accessLog = new log.LogGroup(this, 'AccessLogGroup', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      retention: log.RetentionDays.ONE_WEEK,
    })

    cfnStage.accessLogSettings = {
      destinationArn: accessLog.logGroupArn,
      format: JSON.stringify({
        id: '$context.requestId',
        path: '$context.path',
        protocol: '$context.protocol',
        method: '$context.httpMethod',
        time: '$context.requestTime',
        ip: '$context.identity.sourceIp',
        agent: '$context.identity.userAgent',
        latency: '$context.integration.latency'
      })
    }

    this.gateway = gateway
  }
}
<file_sep>package main

import (
	"context"
	"net/http"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func HomeHandler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
	return events.APIGatewayV2HTTPResponse{
		StatusCode: http.StatusOK,
		Body:       "Home",
	}, nil
}

func main() {
	lambda.Start(HomeHandler)
}
<file_sep>module github.com/singlewind/sampleweb

go 1.16

require (
	github.com/aws/aws-lambda-go v1.24.0 // indirect
	github.com/aws/aws-sdk-go-v2 v1.7.0 // indirect
	github.com/aws/aws-sdk-go-v2/config v1.4.0 // indirect
	github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.1.2 // indirect
	github.com/aws/aws-sdk-go-v2/service/dynamodb v1.4.0 // indirect
	github.com/securego/gosec/v2 v2.8.1 // indirect
)
<file_sep>import { Stack, StackProps, Construct, Tags } from '@aws-cdk/core'

export interface IBaseStackProps extends StackProps {
  environment: string;
  application: string;
  version: string;
  createdBy: string;
}

export class BaseStack extends Stack {
  constructor (scope: Construct, id: string, props: IBaseStackProps) {
    super(scope, id, props)

    Tags.of(this).add('Environment', props.environment)
    Tags.of(this).add('Application', props.application)
    Tags.of(this).add('Version', props.version)
    Tags.of(this).add('CreatedBy', props.createdBy)
  }
}
<file_sep>package aws

import (
	"context"

	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
)

func NewDynamoDBClient() (*dynamodb.Client, error) {
	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		return nil, err
	}

	client := dynamodb.NewFromConfig(cfg)
	return client, nil
}
<file_sep>import logging
import boto3
import cfnresponse
import random
import string
import json

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

def handler(event, context):
  logger.info(json.dumps(event))
  request_type = event.get("RequestType")
  if request_type == "Delete":
    on_delete(event, context)
  else:
    on_update(event, context)

def get_resource_physical_id(event):
  if "PhysicalResourceId" in event:
    return event["PhysicalResourceId"]
  else:
    stack_name = get_stack_name(event["StackId"])
    logical_resource_id = event["LogicalResourceId"]
    random_id = "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(12))
    return "{0}-{1}-{2}".format(stack_name, logical_resource_id.lower(), random_id)

def get_stack_name(stack_id):
  cfn_client = boto3.client("cloudformation")
  response = cfn_client.describe_stacks(StackName=stack_id)
  return response["Stacks"][0]["StackName"]

def on_update(event, context):
  resource_physical_id = get_resource_physical_id(event)
  try:
    region = event['ResourceProperties']['Region']
    output_key = event['ResourceProperties']['OutputKey']
    stack_name = event['ResourceProperties']['StackName']

    usSession = boto3.Session(region_name=region)
    cfn_client = usSession.client("cloudformation")
    outputs = cfn_client.describe_stacks(
      StackName=stack_name,
    )['Stacks'][0]['Outputs']

    logger.info(json.dumps(outputs))

    output_value = [o for o in outputs if o['OutputKey'] == output_key][0]['OutputValue']

    response_data = {
      "OutputValue": output_value,
      "OutputKey": output_key,
    }

    cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, resource_physical_id)
  except Exception as e:
    logger.exception(e)
    cfnresponse.send(event, context, cfnresponse.FAILED, {}, resource_physical_id)

def on_delete(event, context):
  resource_physical_id = get_resource_physical_id(event)
  response_data = {}
  cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, resource_physical_id)<file_sep># Learn

1. Cloudfront associated WebAcl only can be deployed in us-east-1
2. HTTP API Gateway cannot change to edge as default
3. HTTP API has less feature than REST API
4. Cannot use wafv2 to associate cloudfront acl, need pass webacl arn not id cross region to cloudfront stack. (The documentation is incorrect, it says WebACLId but expect ARN value instead Id)
5. New Bot Control is interesting, but still can be fooled
6. Made a custom resource with new cdk interface to allow cross region import
<file_sep># Sampleweb

## Table of Content

PS> Put code in `$GOPATH/src/github.com/singlewind/sampleweb` or clone from [repo](https://github.com/singlewind/sampleweb)

- [Develope](docs/DEVELOP.md)
- [Deploy](docs/DEPLOY.md)
- [Test](docs/TEST.md)
- [Learn](docs/LEARN.md)

## Architecture
```
                bucket                            dynamodb
                  ▲                                  ▲
                  │                                  │
client ─────► cloudfront ─────► api gateway ─────► lambda
                  │                 │                │
                  ▼                 │                ▼
                 waf                └──────────► cloudwatch
```

## References
- [Project Layout](https://github.com/golang-standards/project-layout)
- [Golang SDK v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2)
- [Golang Lambda](https://docs.aws.amazon.com/lambda/latest/dg/lambda-golang.html)
- [CDK lambda-go](https://docs.aws.amazon.com/cdk/api/latest/typescript/api/aws-lambda-go.html)
- [CDK WAF](https://github.com/cdk-patterns/serverless/blob/main/the-waf-apigateway/typescript/lib/api-gateway-stack.ts)
- [CDK Solution Construct](https://docs.aws.amazon.com/solutions/latest/constructs/welcome.html)
- [Cloudfront Cloudformation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-distributionconfig.html#cfn-cloudfront-distribution-distributionconfig-webaclid)<file_sep>import * as cloudwatch from '@aws-cdk/aws-cloudwatch'
import * as cdk from '@aws-cdk/core'

export const requiredEnv = (varKey: string): string => {
  const varValue = process.env[varKey]

  if (!varValue) throw new Error(`${varKey} is required`)

  return varValue
}

export const fromEnv = <K extends string>(
  requirements: Record<K, string>
): Record<K, string> => {
  return Object.keys(requirements).reduce<Record<K, string>>((acc, key) => {
    const value = requirements[key as K]
    return {
      ...acc,
      [key]: process.env[value],
    }
  }, {} as Record<K, string>)
}

export const ifEmtpy = (valueA: string, valueB: string): string => valueA !== '' ? valueA : valueB

export const buildGraphWidget = (widgetName: string, metrics: cloudwatch.IMetric[], stacked = false): cloudwatch.GraphWidget => {
  return new cloudwatch.GraphWidget({
    title: widgetName,
    left: metrics,
    stacked: stacked,
    width: 8
  })
}

export const metricForApiGw = (apiId: string, metricName: string, label: string, stat = 'avg'): cloudwatch.Metric => {
  const dimensions = {
    ApiId: apiId
  }
  return buildMetric(metricName, 'AWS/ApiGateway', dimensions, cloudwatch.Unit.COUNT, label, stat)
}

export const buildMetric = (metricName: string, namespace: string, dimensions: any, unit: cloudwatch.Unit, label: string, stat = 'avg', period = 900): cloudwatch.Metric => {
  return new cloudwatch.Metric({
    metricName,
    namespace: namespace,
    dimensions: dimensions,
    unit: unit,
    label: label,
    statistic: stat,
    period: cdk.Duration.seconds(period)
  })
}
<file_sep>package main

import (
	"context"
	"net/http"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

func ErrorHandler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
	return events.APIGatewayV2HTTPResponse{
		StatusCode: http.StatusInternalServerError,
	}, nil
}

func main() {
	lambda.Start(ErrorHandler)
}
<file_sep>export * from './imports'
export * from './lambdas'
export * from './cloudfront'
export * from './alarm'
export * from './dashboard'
export * from './table'
export * from './gatway'
<file_sep>import * as cdk from '@aws-cdk/core'
import * as dynamodb from '@aws-cdk/aws-dynamodb'
import * as cloudwatch from '@aws-cdk/aws-cloudwatch'
import * as sns from '@aws-cdk/aws-sns'
import * as actions from '@aws-cdk/aws-cloudwatch-actions'
import * as api from '@aws-cdk/aws-apigatewayv2'
import { metricForApiGw } from '../lib/utils'

export interface AlarmProps {
  table?: dynamodb.ITable
  gateway?: api.IApi
}

export class Alarm extends cdk.Construct {
  constructor (scope: cdk.Construct, id: string, props: AlarmProps = {}) {
    super(scope, id)

    const errorTopic = new sns.Topic(this, 'errorTopic')

    if (props.table) {
      const dynamoDBTotalErrors = new cloudwatch.MathExpression({
        expression: 'm1 + m2',
        label: 'DynamoDB Errors',
        usingMetrics: {
          m1: props.table.metricUserErrors(),
          m2: props.table.metricSystemErrorsForOperations(),
        },
        period: cdk.Duration.minutes(5),
      })

      new cloudwatch.Alarm(this, 'DynamoDB Errors > 0', {
        metric: dynamoDBTotalErrors,
        threshold: 0,
        evaluationPeriods: 6,
        datapointsToAlarm: 1,
        treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
      }).addAlarmAction(new actions.SnsAction(errorTopic))

      const dynamoDBThrottles = new cloudwatch.MathExpression({
        expression: 'm1 + m2',
        label: 'DynamoDB Throttles',
        usingMetrics: {
          m1: props.table.metric('ReadThrottleEvents', { statistic: 'sum' }),
          m2: props.table.metric('WriteThrottleEvents', { statistic: 'sum' }),
        },
        period: cdk.Duration.minutes(5),
      })

      new cloudwatch.Alarm(this, 'DynamoDB Table Reads/Writes Throttled', {
        metric: dynamoDBThrottles,
        threshold: 1,
        evaluationPeriods: 6,
        datapointsToAlarm: 1,
        treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
      }).addAlarmAction(new actions.SnsAction(errorTopic))
    }

    if (props.gateway) {
      const apiGateway4xxErrorPercentage = new cloudwatch.MathExpression({
        expression: 'm1/m2*100',
        label: '% API Gateway 4xx Errors',
        usingMetrics: {
          m1: metricForApiGw(props.gateway.apiId, '4XXError', '4XX Errors', 'sum'),
          m2: metricForApiGw(props.gateway.apiId, 'Count', '# Requests', 'sum'),
        },
        period: cdk.Duration.minutes(5),
      })

      new cloudwatch.Alarm(this, 'API Gateway 4XX Errors > 5%', {
        metric: apiGateway4xxErrorPercentage,
        threshold: 5,
        evaluationPeriods: 6,
        datapointsToAlarm: 1,
        treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
      }).addAlarmAction(new actions.SnsAction(errorTopic))

      const apiGateway5xxErrorPercentage = new cloudwatch.MathExpression({
        expression: 'm1/m2*100',
        label: '% API Gateway 4xx Errors',
        usingMetrics: {
          m1: metricForApiGw(props.gateway.apiId, '5XXError', '5XX Errors', 'sum'),
          m2: metricForApiGw(props.gateway.apiId, 'Count', '# Requests', 'sum'),
        },
        period: cdk.Duration.minutes(5),
      })

      new cloudwatch.Alarm(this, 'API Gateway 5XX Errors > 1%', {
        metric: apiGateway5xxErrorPercentage,
        threshold: 1,
        evaluationPeriods: 6,
        datapointsToAlarm: 1,
        treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
      }).addAlarmAction(new actions.SnsAction(errorTopic))

      new cloudwatch.Alarm(this, 'API p99 latency alarm >= 5s', {
        metric: metricForApiGw(props.gateway.apiId, 'Latency', 'API GW Latency', 'p99'),
        threshold: 5000,
        evaluationPeriods: 6,
        datapointsToAlarm: 1,
        treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
      }).addAlarmAction(new actions.SnsAction(errorTopic))
    }
  }
}
<file_sep>import * as cdk from '@aws-cdk/core'
import * as s3 from '@aws-cdk/aws-s3'
import * as iam from '@aws-cdk/aws-iam'
import * as api from '@aws-cdk/aws-apigatewayv2'
import * as cloudfront from '@aws-cdk/aws-cloudfront'
import * as origins from '@aws-cdk/aws-cloudfront-origins'

export interface ICloudfrontProps {
  gateway: api.HttpApi
  stage: string
  webAclArn: string
}

export class Cloudfront extends cdk.Construct {
  readonly cloudfront: cloudfront.Distribution
  constructor (scope: cdk.Construct, id: string, props: ICloudfrontProps) {
    super(scope, id)

    const stack = cdk.Stack.of(this)

    const bucketName = `${stack.stackName}-cloudfront-logging`
    const loggingBucket = new s3.Bucket(this, 'Bucket', {
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      versioned: true,
      encryption: s3.BucketEncryption.S3_MANAGED,
      accessControl: s3.BucketAccessControl.LOG_DELIVERY_WRITE,
    })

    cdk.Tags.of(loggingBucket).add('Name', bucketName)

    const bucketPolicy = new s3.BucketPolicy(this, 'BucketPolicy', {
      bucket: loggingBucket,
    })

    bucketPolicy.document.addStatements(
      new iam.PolicyStatement({
        sid: 'HttpsOnly',
        actions: ['*'],
        conditions: {
          Bool: {
            'aws:SecureTransport': 'false'
          },
        },
        effect: iam.Effect.DENY,
        principals: [new iam.AnyPrincipal()],
        resources: [`${loggingBucket.bucketArn}/*`]
      })
    )

    const apiEndPointUrlWithoutProtocol = cdk.Fn.select(1, cdk.Fn.split('://', props.gateway.apiEndpoint))
    const apiEndPointDomainName = cdk.Fn.select(0, cdk.Fn.split('/', apiEndPointUrlWithoutProtocol))

    this.cloudfront = new cloudfront.Distribution(this, 'Distribution', {
      defaultBehavior: {
        origin: new origins.HttpOrigin(apiEndPointDomainName, {
          originPath: `/${props.stage}`,
        }),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
      },
      enableLogging: true,
      logBucket: loggingBucket,
      webAclId: props.webAclArn,
    })
  }
}
<file_sep>import * as cdk from '@aws-cdk/core'
import * as wafv2 from '@aws-cdk/aws-wafv2'

import { BaseStack, IBaseStackProps } from '../lib/stack'

export interface WafStackProps extends IBaseStackProps {
  scope: 'CLOUDFRONT' | 'REGIONAL'
}

export class WafStack extends BaseStack {
  constructor (scope: cdk.Construct, id: string, props: WafStackProps) {
    super(scope, id, props)

    const awsCommonRule = <wafv2.CfnWebACL.RuleProperty> {
      priority: 1,
      overrideAction: <wafv2.CfnWebACL.OverrideActionProperty> { none: {} },
      statement: <wafv2.CfnWebACL.StatementProperty> {
        managedRuleGroupStatement: <wafv2.CfnWebACL.ManagedRuleGroupStatementProperty>{
          name: 'AWSManagedRulesCommonRuleSet',
          vendorName: 'AWS',
          excludedRules: [{
            name: 'SizeRestrictions_BODY',
          }]
        }
      },
      visibilityConfig: <wafv2.CfnWebACL.VisibilityConfigProperty> {
        cloudWatchMetricsEnabled: true,
        metricName: 'awsCommonRules',
        sampledRequestsEnabled: true,
      },
      name: 'AWS-AWSManagedRulesCommonRuleSet',
    }

    const awsAnonIPList = <wafv2.CfnWebACL.RuleProperty> {
      priority: 2,
      overrideAction: <wafv2.CfnWebACL.OverrideActionProperty> { none: {} },
      statement: <wafv2.CfnWebACL.StatementProperty> {
        managedRuleGroupStatement: <wafv2.CfnWebACL.ManagedRuleGroupStatementProperty>{
          name: 'AWSManagedRulesAnonymousIpList',
          vendorName: 'AWS',
        }
      },
      visibilityConfig: <wafv2.CfnWebACL.VisibilityConfigProperty> {
        cloudWatchMetricsEnabled: true,
        metricName: 'awsAnonIPList',
        sampledRequestsEnabled: true,
      },
      name: 'AWS-AWSManagedRulesAnonymousIpList',
    }

    const awsIPRepList = <wafv2.CfnWebACL.RuleProperty> {
      priority: 3,
      overrideAction: <wafv2.CfnWebACL.OverrideActionProperty> { none: {} },
      statement: <wafv2.CfnWebACL.StatementProperty> {
        managedRuleGroupStatement: <wafv2.CfnWebACL.ManagedRuleGroupStatementProperty>{
          name: 'AWSManagedRulesAmazonIpReputationList',
          vendorName: 'AWS',
        }
      },
      visibilityConfig: <wafv2.CfnWebACL.VisibilityConfigProperty> {
        cloudWatchMetricsEnabled: true,
        metricName: 'awsReputation',
        sampledRequestsEnabled: true,
      },
      name: 'AWS-AWSManagedRulesAmazonIpReputationList',
    }

    const awsBotControl = <wafv2.CfnWebACL.RuleProperty> {
      priority: 4,
      overrideAction: <wafv2.CfnWebACL.OverrideActionProperty> { none: {} },
      statement: <wafv2.CfnWebACL.StatementProperty> {
        managedRuleGroupStatement: <wafv2.CfnWebACL.ManagedRuleGroupStatementProperty>{
          name: 'AWSManagedRulesBotControlRuleSet',
          vendorName: 'AWS',
          excludedRules: [{
            name: 'CategoryMonitoring',
          }],
        }
      },
      visibilityConfig: <wafv2.CfnWebACL.VisibilityConfigProperty> {
        cloudWatchMetricsEnabled: true,
        metricName: 'awsBotControl',
        sampledRequestsEnabled: true,
      },
      name: 'AWS-AWSManagedRulesBotControlRuleSet',
    }

    const webAclName = `${props.environment}-${props.application}`
    const webAclMetricName = `${props.environment}-${props.application}-webacl`

    const webacl = new wafv2.CfnWebACL(this, 'WebAcl', {
      defaultAction: <wafv2.CfnWebACL.DefaultActionProperty> {
        allow: {},
      },
      scope: props.scope,
      visibilityConfig: <wafv2.CfnWebACL.VisibilityConfigProperty> {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: webAclMetricName,
      },
      rules: [
        awsCommonRule,
        awsAnonIPList,
        awsIPRepList,
        awsBotControl,
      ],
      name: webAclName,
    })

    new cdk.CfnOutput(this, 'WebAclID', {
      description: `WebAclID of ${webAclName}`,
      value: webacl.attrId,
      exportName: `${this.stackName}:WebACLID`,
    })

    new cdk.CfnOutput(this, 'WebAclArn', {
      description: `WebAclArn of ${webAclName}`,
      value: webacl.attrArn,
      exportName: `${this.stackName}:WebAclArn`,
    })

    new cdk.CfnOutput(this, 'WebAclMetricName', {
      description: `Metric name of ${webAclName}`,
      value: webAclMetricName,
      exportName: `${this.stackName}:WebAclMetricName`,
    })
  }
}
<file_sep>package main

import (
	"context"
	"fmt"
	"net/http"
	"os"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
	internal "github.com/singlewind/sampleweb/internal/aws"
	"github.com/singlewind/sampleweb/pkg"
)

func UpsertHandler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
	client, err := internal.NewDynamoDBClient()

	if err != nil {
		response := events.APIGatewayV2HTTPResponse{
			StatusCode: http.StatusInternalServerError,
		}
		fmt.Println("Fail to start session")
		fmt.Println(err.Error())
		return response, err
	}

	item := pkg.Item{
		Year:   2015,
		Title:  "The Big New Movie",
		Plot:   "Nothing happens at all.",
		Rating: 0.0,
	}

	av, err := attributevalue.MarshalMap(item)
	if err != nil {
		response := events.APIGatewayV2HTTPResponse{
			StatusCode: http.StatusInternalServerError,
		}
		fmt.Println("Got error marshalling new movie item:")
		fmt.Println(err.Error())
		return response, err
	}

	tableName, _ := os.LookupEnv("TABLE_NAME")

	input := &dynamodb.PutItemInput{
		Item:      av,
		TableName: aws.String(tableName),
	}

	_, err = client.PutItem(context.TODO(), input)
	if err != nil {
		response := events.APIGatewayV2HTTPResponse{
			StatusCode: http.StatusInternalServerError,
		}
		fmt.Println("Got error calling PutItem:")
		fmt.Println(err.Error())
		return response, err
	}

	return events.APIGatewayV2HTTPResponse{
		StatusCode: http.StatusOK,
		Body:       "Added!",
	}, nil

}

func main() {
	lambda.Start(UpsertHandler)
}
<file_sep># How to Develope

1. clone repo into `git clone <EMAIL>:singlewind/sampleweb.git $GOPATH/src/github.com/singlewind/sampleweb`
2. Install `make` by run `xcode-select --install` or `brew install make`
3. Run `make build`
4. You can find the binaries in `build/cmd` folder

## Other Commands
- `make build` run security and build binaries 
- `make security` scan go source code for vulnerabilities
- `make clean` delete all builds
- `npm run lint` lint typescript code

## Local DynamoDB

1. Install [Docker Desktop](https://www.docker.com/products/docker-desktop)
2. Run `docker-compose up -d` to start DynamoDB local version
3. Run `aws dynamodb list-tables --endpoint-url http://localhost:8000` to test dynamodb is working
4. Open `http://localhost:8000/shell` and copy the following code into the editor, then run to create the table
```javascript
var params = {
  TableName: 'Movies',
  KeySchema: [ 
    { 
      AttributeName: 'Title',
      KeyType: 'HASH',
    },
    {
      AttributeName: 'Year',
      KeyType: 'RANGE',
    }
  ],
  AttributeDefinitions: [ 
    {
      AttributeName: 'Title',
      AttributeType: 'S', 
    },
    {
      AttributeName: 'Year',
      AttributeType: 'N', 
    }
  ],
  ProvisionedThroughput: { 
    ReadCapacityUnits: 10, 
    WriteCapacityUnits: 10, 
  }
};

dynamodb.createTable(params, function(err, data) {
  if (err) ppJson(err); // an error occurred
  else ppJson(data); // successful response
});
```
5. Repeat step 3 to confirm<file_sep>import * as cdk from '@aws-cdk/core'
import * as dynamodb from '@aws-cdk/aws-dynamodb'

export interface IDynamoDBThrotllingProps {
  readMaxRCU: number,
  writeMaxRCU: number,
  readMinRCU: number,
  writeMinRCU: number,
}

export interface IDynamoDBProps extends IDynamoDBThrotllingProps {
  tableName: string
}

export class Table extends cdk.Construct {
  readonly table: dynamodb.Table
  constructor (scope: cdk.Construct, id: string, props: IDynamoDBProps) {
    super(scope, id)

    const table = new dynamodb.Table(this, 'DynamoDB', {
      billingMode: dynamodb.BillingMode.PROVISIONED,
      readCapacity: 1,
      writeCapacity: 1,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      partitionKey: { name: 'Title', type: dynamodb.AttributeType.STRING },
      sortKey: { name: 'Year', type: dynamodb.AttributeType.NUMBER },
      pointInTimeRecovery: true,
      tableName: props.tableName,
    })

    const readScaling = table.autoScaleReadCapacity({
      minCapacity: props.readMinRCU,
      maxCapacity: props.readMaxRCU,
    })

    readScaling.scaleOnUtilization({
      targetUtilizationPercent: 50,
    })

    const writeCapacity = table.autoScaleWriteCapacity({
      minCapacity: props.writeMinRCU,
      maxCapacity: props.writeMaxRCU,
    })

    writeCapacity.scaleOnUtilization({
      targetUtilizationPercent: 50,
    })

    this.table = table
  }
}
<file_sep># TODO List

## CDK
- [x] DynamoDB
- [x] API Gateway
- [x] CloudFront
- [x] WAF
- [x] Lambda
- [x] Alarm
- [x] Dashboard

## Lambda
- [x] Read (GET)
- [x] Write (PUT|POST)
- [x] Home (GET)
- [x] Error (GET)

## DEV ENV
- [x] Typescript Dev Environment
- [x] Golang Dev Environment
- [ ] ~~DynamoDB Local~~ 

* Have setup dynamodb locally, could not make `sam` working with local invoke. It may relates to `GoFunction` experimental cdk feature. Instead of zip code, it is bootstraped binary. Will dig more later.
<file_sep>import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'
import * as iam from '@aws-cdk/aws-iam'
import { PythonFunction } from '@aws-cdk/aws-lambda-python'

import { BaseStack, IBaseStackProps } from '../lib/stack'

export class InfraStack extends BaseStack {
  constructor (scope: cdk.Construct, id: string, props: IBaseStackProps) {
    super(scope, id, props)

    const crossImportFunctionName = `${this.stackName}-cross-import`
    const crossImportFunction = new PythonFunction(this, 'CrossImportCustomResourceFunction', {
      entry: 'deployments/cdk/lambda/cross-import',
      runtime: lambda.Runtime.PYTHON_3_8,
    })

    cdk.Tags.of(crossImportFunction).add('Name', crossImportFunctionName)

    crossImportFunction.addToRolePolicy(new iam.PolicyStatement({
      actions: ['cloudformation:*'],
      resources: ['*'],
      effect: iam.Effect.ALLOW,
    }))

    new cdk.CfnOutput(this, 'CrossImportFuncArn', {
      description: `Arn of ${crossImportFunctionName}`,
      value: crossImportFunction.functionArn,
      exportName: `${this.stackName}:CrossImportFuncArn`,
    })
  }
}
<file_sep>import * as cdk from '@aws-cdk/core'
import * as dynamodb from '@aws-cdk/aws-dynamodb'
import * as cloudwatch from '@aws-cdk/aws-cloudwatch'
import * as api from '@aws-cdk/aws-apigatewayv2'
import * as utils from '../lib/utils'

export interface DashbaordProps {
  table?: dynamodb.ITable
  gateway?: api.IApi
}

export class Dashbaord extends cdk.Construct {
  constructor (scope: cdk.Construct, id: string, props: DashbaordProps = {}) {
    super(scope, id)

    const stack = cdk.Stack.of(this)

    const dashboard = new cloudwatch.Dashboard(this, 'CloudWatchDashBoard', {
      dashboardName: `${stack.stackName}-dashboard`
    })

    if (props.table) {
      dashboard.addWidgets(
        utils.buildGraphWidget('DynamoDB Latency', [
          props.table.metricSuccessfulRequestLatency({ dimensions: { TableName: props.table.tableName, Operation: 'GetItem' } }),
          props.table.metricSuccessfulRequestLatency({ dimensions: { TableName: props.table.tableName, Operation: 'PutItem' } }),
        ], true),
        utils.buildGraphWidget('DynamoDB Consumed Read/Write Units', [
          props.table.metric('ConsumedReadCapacityUnits'),
          props.table.metric('ConsumedWriteCapacityUnits')
        ], false),
        utils.buildGraphWidget('DynamoDB Throttles', [
          props.table.metric('ReadThrottleEvents', { statistic: 'sum' }),
          props.table.metric('WriteThrottleEvents', { statistic: 'sum' }),
        ], true)
      )
    }

    if (props.gateway) {
      dashboard.addWidgets(
        utils.buildGraphWidget('Requests', [
          utils.metricForApiGw(props.gateway.apiId, 'Count', '# Requests', 'sum')
        ]),
        utils.buildGraphWidget('API GW Latency', [
          utils.metricForApiGw(props.gateway.apiId, 'Latency', 'API Latency p50', 'p50'),
          utils.metricForApiGw(props.gateway.apiId, 'Latency', 'API Latency p90', 'p90'),
          utils.metricForApiGw(props.gateway.apiId, 'Latency', 'API Latency p99', 'p99')
        ], true),
        utils.buildGraphWidget('API GW Errors', [
          utils.metricForApiGw(props.gateway.apiId, '4XXError', '4XX Errors', 'sum'),
          utils.metricForApiGw(props.gateway.apiId, '5XXError', '5XX Errors', 'sum')
        ], true),
      )
    }
  }
}
<file_sep>export * from './wafStack'
export * from './infraStack'
export * from './serviceStack'
<file_sep>import * as cdk from '@aws-cdk/core'
import * as dynamodb from '@aws-cdk/aws-dynamodb'
import * as api from '@aws-cdk/aws-apigatewayv2'
import * as integrations from '@aws-cdk/aws-apigatewayv2-integrations'
import { GoFunction } from '@aws-cdk/aws-lambda-go'

export interface ILambdasProps {
  gateway: api.HttpApi,
  table: dynamodb.Table,
}

export class Lambdas extends cdk.Construct {
  readonly webAclID: string
  constructor (scope: cdk.Construct, id: string, props: ILambdasProps) {
    super(scope, id)

    const stack = cdk.Stack.of(this)

    const errorFunction = new GoFunction(this, 'ErrorFunction', {
      entry: 'cmd/error',
    })

    cdk.Tags.of(errorFunction).add('Name', `${stack.stackName}-error`)

    props.gateway.addRoutes({
      path: '/error',
      methods: [api.HttpMethod.GET, api.HttpMethod.HEAD, api.HttpMethod.OPTIONS],
      integration: new integrations.LambdaProxyIntegration({
        handler: errorFunction,
      })
    })

    const homeFunction = new GoFunction(this, 'HomeFunction', {
      entry: 'cmd/home',
    })

    cdk.Tags.of(homeFunction).add('Name', `${stack.stackName}-home`)

    props.gateway.addRoutes({
      path: '/',
      methods: [api.HttpMethod.GET, api.HttpMethod.HEAD, api.HttpMethod.OPTIONS],
      integration: new integrations.LambdaProxyIntegration({
        handler: homeFunction,
      })
    })

    const showFunction = new GoFunction(this, 'ShowFunction', {
      entry: 'cmd/show',
      environment: {
        TABLE_NAME: props.table.tableName,
      },
    })

    cdk.Tags.of(showFunction).add('Name', `${stack.stackName}-show`)

    props.table.grant(showFunction, 'dynamodb:GetItem')

    props.gateway.addRoutes({
      path: '/show',
      methods: [api.HttpMethod.GET, api.HttpMethod.HEAD, api.HttpMethod.OPTIONS],
      integration: new integrations.LambdaProxyIntegration({
        handler: showFunction,
      })
    })

    const upsertFunction = new GoFunction(this, 'UpsertFunction', {
      entry: 'cmd/upsert',
      environment: {
        TABLE_NAME: props.table.tableName,
      },
    })

    cdk.Tags.of(upsertFunction).add('Name', `${stack.stackName}-upsert`)

    props.table.grant(upsertFunction, 'dynamodb:GetItem', 'dynamodb:PutItem')

    props.gateway.addRoutes({
      path: '/upsert',
      methods: [api.HttpMethod.POST, api.HttpMethod.PUT, api.HttpMethod.PATCH, api.HttpMethod.HEAD, api.HttpMethod.OPTIONS],
      integration: new integrations.LambdaProxyIntegration({
        handler: upsertFunction,
      })
    })
  }
}
<file_sep># Deploy

1. Run `npm install`
2. Run `cp .sample.envrc .envrc` and change the value based on needs. Default value should working straight away
3. If you have `direnv`, run `direnv allow`, other wise `source .envrc` 
4. Run `npx cdk bootstrap` (optional, when you run `npm run deploy` it will run bootstrap anyway)
5. Run `npx cdk bootstrap --region us-east-1` (optional, if you would like deploy custom resource in us-east-1 as well)
6. Run `npm run deploy`

## Other Commands
- `npm run cdk:synth` synth cdk template
- `npm run deploy` bootstrap then deploy
<file_sep>import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'

export interface IWafImportsProps {
  stackName: string,
  region: string,
  outputKey: string,
}

export interface IImportsProps {
  crossImportFuncArnExportName: string
  waf: IWafImportsProps
}

export class Imports extends cdk.Construct {
  readonly webAclID: cdk.Reference
  constructor (scope: cdk.Construct, id: string, props: IImportsProps) {
    super(scope, id)

    const crossImportFunction = lambda.Function.fromFunctionArn(this, 'CrossImportFunction', cdk.Fn.importValue(props.crossImportFuncArnExportName))

    const wafInfo = new cdk.CustomResource(this, 'WafAclResource', {
      serviceToken: crossImportFunction.functionArn,
      properties: {
        StackName: props.waf.stackName,
        Region: props.waf.region,
        OutputKey: props.waf.outputKey,
      }
    })
    this.webAclID = wafInfo.getAtt('OutputValue')
  }
}
