To refresh your understanding of Terraform modules, take a look at the Terraform S3 Website exercise.
The application in this repository is a "mock" bank application that features various endpoints for common banking transactions. In this exercise, you will learn how to instrument a Spring Boot application with business metrics. By business metrics, we refer to metrics that do not pertain to the technical performance of the application, but rather to insights into user behavior, such as the amount of money transferred and the number of transactions performed. This exercise utilizes Java, Spring Boot, and a metrics framework called Micrometer. We will also explore how to visualize metrics in AWS CloudWatch and how to use Terraform to create a dashboard.
- Fork this repository to your own GitHub account by clicking the "Fork" button at the top right of the repository page
- Once forked, navigate to your forked repository
- Click the green "Code" button and select "Codespaces"
- Click "Create codespace on main" to launch a new Codespaces environment
GitHub Codespaces will automatically set up your development environment with all necessary tools including:
- Java 17
- Maven
- Terraform
- AWS CLI
- jq
The setup process may take a few minutes the first time you create the codespace. Once complete, you'll have a fully configured environment ready to go!
The devcontainer should have already installed all necessary tools. You can verify the installations by running:
java -version # Should show Java 17
mvn -version # Should show Maven
terraform --version
aws --version
jq --versionBefore you can use AWS services from your Codespace, you need to create access keys for your IAM user:
-
Find Your IAM User:
- You already have an IAM user named
seat+ a number (e.g.,seat01,seat02) - This user already has the necessary permissions
- You already have an IAM user named
-
Create Access Keys:
- Go to AWS Console → IAM → Users
- Find and click on your user (e.g.,
seat01) - Go to "Security credentials" tab
- Scroll to "Access keys" section
- Click "Create access key"
- Choose "Application running outside AWS"
- Click "Next" and "Create access key"
- Important: Save both the Access Key ID and Secret Access Key somewhere safe - you won't see the secret again!
- Keep these credentials handy - you'll need them later for the optional GitHub Actions exercise
Now configure the AWS CLI with your credentials:
aws configureWhen prompted, enter:
- AWS Access Key ID: (paste the Access Key ID you just created)
- AWS Secret Access Key: (paste the Secret Access Key you just created)
- Default region name:
eu-west-1 - Default output format:
json
- You should now have this repository open in your Codespaces environment
- Look in the "infra" directory - here you will find the file dashboard.tf which contains Terraform code for a CloudWatch Dashboard.
- As you can see, the dashboard is described in a JSON format. Here you can find documentation https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/CloudWatch-Dashboard-Body-Structure.html Here you also see how one often includes text or code using "Heredoc" syntax in Terraform code, so that we don't have to worry about "newline", "Escaping" of special characters, etc. (https://developer.hashicorp.com/terraform/language/expressions/strings)
resource "aws_cloudwatch_dashboard" "main" {
dashboard_name = var.student_name
dashboard_body = <<DEATHSTAR
{
"widgets": [
{
"type": "metric",
"x": 0,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"metrics": [
[
"${var.student_name}",
"account_count.value"
]
],
"period": 300,
"stat": "Maximum",
"region": "eu-west-1",
"title": "Total number of accounts"
}
}
]
}
DEATHSTAR
}Run the following commands:
cd infra
terraform init
terraform plan
terraform apply- You have to type in a student name, why?
- Can you think of at least two ways to fix that- so that you don't have to type a student name on plan and apply?
- the Java application can be found in the
src/folder
Open BankAccountController.Java , You'll find this code
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
Gauge.builder("account_count", theBank,
b -> b.values().size()).register(meterRegistry);
}This creates a new metric - of the type Gauge, which constantly reports how many bank accounts exist in the system by counting the size of the values in the map holding the accounts.
You need to modify the MetricsConfig class and use your own name for the cloudwatch namespace, replace the empty string "" with your student name.
return new CloudWatchConfig() {
private Map<String, String> configuration = Map.of(
"cloudwatch.namespace", "",
"cloudwatch.step", Duration.ofSeconds(5).toString());
....
};From the root folder (where you cloned repository for this exercise); Start the Spring boot app with maven with
mvn spring-boot:run
The code in this repository exposes a REST interface at http://localhost:8080/account
When you run the Spring Boot application on port 8080, GitHub Codespaces will automatically detect it and forward the port. You'll see a notification in VS Code, and the port will appear in the "Ports" tab. You can:
- Click on the forwarded address to open it in your browser
- Use tools like Postman or curl from your local machine by using the forwarded URL
- The forwarded URL will look something like:
https://username-repo-xxxxx.github.dev
Curl is a command-line tool used to transfer data to or from a server, supporting a wide range of protocols including HTTP, HTTPS, FTP, and more. It is widely used for testing, sending requests, and interacting with APIs directly fro the terminal or in scripts.
Create an account with an id and balance
curl --location --request POST 'http://localhost:8080/account' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": 3,
"balance" : "100000"
}'|jq- See information about an account
curl --location --request GET 'http://localhost:8080/account/3' \
--header 'Content-Type: application/json'|jq- Transfer money from one account to another (It will create accounts if they do not exist)
curl --location --request POST 'http://localhost:8080/account/2/transfer/3' \
--header 'Content-Type: application/json' \
--data-raw '{
"fromCountry": "SE",
"toCountry" : "US",
"amount" : 500
}
'|jq- Go to the AWS UI, and select the CloudWatch service. Choose "Dashboards".
- Search for your own student name and open the dashboard you created.
- See that you get data points on the graph.
It should look something like this:
- Look at the micrometer documentation- (https://docs.spring.io/spring-boot/reference/actuator/metrics.html)
- Try to add more metrics to the code, also make new endpoints if you want more code to test
- Make sure you understand the CloudWatch dashboard Syntax, at least at a high level - try to add another widget!
You are now going to create a Micrometer Gauge that displays the net balance of the bank. How much money that is in all accounts - Place it inside the public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) method in BankAcountController class.
Please note that you have to import the BigDecimal class. When you add this code. Add the import java.math.BigDecimal line to the top of the file.
// This meter type "Gauge" reports the total amount of money in the bank
Gauge.builder("bank_sum", theBank,
b -> b.values()
.stream()
.map(Account::getBalance)
.mapToDouble(BigDecimal::doubleValue)
.sum())
.register(meterRegistry);
Extend the Terraform code in the infra folder, so that it displays an additional widget for the metric bank_sum you just created.
In the resource "aws_cloudwatch_dashboard" "main"`, in the file infra/dashboards.tf, add more code.
resource "aws_cloudwatch_dashboard" "main" {
dashboard_name = var.student_name
dashboard_body = <<DEATHSTAR
{
"widgets": [
{
<<<<<< ----- INSERT ANOTHER WIDGET HERE!
},
{
"type": "metric",
"x": 0,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"metrics": [
[
"${var.student_name}",
"account_count.value"
]
],
"period": 300,
"stat": "Maximum",
"region": "eu-west-1",
"title": "Total number of accounts"
}
}
]
}
DEATHSTAR
}
Remember to change the X/Y values so they do not overlap!
We will create an Alarm that is triggered if the total sum of the bank exceeds a given amount.
This can be done using CloudWatch. We will also create a module for this alarm, so others can benefit from it.
We will also use the service SNS, Simple Notification Service. By sending a message to an SNS topic when the alarm is triggered, we can respond to such a message, for example, by sending an email, running a lambda function, etc.
We will now create a Terraform module. While we work on it, it's smart to keep it on a local filesystem so we do not need to do git add/commit/push etc., to update the code.
- Create a new folder under infra/ called alarm_module
- In this folder, create a new Terraform file named main.tf
Take not of the fact that we're following best practices, and adding a prefix that we'll use in naming resources. This way, more than one instance of this module can be used in same AWS Environment
resource "aws_cloudwatch_metric_alarm" "threshold" {
alarm_name = "${var.prefix}-threshold"
namespace = var.prefix
metric_name = "bank_sum.value"
comparison_operator = "GreaterThanThreshold"
threshold = var.threshold
evaluation_periods = "2"
period = "60"
statistic = "Maximum"
alarm_description = "This alarm goes off as soon as the total amount of money in the bank exceeds an amount."
alarm_actions = [aws_sns_topic.user_updates.arn]
}
resource "aws_sns_topic" "user_updates" {
name = "${var.prefix}-alarm-topic"
}
resource "aws_sns_topic_subscription" "user_updates_sqs_target" {
topic_arn = aws_sns_topic.user_updates.arn
protocol = "email"
endpoint = var.alarm_email
}- Namespace is typically your student name. It's the same value that you changed in the MetricsConfig.java file.
- There are a wide range of
comparison_operatoroptions to choose from! evaluation_periodsandperiodwork together to avoid the alarm being triggered by short-term "spikes" or outlier observations.statisticis an operation that is performed on all values within a time interval given byperiod- for aGaugemetric, in this case, we choose Maximum.- Notice how one
resourcerefers to another in Terraform! - Terraform creates both an SNS Topic and an email subscription.
This will be the variables available in the module, as you can see, we make a sensible default for threshold, but require the user of the module of specify email and prefix
variable "threshold" {
default = "50"
type = string
}
variable "alarm_email" {
type = string
}
variable "prefix" {
type = string
}
output "alarm_arn" {
value = aws_sns_topic.user_updates.arn
}
You can now modify dashboard.tf in the /infra directory to include your module. It will then look like this:
resource "aws_cloudwatch_dashboard" "main" {
dashboard_name = var.student_name
dashboard_body = <<DASHBOARD
{
"widgets": [
{
"type": "metric",
"x": 0,
"y": 0,
"width": 12,
"height": 6,
"properties": {
"metrics": [
[
"${var.student_name}",
"account_count.value"
]
],
"period": 300,
"stat": "Maximum",
"region": "eu-west-1",
"title": "Total number of accounts"
}
}
]
}
DASHBOARD
}
module "alarm" { << ----------- THIS IS ADDED!
source = "./alarm_module"
alarm_email = var.alarm_email
prefix = var.student_name
}
When we run terraform frmo the main folder - we want the user to specify the email recipient for the alarm. do not add add your student email in the variable like this!
variable "glenn.bech@gmail.com" {
type = string
}The name of the variable is alarm_email, the value is your email address and will be provided as a value when terraform run. You can Feel free to set your own email address as the default value for this variable
variable "alarm_email" {
type = string
}Run the following commands:
cd infra
terraform init
terraform applyFor SNS to be allowed to send you emails, you must confirm your email address. You will receive an email with a link you must click the first time you run terraform apply.
- Go to the AWS console
- Go to SNS
- From the left menu, select "Topics"
- Find your own Topic
- Test sending an email by pressing "Publish message" at the top right of the page
Try to create new accounts, or a new account, so that the bank's total sum exceeds 1 MNOK.
curl --location --request POST 'http://localhost:8080/account' \
--header 'Content-Type: application/json' \
--data-raw '{
"id": 999,
"balance" : "5000000"
}'|jq
- Check that the alarm goes off by seeing that you have received an email.
- Go to CloudWatch Alarms in AWS and see that the alarm's state is
IN_ALARM. - Get the bank's balance back to 0, for example by creating an account with a negative balance.
- See that the alarm's state moves away from
IN_ALARM.
Want more practice? Try these challenges to deepen your understanding:
Add a widget to your CloudWatch dashboard to visualize the transfer counter metric that already exists in the code.
Hints:
- Look at the existing widget in
dashboard.tfas a template - The metric name is
transfer - Remember to adjust x/y coordinates so widgets don't overlap
- Counters work well with the "Sum" statistic
Create a histogram metric to track the distribution of transaction amounts and visualize percentiles (P50, P90, P99).
Hints:
- Use Micrometer's
TimerorDistributionSummary - Look at the
@Timedannotation already used in the code - Percentiles help you understand outliers vs typical transactions
- CloudWatch can display multiple percentiles in one widget
Implement a new /convert endpoint that converts amounts between currencies and tracks metrics by currency pair.
Hints:
- Add tags to your metrics:
.tag("fromCurrency", from).tag("toCurrency", to) - Use a simple fixed exchange rate (e.g., 1 USD = 10 NOK)
- Track conversion volume by currency pair
- Create a dashboard widget that shows conversions grouped by currency
Based on previous labs and what you have learned about Terraform state, you can optionally create a GitHub Actions workflow that applies the infrastructure automatically on pushes to the main branch.
Need inspiration? You can peek at a working example pipeline in the Terraform S3 Website repository - Part 2.
You already created AWS access keys earlier in this exercise. Now you need to add them to GitHub Secrets:
- Go to your GitHub repository
- Click Settings → Secrets and variables → Actions
- Click "New repository secret"
- Add two secrets:
- Name:
AWS_ACCESS_KEY_ID, Value: (the Access Key ID you saved earlier) - Name:
AWS_SECRET_ACCESS_KEY, Value: (the Secret Access Key you saved earlier)
- Name:
- Create a
.github/workflowsdirectory with a Terraform workflow file - Configure the workflow to run
terraform planon pull requests and add plan output as a PR comment - Configure the workflow to run
terraform applyon pushes to main branch - You must configure Terraform state properly using AWS S3 as a backend (with DynamoDB state locking
- Use the GitHub Secrets you created above for AWS credentials

