diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml index 73ae67553..79945f237 100644 --- a/.github/workflows/pr_build.yml +++ b/.github/workflows/pr_build.yml @@ -53,6 +53,9 @@ jobs: env: JAVA: ${{ matrix.java }} AWS_REGION: eu-west-1 + permissions: + id-token: write # needed to interact with GitHub's OIDC Token endpoint. + contents: read steps: - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - name: Setup java @@ -68,6 +71,32 @@ jobs: run: | cd examples/powertools-examples-core/gradle ./gradlew build + - name: Setup Terraform + if: ${{ matrix.java == '11' }} + uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 #v2.0.3 + - name: Setup AWS credentials + uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN_TO_ASSUME }} + aws-region: ${{ env.AWS_REGION }} + - name: Terraform validate + if: ${{ matrix.java == '11' }} + run: | + terraform -version + cd examples/powertools-examples-core/terraform + terraform init -backend=false + terraform validate + terraform plan + - name: Setup Terraform lint + if: ${{ matrix.java == '11' }} + uses: terraform-linters/setup-tflint@a5a1af8c6551fb10c53f1cd4ba62359f1973746f # v3.1.1 + - name: Terraform lint + if: ${{ matrix.java == '11' }} + run: | + tflint --version + cd examples/powertools-examples-core/terraform + tflint --init + tflint -f compact - name: Upload coverage to Codecov uses: codecov/codecov-action@d9f34f8cd5cb3b3eb79b3e4b5dae3a16df499a70 # v3.1.1 if: ${{ matrix.java == '11' }} # publish results once diff --git a/.gitignore b/.gitignore index b404d2cb2..6615ac729 100644 --- a/.gitignore +++ b/.gitignore @@ -108,4 +108,6 @@ example/HelloWorldFunction/build /example/.gradle/ /example/.java-version .gradle -build/ \ No newline at end of file +build/ +.terraform* +terraform.tfstate* \ No newline at end of file diff --git a/examples/pom.xml b/examples/pom.xml index 810ec1b36..900f095f8 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -33,6 +33,7 @@ powertools-examples-core/cdk/app powertools-examples-core/cdk/infra powertools-examples-core/serverless + powertools-examples-core/terraform powertools-examples-idempotency powertools-examples-parameters powertools-examples-serialization diff --git a/examples/powertools-examples-core/README.md b/examples/powertools-examples-core/README.md index 1d1dd031f..a0ffb125d 100644 --- a/examples/powertools-examples-core/README.md +++ b/examples/powertools-examples-core/README.md @@ -10,6 +10,7 @@ We provide examples for the following infrastructure-as-code tools: * [AWS SAM](sam/) * [AWS CDK](cdk/) * [Serverless framework](serverless/) +* [Terraform](terraform/) We also provide an example showing the integration of SAM, Powertools, and Gradle: diff --git a/examples/powertools-examples-core/terraform/.tflint.hcl b/examples/powertools-examples-core/terraform/.tflint.hcl new file mode 100644 index 000000000..18e69352b --- /dev/null +++ b/examples/powertools-examples-core/terraform/.tflint.hcl @@ -0,0 +1,3 @@ +rule "terraform_required_version" { + enabled = false +} \ No newline at end of file diff --git a/examples/powertools-examples-core/terraform/README.md b/examples/powertools-examples-core/terraform/README.md new file mode 100644 index 000000000..71c78d437 --- /dev/null +++ b/examples/powertools-examples-core/terraform/README.md @@ -0,0 +1,27 @@ +# Powertools for AWS Lambda (Java) - Core Utilities Example with Terraform + +This project demonstrates the Lambda for Powertools Java module deployed using [Terraform](https://www.terraform.io/). +For general information on the deployed example itself, you can refer to the parent [README](../README.md). +To install Terraform if you don't have it yet, you can follow the [Install Terraform Guide](https://developer.hashicorp.com/terraform/downloads?product_intent=terraform). + +## Configuration +Terraform uses [main.tf](./main.tf) to define the application's AWS resources. +This file defines the Lambda function to be deployed as well as API Gateway for it. + +It is a [Maven](https://maven.apache.org/) based project, so you can open this project with any Maven compatible Java IDE to build and run tests. + + +## Deploy the sample application + +To deploy the app, simply run the following commands: +```bash +terraform init +mvn package && terraform apply +``` + +## Useful commands + +To destroy the app +```bash +terraform destroy +``` diff --git a/examples/powertools-examples-core/terraform/infra/api-gateway.tf b/examples/powertools-examples-core/terraform/infra/api-gateway.tf new file mode 100644 index 000000000..dba1c3616 --- /dev/null +++ b/examples/powertools-examples-core/terraform/infra/api-gateway.tf @@ -0,0 +1,94 @@ +resource "aws_api_gateway_rest_api" "hello_world_api" { + name = "hello_world_api" + description = "API Gateway endpoint URL for Prod stage for Hello World function" +} + +resource "aws_api_gateway_resource" "hello_resource" { + rest_api_id = "${aws_api_gateway_rest_api.hello_world_api.id}" + parent_id = "${aws_api_gateway_rest_api.hello_world_api.root_resource_id}" + path_part = "hello" +} + +resource "aws_api_gateway_resource" "hello_stream_resource" { + rest_api_id = "${aws_api_gateway_rest_api.hello_world_api.id}" + parent_id = "${aws_api_gateway_rest_api.hello_world_api.root_resource_id}" + path_part = "hellostream" +} + +resource "aws_api_gateway_method" "hello_get_method" { + rest_api_id = "${aws_api_gateway_rest_api.hello_world_api.id}" + resource_id = "${aws_api_gateway_resource.hello_resource.id}" + http_method = "GET" + authorization = "NONE" +} + +resource "aws_api_gateway_method" "hello_stream_get_method" { + rest_api_id = "${aws_api_gateway_rest_api.hello_world_api.id}" + resource_id = "${aws_api_gateway_resource.hello_stream_resource.id}" + http_method = "GET" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "java_lambda_integration" { + rest_api_id = "${aws_api_gateway_rest_api.hello_world_api.id}" + resource_id = "${aws_api_gateway_resource.hello_resource.id}" + http_method = "${aws_api_gateway_method.hello_get_method.http_method}" + + integration_http_method = "POST" + type = "AWS_PROXY" + uri = "${aws_lambda_function.hello_world_lambda.invoke_arn}" +} + +resource "aws_api_gateway_integration" "java_stream_lambda_integration" { + rest_api_id = "${aws_api_gateway_rest_api.hello_world_api.id}" + resource_id = "${aws_api_gateway_resource.hello_stream_resource.id}" + http_method = "${aws_api_gateway_method.hello_stream_get_method.http_method}" + + integration_http_method = "POST" + type = "AWS_PROXY" + uri = "${aws_lambda_function.hello_world_stream_lambda.invoke_arn}" +} + +resource "aws_api_gateway_deployment" "prod_deployment" { + depends_on = [aws_api_gateway_integration.java_lambda_integration, aws_api_gateway_integration.java_stream_lambda_integration] + rest_api_id = "${aws_api_gateway_rest_api.hello_world_api.id}" + stage_name = "prod" +} + +# Allows API gateway to invoke lambda +resource "aws_lambda_permission" "hello_world_lambda_invoke" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = "${aws_lambda_function.hello_world_lambda.function_name}" + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.hello_world_api.execution_arn}/${aws_api_gateway_deployment.prod_deployment.stage_name}/GET/hello" +} + +# Allows API gateway to invoke lambda +resource "aws_lambda_permission" "hello_world_lambda_testinvoke" { + statement_id = "AllowAPIGatewayTestInvoke" + action = "lambda:InvokeFunction" + function_name = "${aws_lambda_function.hello_world_lambda.function_name}" + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.hello_world_api.execution_arn}/test-invoke-stage/GET/hello" +} + +# Allows API gateway to invoke lambda +resource "aws_lambda_permission" "hello_world_stream_lambda_invoke" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = "${aws_lambda_function.hello_world_stream_lambda.function_name}" + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.hello_world_api.execution_arn}/${aws_api_gateway_deployment.prod_deployment.stage_name}/GET/hellostream" +} + +# Allows API gateway to invoke lambda +resource "aws_lambda_permission" "hello_world_stream_lambda_testinvoke" { + statement_id = "AllowAPIGatewayTestInvoke" + action = "lambda:InvokeFunction" + function_name = "${aws_lambda_function.hello_world_stream_lambda.function_name}" + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.hello_world_api.execution_arn}/test-invoke-stage/GET/hellostream" +} + +output "invoke" {value=aws_api_gateway_deployment.prod_deployment.invoke_url} \ No newline at end of file diff --git a/examples/powertools-examples-core/terraform/infra/lambda.tf b/examples/powertools-examples-core/terraform/infra/lambda.tf new file mode 100644 index 000000000..abebccf54 --- /dev/null +++ b/examples/powertools-examples-core/terraform/infra/lambda.tf @@ -0,0 +1,95 @@ +resource "aws_lambda_function" "hello_world_lambda" { + runtime = "java11" + filename = "target/helloworld-lambda.jar" + source_code_hash = filebase64sha256("target/helloworld-lambda.jar") + function_name = "hello_world_lambda" + + handler = "helloworld.App" + description = "Powertools example, deployed by Terraform" + timeout = 20 + memory_size = 512 + role = "${aws_iam_role.iam_role_for_lambda.arn}" + tracing_config { + mode = "Active" + } + depends_on = [aws_cloudwatch_log_group.log_group] +} + +resource "aws_lambda_function" "hello_world_stream_lambda" { + runtime = "java11" + filename = "target/helloworld-lambda.jar" + source_code_hash = filebase64sha256("target/helloworld-lambda.jar") + function_name = "hello_world_stream_lambda" + + handler = "helloworld.AppStream" + description = "Powertools example, deployed by Terraform" + timeout = 20 + memory_size = 512 + role = "${aws_iam_role.iam_role_for_lambda.arn}" + tracing_config { + mode = "Active" + } + depends_on = [aws_cloudwatch_log_group.log_group] +} + +# Create a log group for the lambda +resource "aws_cloudwatch_log_group" "log_group" { + name = "/aws/lambda/hello_world_lambda" +} + +# Create a log group for the lambda +resource "aws_cloudwatch_log_group" "log_group_stream" { + name = "/aws/lambda/hello_world_stream_lambda" +} + +# lambda role +resource "aws_iam_role" "iam_role_for_lambda" { + name = "lambda-invoke-role" + assume_role_policy = < + 4.0.0 + + software.amazon.lambda.examples + 1.18.0-SNAPSHOT + powertools-examples-core-terraform + jar + + Powertools for AWS Lambda (Java) library Examples - Core + + + 2.20.0 + 1.8 + 1.8 + + + + + software.amazon.lambda + powertools-tracing + ${project.version} + + + software.amazon.lambda + powertools-logging + ${project.version} + + + software.amazon.lambda + powertools-metrics + ${project.version} + + + com.amazonaws + aws-lambda-java-core + 1.2.2 + + + com.amazonaws + aws-lambda-java-events + 3.11.2 + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + + junit + junit + 4.13.2 + test + + + + + helloworld-lambda + + + dev.aspectj + aspectj-maven-plugin + 1.13.1 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-tracing + + + software.amazon.lambda + powertools-logging + + + software.amazon.lambda + powertools-metrics + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + + + + + + + + + org.apache.logging.log4j + log4j-transform-maven-shade-plugin-extensions + 0.1.0 + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + handler + + + + + + + + + jdk8 + + (,11) + + + 1.9.7 + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + + + + dev.aspectj + aspectj-maven-plugin + ${aspectj.plugin.version} + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-tracing + + + software.amazon.lambda + powertools-logging + + + software.amazon.lambda + powertools-metrics + + + + + + + compile + test-compile + + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + + + + + diff --git a/examples/powertools-examples-core/terraform/src/main/java/helloworld/App.java b/examples/powertools-examples-core/terraform/src/main/java/helloworld/App.java new file mode 100644 index 000000000..dacd7f1d4 --- /dev/null +++ b/examples/powertools-examples-core/terraform/src/main/java/helloworld/App.java @@ -0,0 +1,105 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package helloworld; + +import static software.amazon.lambda.powertools.metrics.MetricsUtils.metricsLogger; +import static software.amazon.lambda.powertools.metrics.MetricsUtils.withSingleMetric; +import static software.amazon.lambda.powertools.tracing.TracingUtils.putMetadata; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.cloudwatchlogs.emf.model.DimensionSet; +import software.amazon.cloudwatchlogs.emf.model.Unit; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.logging.LoggingUtils; +import software.amazon.lambda.powertools.metrics.Metrics; +import software.amazon.lambda.powertools.tracing.CaptureMode; +import software.amazon.lambda.powertools.tracing.Tracing; +import software.amazon.lambda.powertools.tracing.TracingUtils; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + private final static Logger log = LogManager.getLogger(App.class); + + // This is controlled by POWERTOOLS_LOGGER_SAMPLE_RATE environment variable + // @Logging(logEvent = true, samplingRate = 0.7) + // This is controlled by POWERTOOLS_METRICS_NAMESPACE environment variable + // @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + // This is controlled by POWERTOOLS_TRACER_CAPTURE_ERROR environment variable + @Tracing(captureMode = CaptureMode.RESPONSE_AND_ERROR) + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + Map headers = new HashMap<>(); + + headers.put("Content-Type", "application/json"); + headers.put("X-Custom-Header", "application/json"); + + metricsLogger().putMetric("CustomMetric1", 1, Unit.COUNT); + + withSingleMetric("CustomMetrics2", 1, Unit.COUNT, "Another", (metric) -> + { + metric.setDimensions(DimensionSet.of("AnotherService", "CustomService")); + metric.setDimensions(DimensionSet.of("AnotherService1", "CustomService1")); + }); + + LoggingUtils.appendKey("test", "willBeLogged"); + + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withHeaders(headers); + try { + final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); + log.info(pageContents); + TracingUtils.putAnnotation("Test", "New"); + String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); + + TracingUtils.withSubsegment("loggingResponse", subsegment -> + { + String sampled = "log something out"; + log.info(sampled); + log.info(output); + }); + + log.info("After output"); + return response + .withStatusCode(200) + .withBody(output); + } catch (RuntimeException | IOException e) { + return response + .withBody("{}") + .withStatusCode(500); + } + } + + @Tracing(namespace = "getPageContents", captureMode = CaptureMode.DISABLED) + private String getPageContents(String address) throws IOException { + URL url = new URL(address); + putMetadata("getPageContents", address); + try (BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines().collect(Collectors.joining(System.lineSeparator())); + } + } +} diff --git a/examples/powertools-examples-core/terraform/src/main/java/helloworld/AppStream.java b/examples/powertools-examples-core/terraform/src/main/java/helloworld/AppStream.java new file mode 100644 index 000000000..401ef8c48 --- /dev/null +++ b/examples/powertools-examples-core/terraform/src/main/java/helloworld/AppStream.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.metrics.Metrics; + +public class AppStream implements RequestStreamHandler { + private static final ObjectMapper mapper = new ObjectMapper(); + + @Override + @Logging(logEvent = true) + @Metrics(namespace = "ServerlessAirline", service = "payment", captureColdStart = true) + public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { + Map map = mapper.readValue(input, Map.class); + + System.out.println(map.size()); + } +} diff --git a/examples/powertools-examples-core/terraform/src/main/resources/log4j2.xml b/examples/powertools-examples-core/terraform/src/main/resources/log4j2.xml new file mode 100644 index 000000000..0cc0953f0 --- /dev/null +++ b/examples/powertools-examples-core/terraform/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/examples/powertools-examples-core/terraform/src/test/java/helloworld/AppTest.java b/examples/powertools-examples-core/terraform/src/test/java/helloworld/AppTest.java new file mode 100644 index 000000000..0ca4f264d --- /dev/null +++ b/examples/powertools-examples-core/terraform/src/test/java/helloworld/AppTest.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package helloworld; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.junit.Test; + +public class AppTest { + + @Test + public void successfulResponse() { + App app = new App(); + APIGatewayProxyResponseEvent result = app.handleRequest(null, null); + assertEquals(result.getStatusCode().intValue(), 200); + assertEquals(result.getHeaders().get("Content-Type"), "application/json"); + String content = result.getBody(); + assertNotNull(content); + assertTrue(content.contains("\"message\"")); + assertTrue(content.contains("\"hello world\"")); + assertTrue(content.contains("\"location\"")); + } +}