Chapter 14:
- Write a Terraform template to create the
InsertMovie
Lambda function resources.
Answer: Setup execution role for the Lambda function:
resource "aws_iam_role" "role" { name = "InsertMovieRole" assume_role_policy = "${file("assume-role-policy.json")}" } resource "aws_iam_policy" "cloudwatch_policy" { name = "PushCloudWatchLogsPolicy" policy = "${file("cloudwatch-policy.json")}" } resource "aws_iam_policy" "dynamodb_policy" { name = "ScanDynamoDBPolicy" policy = "${file("dynamodb-policy.json")}" } resource "aws_iam_policy_attachment" "cloudwatch-attachment" { name = "cloudwatch-lambda-attchment" roles = ["${aws_iam_role.role.name}"] policy_arn = "${aws_iam_policy.cloudwatch_policy.arn}" } resource "aws_iam_policy_attachment" "dynamodb-attachment" { name = "dynamodb-lambda-attchment" roles = ["${aws_iam_role.role.name}"] policy_arn = "${aws_iam_policy.dynamodb_policy.arn}" }
Next, create the Lambda function:
resource "aws_lambda_function" "insert" { function_name = "InsertMovie" handler = "main" filename = "function/deployment.zip" runtime = "go1.x" role = "${aws_iam_role.role.arn}" environment { variables { TABLE_NAME = "movies" } } }
Expose a POST
method on /movies
resources in the REST API:
resource "aws_api_gateway_method" "proxy" { rest_api_id = "${var.rest_api_id}" resource_id = "${var.resource_id}" http_method = "POST" authorization = "NONE" } resource "aws_api_gateway_integration" "lambda" { rest_api_id = "${var.rest_api_id}" resource_id = "${var.resource_id}" http_method = "${aws_api_gateway_method.proxy.http_method}" integration_http_method = "POST" type = "AWS_PROXY" uri = "${aws_lambda_function.insert.invoke_arn}" } resource "aws_lambda_permission" "apigw" { statement_id = "AllowAPIGatewayInvoke" action = "lambda:InvokeFunction" function_name = "${aws_lambda_function.insert.arn}" principal = "apigateway.amazonaws.com" source_arn = "${var.execution_arn}/*/*" }
- Update the CloudFormation template to trigger the defined Lambda function with API Gateway in response to incoming HTTP request.
Answer: Add the following properties to the Resources
section:
API: Type: 'AWS::ApiGateway::RestApi' Properties: Name: API FailOnWarnings: 'true' DemoResource: Type: 'AWS::ApiGateway::Resource' Properties: ParentId: 'Fn::GetAtt': [API, RootResourceId] PathPart: demo RestApiId: Ref: API DisplayMessageMethod: Type: 'AWS::ApiGateway::Method' Properties: HttpMethod: GET AuthorizationType: NONE ResourceId: Ref: DemoResource RestApiId: Ref: API Integration: Type: AWS Uri: {'Fn::Join': ["", "- \"arn:aws:apigateway:\"\n- !Ref \"AWS::Region\"\n- \":lambda:path/\"\n- \"/2015-03-31/functions/\"\n- Fn::GetAtt:\n - HelloWorldFunction\n - Arn\n- \"/invocations\""]} IntegrationHttpMethod: GET
- Write a SAM file to model and defines all the resources needed to build the Serverless API we built through this book.Answer:
Resources: FindAllMovies: Type: AWS::Serverless::Function Properties: Handler: main Runtime: go1.x Role: !GetAtt FindAllMoviesRole.Arn CodeUri: ./findall/deployment.zip Environment: Variables: TABLE_NAME: !Ref MoviesTable Events: AnyRequest: Type: Api Properties: Path: /movies Method: GET RestApiId: Ref: MoviesAPI InsertMovie: Type: AWS::Serverless::Function Properties: Handler: main Runtime: go1.x Role: !GetAtt InsertMovieRole.Arn CodeUri: ./insert/deployment.zip Environment: Variables: TABLE_NAME: !Ref MoviesTable Events: AnyRequest: Type: Api Properties: Path: /movies Method: POST RestApiId: Ref: MoviesAPI DeleteMovie: Type: AWS::Serverless::Function Properties: Handler: main Runtime: go1.x Role: !GetAtt DeleteMovieRole.Arn CodeUri: ./delete/deployment.zip Environment: Variables: TABLE_NAME: !Ref MoviesTable Events: AnyRequest: Type: Api Properties: Path: /movies Method: DELETE RestApiId: Ref: MoviesAPI UpdateMovie: Type: AWS::Serverless::Function Properties: Handler: main Runtime: go1.x Role: !GetAtt UpdateMovieRole.Arn CodeUri: ./update/deployment.zip Environment: Variables: TABLE_NAME: !Ref MoviesTable Events: AnyRequest: Type: Api Properties: Path: /movies Method: PUT RestApiId: Ref: MoviesAPI
- Configure Terraform to store the generated state file in a remote S3 backend.
Answer: Create an S3 bucket with the following AWS CLI command:
aws s3 mb s3://terraform-state-files --region us-east-1
Enable server side encryption on the bucket:
aws s3api put-bucket-encryption --bucket terraform-state-files \
--server-side-encryption-configuration file://config.json
The encryption mechanism is set to AES-256:
{ "Rules": [ { "ApplyServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" } } ] }
Configure Terraform to use the bucket defined earlier:
terraform { backend "s3" { bucket = "terraform-state-files" key = "KEY_NAME" region = "us-east-1" } }
- Create a CloudFormation template for the Serverless API we built through this book.Answer:
AWSTemplateFormatVersion: "2010-09-09" Description: "Simple Lambda Function" Parameters: BucketName: Description: "S3 Bucket name" Type: "String" TableName: Description: "DynamoDB Table Name" Type: "String" Default: "movies" Resources: FindAllMoviesRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Policies: - PolicyName: "PushCloudWatchLogsPolicy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "*" - PolicyName: "ScanDynamoDBTablePolicy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - dynamodb:Scan Resource: "*" FindAllMovies: Type: "AWS::Lambda::Function" Properties: Code: S3Bucket: !Ref BucketName S3Key: findall-deployment.zip FunctionName: "FindAllMovies" Handler: "main" Runtime: "go1.x" Role: !GetAtt FindAllMoviesRole.Arn Environment: Variables: TABLE_NAME: !Ref TableName InsertMovieRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Policies: - PolicyName: "PushCloudWatchLogsPolicy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "*" - PolicyName: "PutItemDynamoDBTablePolicy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: "*" InsertMovie: Type: "AWS::Lambda::Function" Properties: Code: S3Bucket: !Ref BucketName S3Key: insert-deployment.zip FunctionName: "InsertMovie" Handler: "main" Runtime: "go1.x" Role: !GetAtt InsertMovieRole.Arn Environment: Variables: TABLE_NAME: !Ref TableName UpdateMovieRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Policies: - PolicyName: "PushCloudWatchLogsPolicy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "*" - PolicyName: "PutItemDynamoDBTablePolicy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - dynamodb:PutItem Resource: "*" UpdateMovie: Type: "AWS::Lambda::Function" Properties: Code: S3Bucket: !Ref BucketName S3Key: update-deployment.zip FunctionName: "UpdateMovie" Handler: "main" Runtime: "go1.x" Role: !GetAtt UpdateMovieRole.Arn Environment: Variables: TABLE_NAME: !Ref TableName DeleteMovieRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Policies: - PolicyName: "PushCloudWatchLogsPolicy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "*" - PolicyName: "DeleteItemDynamoDBTablePolicy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - dynamodb:DeleteItem Resource: "*" DeleteMovie: Type: "AWS::Lambda::Function" Properties: Code: S3Bucket: !Ref BucketName S3Key: update-deployment.zip FunctionName: "DeleteMovie" Handler: "main" Runtime: "go1.x" Role: !GetAtt DeleteMovieRole.Arn Environment: Variables: TABLE_NAME: !Ref TableName MoviesApi: Type: "AWS::ApiGateway::RestApi" Properties: Name: "MoviesApi" FailOnWarnings: "true" MoviesResource: Type: "AWS::ApiGateway::Resource" Properties: ParentId: Fn::GetAtt: - "MoviesApi" - "RootResourceId" PathPart: "movies" RestApiId: Ref: MoviesApi CreateMovieMethod: Type: "AWS::ApiGateway::Method" Properties: HttpMethod: "POST" AuthorizationType: "NONE" ResourceId: Ref: MoviesResource RestApiId: Ref: MoviesApi Integration: Type: "AWS" Uri: Fn::Join: - "" - - "arn:aws:apigateway:" - !Ref "AWS::Region" - ":lambda:path/" - "/2015-03-31/functions/" - Fn::GetAtt: - InsertMovie - Arn - "/invocations" IntegrationHttpMethod: "POST" DeleteMovieMethod: Type: "AWS::ApiGateway::Method" Properties: HttpMethod: "DELETE" AuthorizationType: "NONE" ResourceId: Ref: MoviesResource RestApiId: Ref: MoviesApi Integration: Type: "AWS" Uri: Fn::Join: - "" - - "arn:aws:apigateway:" - !Ref "AWS::Region" - ":lambda:path/" - "/2015-03-31/functions/" - Fn::GetAtt: - DeleteMovie - Arn - "/invocations" IntegrationHttpMethod: "DELETE" UpdateMovieMethod: Type: "AWS::ApiGateway::Method" Properties: HttpMethod: "PUT" AuthorizationType: "NONE" ResourceId: Ref: MoviesResource RestApiId: Ref: MoviesApi Integration: Type: "AWS" Uri: Fn::Join: - "" - - "arn:aws:apigateway:" - !Ref "AWS::Region" - ":lambda:path/" - "/2015-03-31/functions/" - Fn::GetAtt: - UpdateMovie - Arn - "/invocations" IntegrationHttpMethod: "PUT" ListMoviesMethod: Type: "AWS::ApiGateway::Method" Properties: HttpMethod: "GET" AuthorizationType: "NONE" ResourceId: Ref: MoviesResource RestApiId: Ref: MoviesApi Integration: Type: "AWS" Uri: Fn::Join: - "" - - "arn:aws:apigateway:" - !Ref "AWS::Region" - ":lambda:path/" - "/2015-03-31/functions/" - Fn::GetAtt: - FindAllMovies - Arn - "/invocations" IntegrationHttpMethod: "GET" DynamoDBTable: Type: "AWS::DynamoDB::Table" Properties: TableName: !Ref TableName AttributeDefinitions: - AttributeName: "ID" AttributeType: "S" KeySchema: - AttributeName: "ID" KeyType: "HASH" ProvisionedThroughput: ReadCapacityUnits: 5 WriteCapacityUnits: 5
- Create a Terraform template for the Serverless API we built through this book.
Answer: In order to avoid duplication of code and keep the template file clean and easy to follow and maintain, Loops
, conditions
, maps
and list
can be used to create the IAM roles for the defined Lambda functions:
resource "aws_iam_role" "roles" { count = "${length(var.functions)}" name = "${element(var.functions, count.index)}Role" assume_role_policy = "${file("policies/assume-role-policy.json")}" } resource "aws_iam_policy" "policies" { count = "${length(var.functions)}" name = "${element(var.functions, count.index)}Policy" policy = "${file("policies/${element(var.functions, count.index)}-policy.json")}" } resource "aws_iam_policy_attachment" "policy-attachments" { count = "${length(var.functions)}" name = "${element(var.functions, count.index)}Attachment" roles = ["${element(aws_iam_role.roles.*.name, count.index)}"] policy_arn = "${element(aws_iam_policy.policies.*.arn, count.index)}" }
Same can be applied to create the required Lambda functions:
resource "aws_lambda_function" "functions" { count = "${length(var.functions)}" function_name = "${element(var.functions, count.index)}" handler = "main" filename = "functions/${element(var.functions, count.index)}.zip" runtime = "go1.x" role = "${element(aws_iam_role.roles.*.arn, count.index)}" environment { variables { TABLE_NAME = "${var.table_name}" } } }
Finally, the RESTful API can be created as follows:
resource "aws_api_gateway_rest_api" "api" { name = "MoviesAPI" } resource "aws_api_gateway_resource" "proxy" { rest_api_id = "${aws_api_gateway_rest_api.api.id}" parent_id = "${aws_api_gateway_rest_api.api.root_resource_id}" path_part = "movies" } resource "aws_api_gateway_deployment" "staging" { depends_on = ["aws_api_gateway_integration.integrations"] rest_api_id = "${aws_api_gateway_rest_api.api.id}" stage_name = "staging" } resource "aws_api_gateway_method" "proxies" { count = "${length(var.functions)}" rest_api_id = "${aws_api_gateway_rest_api.api.id}" resource_id = "${aws_api_gateway_resource.proxy.id}" http_method = "${lookup(var.methods, element(var.functions, count.index))}" authorization = "NONE" } resource "aws_api_gateway_integration" "integrations" { count = "${length(var.functions)}" rest_api_id = "${aws_api_gateway_rest_api.api.id}" resource_id = "${element(aws_api_gateway_method.proxies.*.resource_id, count.index)}" http_method = "${element(aws_api_gateway_method.proxies.*.http_method, count.index)}" integration_http_method = "POST" type = "AWS_PROXY" uri = "${element(aws_lambda_function.functions.*.invoke_arn, count.index)}" } resource "aws_lambda_permission" "permissions" { count = "${length(var.functions)}" statement_id = "AllowAPIGatewayInvoke" action = "lambda:InvokeFunction" function_name = "${element(aws_lambda_function.functions.*.arn, count.index)}" principal = "apigateway.amazonaws.com" source_arn = "${aws_api_gateway_deployment.staging.execution_arn}/*/*" }