Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions Examples/LambdaFunctions/DynamoDBPutBackend-template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM template for deploying the DynamoDBPutBackend Lambda function.

Resources:
# DynamoDBPutBackend Function
dynamoDBPutBackendFunction:
Type: AWS::Serverless::Function
Properties:
Handler: Provided
Runtime: provided
CodeUri: .build/lambda/DynamoDBPutBackend/lambda.zip
Environment:
Variables:
DYNAMO_ENDPOINT_HOST_NAME: !Sub 'dynamodb.${AWS::Region}.amazonaws.com'
DYNAMO_TABLE_NAME: !Ref backendTable
LOG_LEVEL: debug
# Grants this function permission to perform actions on the table
Policies:
- Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "dynamodb:BatchGetItem"
- "dynamodb:GetItem"
- "dynamodb:Query"
- "dynamodb:Scan"
- "dynamodb:BatchWriteItem"
- "dynamodb:PutItem"
- "dynamodb:UpdateItem"
Resource: !GetAtt backendTable.Arn
# Instructs new versions to be published to an alias named "live".
AutoPublishAlias: live

backendTable:
Type: "AWS::DynamoDB::Table"
DeletionPolicy: Retain
Properties:
# Sets the table to On-Demand billing; PROVISIONED is recommended for predictable workloads
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: "PK"
AttributeType: "S"
- AttributeName: "SK"
AttributeType: "S"
KeySchema:
- AttributeName: "PK"
KeyType: "HASH"
- AttributeName: "SK"
KeyType: "RANGE"
11 changes: 10 additions & 1 deletion Examples/LambdaFunctions/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PackageDescription
let package = Package(
name: "swift-aws-lambda-runtime-samples",
platforms: [
.macOS(.v10_13),
.macOS(.v10_15),
],
products: [
// introductory example
Expand All @@ -16,6 +16,8 @@ let package = Package(
.executable(name: "ErrorHandling", targets: ["ErrorHandling"]),
// demostrate how to integrate with AWS API Gateway
.executable(name: "APIGateway", targets: ["APIGateway"]),
// demostrate how to integrate with AWS DynamoDB as a backend
.executable(name: "DynamoDBPutBackend", targets: ["DynamoDBPutBackend"]),
// fully featured example with domain specific business logic
.executable(name: "CurrencyExchange", targets: ["CurrencyExchange"]),
],
Expand All @@ -24,6 +26,8 @@ let package = Package(
// in real-world projects this would say
// .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0")
.package(name: "swift-aws-lambda-runtime", path: "../.."),
.package(url: "https://github.com/amzn/smoke-aws-credentials.git", from: "2.0.0"),
.package(url: "https://github.com/amzn/smoke-dynamodb.git", from: "2.0.0"),
],
targets: [
.target(name: "HelloWorld", dependencies: [
Expand All @@ -42,5 +46,10 @@ let package = Package(
.target(name: "CurrencyExchange", dependencies: [
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
]),
.target(name: "DynamoDBPutBackend", dependencies: [
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
.product(name: "SmokeAWSCredentials", package: "smoke-aws-credentials"),
.product(name: "SmokeDynamoDB", package: "smoke-dynamodb"),
]),
]
)
206 changes: 206 additions & 0 deletions Examples/LambdaFunctions/Sources/DynamoDBPutBackend/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import Foundation
import SmokeDynamoDB
import AWSLambdaRuntime
import AsyncHTTPClient
import SmokeAWSCredentials
import SmokeAWSCore
import NIO
import Logging

// MARK: - Run Lambda

Lambda.run(DynamoDBPutBackendHandler.init)

// MARK: - Custom Context handler

struct DynamoDBPutBackendContextHandler {

/**
The context to be passed to invocations of the DynamoDBPutBackend function.
*/
public struct Context {
public let dynamodbTable: DynamoDBCompositePrimaryKeyTable
public let idGenerator: () -> String
public let timestampGenerator: () -> String
public let logger: Logger

static let itemsPartitionKey = ["swift", "aws", "lambda", "runtime", "samples", "items"].dynamodbKey
static let itemPrefix = ["swift", "aws", "lambda", "runtime", "samples", "item"]

public init(dynamodbTable: DynamoDBCompositePrimaryKeyTable,
idGenerator: @escaping () -> String,
timestampGenerator: @escaping () -> String,
logger: Logger) {
self.dynamodbTable = dynamodbTable
self.idGenerator = idGenerator
self.timestampGenerator = timestampGenerator
self.logger = logger
}
}

struct Request: Codable {
let value: String

public init(value: String) {
self.value = value
}
}

struct Response: Codable {
let keyId: String

public init(keyId: String) {
self.keyId = keyId
}
}

func handle(context: Context, payload: Request, callback: @escaping (Result<Response, Error>) -> Void) {

let itemIdPostfix = "\(context.timestampGenerator()).\(context.idGenerator())"
let itemId = (Context.itemPrefix + [itemIdPostfix]).dynamodbKey

let itemKey = StandardCompositePrimaryKey(partitionKey: Context.itemsPartitionKey,
sortKey: itemId)
let itemDatabaseItem = StandardTypedDatabaseItem.newItem(withKey: itemKey,
andValue: payload)

do {
try context.dynamodbTable.insertItemAsync(itemDatabaseItem) { error in
if let error = error {
callback(.failure(error))
} else {
callback(.success(Response(keyId: itemId)))
}
}
} catch {
callback(.failure(error))
}
}
}

// MARK: - LambdaHandler implementation

enum DynamoDBPutBackendError: Swift.Error {
case unableToObtainCredentialsFromLambdaEnvironment
case missingEnvironmentVariable(reason: String)
case invalidEnvironmentVariable(reason: String)
}

struct DynamoDBPutBackendHandler: LambdaHandler {

typealias In = DynamoDBPutBackendContextHandler.Request
typealias Out = DynamoDBPutBackendContextHandler.Response

let dynamodbTableGenerator: AWSDynamoDBCompositePrimaryKeyTableGenerator
let handler: DynamoDBPutBackendContextHandler

// run once at cold start
init(eventLoop: EventLoop) throws {
let environment = EnvironmentVariables.getEnvironment()

// use the internal LambdaEventLoop as the EventLoopGroup
let clientEventLoopProvider = HTTPClient.EventLoopGroupProvider.shared(eventLoop)

guard let credentialsProvider = AwsContainerRotatingCredentialsProvider.get(
fromEnvironment: environment,
eventLoopProvider: clientEventLoopProvider) else {
throw DynamoDBPutBackendError.unableToObtainCredentialsFromLambdaEnvironment
}

let region = try environment.getRegion()

self.dynamodbTableGenerator = try Self.initializeDynamoDBTableGeneratorFromEnvironment(
environment: environment,
credentialsProvider: credentialsProvider,
region: region,
clientEventLoopProvider: clientEventLoopProvider)
self.handler = DynamoDBPutBackendContextHandler()
}

func timestampGenerator() -> String {
"\(Date().timeIntervalSinceReferenceDate)"
}

func idGenerator() -> String {
UUID().uuidString
}

private static func initializeDynamoDBTableGeneratorFromEnvironment(
environment: [String: String],
credentialsProvider: CredentialsProvider,
region: AWSRegion,
clientEventLoopProvider: HTTPClient.EventLoopGroupProvider) throws -> AWSDynamoDBCompositePrimaryKeyTableGenerator {
let dynamoEndpointHostName = try environment.get(EnvironmentVariables.dynamoEndpointHostName)
let dynamoTableName = try environment.get(EnvironmentVariables.dynamoTableName)

return AWSDynamoDBCompositePrimaryKeyTableGenerator(
credentialsProvider: credentialsProvider,
region: region, endpointHostName: dynamoEndpointHostName,
tableName: dynamoTableName,
eventLoopProvider: clientEventLoopProvider)
}

func getContext(context: Lambda.Context) -> DynamoDBPutBackendContextHandler.Context {
let dynamodbTable = dynamodbTableGenerator.with(logger: context.logger,
internalRequestId: context.requestId)

return DynamoDBPutBackendContextHandler.Context(dynamodbTable: dynamodbTable,
idGenerator: idGenerator,
timestampGenerator: timestampGenerator,
logger: context.logger)
}

func handle(context: Lambda.Context, payload: In, callback: @escaping (Result<Out, Error>) -> Void) {
let dynamoDBPutBackendContext = getContext(context: context)

handler.handle(context: dynamoDBPutBackendContext, payload: payload, callback: callback)
}
}

// MARK: - Convenience extensions

private struct EnvironmentVariables {
static let dynamoEndpointHostName = "DYNAMO_ENDPOINT_HOST_NAME"
static let dynamoTableName = "DYNAMO_TABLE_NAME"
static let region = "AWS_REGION"

static func getEnvironment() -> [String: String] {
return ProcessInfo.processInfo.environment
}
}

private extension Dictionary where Key == String, Value == String {
func get(_ key: String) throws -> String {
guard let value = self[key] else {
throw DynamoDBPutBackendError.missingEnvironmentVariable(reason:
"'\(key)' environment variable not specified.")
}

return value
}

func getRegion() throws -> AWSRegion {
let regionString = try get(EnvironmentVariables.region)

guard let region = AWSRegion(rawValue: regionString) else {
throw DynamoDBPutBackendError.invalidEnvironmentVariable(reason:
"Specified '\(EnvironmentVariables.region)' environment variable '\(regionString)' not a valid region.")
}

return region
}
}