Skip to content

Commit d8150b4

Browse files
committed
Add first pass at access logging.
1 parent 57bd432 commit d8150b4

File tree

10 files changed

+235
-7
lines changed

10 files changed

+235
-7
lines changed

modules/stage/log_group.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
resource "aws_cloudwatch_log_group" "access_logs" {
2+
name = "/${var.component}/${var.deployment_identifier}/api-gateway/${var.name}"
3+
}

modules/stage/outputs.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,10 @@ output "domain_name_arn" {
2121
output "domain_name_configuration" {
2222
value = try(aws_apigatewayv2_domain_name.domain_name[0].domain_name_configuration[0], {})
2323
}
24+
25+
output "access_logging_log_group_arn" {
26+
value = aws_cloudwatch_log_group.access_logs.arn
27+
}
28+
output "access_logging_log_group_name" {
29+
value = aws_cloudwatch_log_group.access_logs.name
30+
}

modules/stage/stage.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,9 @@ resource "aws_apigatewayv2_stage" "stage" {
55
auto_deploy = local.enable_auto_deploy
66

77
tags = local.tags
8+
9+
access_log_settings {
10+
destination_arn = aws_cloudwatch_log_group.access_logs.arn
11+
format = var.access_logging_log_format
12+
}
813
}

modules/stage/variables.tf

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,34 @@ variable "domain_name_certificate_arn" {
3333
default = ""
3434
}
3535

36+
variable "access_logging_log_format" {
37+
type = string
38+
description = "The log format to use for access logging."
39+
default = "{\"accountId\": \"$context.accountId\", \"apiId\": \"$context.apiId\", \"authorizer.claims.property\": \"$context.authorizer.claims.property\", \"authorizer.error\": \"$context.authorizer.error\", \"authorizer.principalId\": \"$context.authorizer.principalId\", \"authorizer.property\": \"$context.authorizer.property\", \"awsEndpointRequestId\": \"$context.awsEndpointRequestId\", \"awsEndpointRequestId2\": \"$context.awsEndpointRequestId2\", \"customDomain.basePathMatched\": \"$context.customDomain.basePathMatched\", \"dataProcessed\": $context.dataProcessed, \"domainName\": \"$context.domainName\", \"domainPrefix\": \"$context.domainPrefix\", \"error.message\": \"$context.error.message\", \"error.messageString\": \"$context.error.messageString\", \"error.responseType\": \"$context.error.responseType\", \"extendedRequestId\": \"$context.extendedRequestId\", \"httpMethod\": \"$context.httpMethod\", \"identity.accountId\": \"$context.identity.accountId\", \"identity.caller\": \"$context.identity.caller\", \"identity.cognitoAuthenticationProvider\": \"$context.identity.cognitoAuthenticationProvider\", \"identity.cognitoAuthenticationType\": \"$context.identity.cognitoAuthenticationType\", \"identity.cognitoIdentityId\": \"$context.identity.cognitoIdentityId\", \"identity.cognitoIdentityPoolId\": \"$context.identity.cognitoIdentityPoolId\", \"identity.principalOrgId\": \"$context.identity.principalOrgId\", \"identity.clientCert.clientCertPem\": \"$context.identity.clientCert.clientCertPem\", \"identity.clientCert.subjectDN\": \"$context.identity.clientCert.subjectDN\", \"identity.clientCert.issuerDN\": \"$context.identity.clientCert.issuerDN\", \"identity.clientCert.serialNumber\": \"$context.identity.clientCert.serialNumber\", \"identity.clientCert.validity.notBefore\": \"$context.identity.clientCert.validity.notBefore\", \"identity.clientCert.validity.notAfter\": \"$context.identity.clientCert.validity.notAfter\", \"identity.sourceIp\": \"$context.identity.sourceIp\", \"identity.user\": \"$context.identity.user\", \"identity.userAgent\": \"$context.identity.userAgent\", \"identity.userArn\": \"$context.identity.userArn\", \"integration.error\": \"$context.integration.error\", \"integration.integrationStatus\": $context.integration.integrationStatus, \"integration.latency\": $context.integration.latency, \"integration.requestId\": \"$context.integration.requestId\", \"integration.status\": $context.integration.status, \"integrationErrorMessage\": \"$context.integrationErrorMessage\", \"integrationLatency\": $context.integrationLatency, \"integrationStatus\": $context.integrationStatus, \"path\": \"$context.path\", \"protocol\": \"$context.protocol\", \"requestId\": \"$context.requestId\", \"requestTime\": \"$context.requestTime\", \"requestTimeEpoch\": \"$context.requestTimeEpoch\", \"responseLatency\": $context.responseLatency, \"responseLength\": $context.responseLength, \"routeKey\": \"$context.routeKey\", \"stage\": \"$context.stage\", \"status\": $context.status}"
40+
}
41+
variable "access_logging_log_group_arn" {
42+
type = string
43+
description = "The ARN of the CloudWatch log group to use for access logging. Required when `include_access_log_log_group` is `false`."
44+
default = ""
45+
}
46+
3647
variable "tags" {
3748
type = map(string)
3849
description = "Additional tags to set on created resources."
3950
default = {}
4051
}
4152

53+
variable "enable_auto_deploy" {
54+
type = bool
55+
description = "Whether or not to enable auto-deploy for the created stage. Defaults to `true`."
56+
default = true
57+
}
58+
variable "enable_access_logging" {
59+
type = bool
60+
description = "Whether or not to enable access logging for the created stage. Defaults to `true`."
61+
default = true
62+
}
63+
4264
variable "include_default_tags" {
4365
type = bool
4466
description = "Whether or not to include default tags on created resources. Defaults to `true`."
@@ -54,8 +76,8 @@ variable "include_dns_record" {
5476
description = "Whether or not to create a DNS record in Route 53 for the domain name of the stage. Defaults to `true`."
5577
default = true
5678
}
57-
variable "enable_auto_deploy" {
79+
variable "include_access_logging_log_group" {
5880
type = bool
59-
description = "Whether or not to enable auto-deploy for the created stage. Defaults to `true`."
81+
description = "Whether or not to create a log group for access logging for the created stage. Defaults to `true`."
6082
default = true
6183
}

spec/infra/prerequisites/main.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,7 @@ resource "aws_apigatewayv2_vpc_link" "vpc_link" {
5050
security_group_ids = [aws_security_group.vpc_link.id]
5151
subnet_ids = module.base_networking.private_subnet_ids
5252
}
53+
54+
resource "aws_cloudwatch_log_group" "log_group" {
55+
name = "provided-log-group-${var.component}-${var.deployment_identifier}"
56+
}

spec/infra/prerequisites/outputs.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ output "api_id" {
1313
output "vpc_link_security_group_id" {
1414
value = aws_security_group.vpc_link.id
1515
}
16+
output "log_group_arn" {
17+
value = aws_cloudwatch_log_group.log_group.arn
18+
}

spec/infra/stage/main.tf

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ module "stage" {
2121

2222
hosted_zone_id = var.hosted_zone_id
2323

24+
access_logging_log_group_arn = var.access_logging_log_group_arn
25+
2426
tags = var.tags
2527

26-
include_default_tags = var.include_default_tags
27-
include_domain_name = var.include_domain_name
28-
include_dns_record = var.include_dns_record
29-
enable_auto_deploy = var.enable_auto_deploy
28+
enable_auto_deploy = var.enable_auto_deploy
29+
30+
include_default_tags = var.include_default_tags
31+
include_domain_name = var.include_domain_name
32+
include_dns_record = var.include_dns_record
33+
include_access_logging_log_group = var.include_access_logging_log_group
3034

3135
providers = {
3236
aws = aws

spec/infra/stage/outputs.tf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ output "domain_name_arn" {
2121
output "domain_name_configuration" {
2222
value = module.stage.domain_name_configuration
2323
}
24+
25+
output "access_logging_log_group_arn" {
26+
value = module.stage.access_logging_log_group_arn
27+
}
28+
29+
output "access_logging_log_group_name" {
30+
value = module.stage.access_logging_log_group_name
31+
}

spec/infra/stage/variables.tf

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,20 @@ variable "domain_name_certificate_arn" {
1616
default = null
1717
}
1818

19+
variable "access_logging_log_group_arn" {
20+
default = null
21+
}
22+
1923
variable "tags" {
2024
type = map(string)
2125
default = null
2226
}
2327

28+
variable "enable_auto_deploy" {
29+
type = bool
30+
default = null
31+
}
32+
2433
variable "include_default_tags" {
2534
type = bool
2635
default = null
@@ -33,7 +42,7 @@ variable "include_dns_record" {
3342
type = bool
3443
default = null
3544
}
36-
variable "enable_auto_deploy" {
45+
variable "include_access_logging_log_group" {
3746
type = bool
3847
default = null
3948
}

spec/stage_access_logging_spec.rb

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe 'stage domain name' do
6+
let(:component) { vars(:stage).component }
7+
let(:deployment_identifier) { vars(:stage).deployment_identifier }
8+
9+
let(:stage_name) { vars(:stage).name }
10+
11+
let(:api_id) do
12+
output(:prerequisites, 'api_id')
13+
end
14+
15+
let(:output_stage_id) do
16+
output(:stage, 'stage_id')
17+
end
18+
19+
let(:output_access_logging_log_group_arn) do
20+
output(:stage, 'access_logging_log_group_arn')
21+
end
22+
23+
let(:output_access_logging_log_group_name) do
24+
output(:stage, 'access_logging_log_group_name')
25+
end
26+
27+
let(:api_gateway_stages) do
28+
api_gateway_v2_client.get_stages(api_id:).items
29+
end
30+
31+
let(:api_gateway_stage) do
32+
api_gateway_stages[0]
33+
end
34+
35+
before(:context) do
36+
provision(:stage) do |vars|
37+
vars.merge(
38+
api_id: output(:prerequisites, 'api_id'),
39+
include_domain_name: false
40+
)
41+
end
42+
end
43+
44+
after(:context) do
45+
destroy(:stage) do |vars|
46+
vars.merge(
47+
api_id: output(:prerequisites, 'api_id'),
48+
include_domain_name: false
49+
)
50+
end
51+
end
52+
53+
describe 'by default' do
54+
it 'enables access logging' do
55+
expect(api_gateway_stage.access_log_settings).not_to(be_nil)
56+
end
57+
58+
it 'creates a log group for the access logs' do
59+
expect(cloudwatch_logs(output_access_logging_log_group_name)).to(exist)
60+
end
61+
62+
it 'includes the component in the log group name' do
63+
expect(output_access_logging_log_group_name)
64+
.to(match(/.*#{component}.*/))
65+
end
66+
67+
it 'includes the deployment identifier in the log group name' do
68+
expect(output_access_logging_log_group_name)
69+
.to(match(/.*#{deployment_identifier}.*/))
70+
end
71+
72+
it 'includes the stage name in the log group name' do
73+
expect(output_access_logging_log_group_name)
74+
.to(match(/.*#{stage_name}.*/))
75+
end
76+
77+
it 'uses the created log group to store access logs' do
78+
expect(api_gateway_stage.access_log_settings.destination_arn)
79+
.to(eq(output_access_logging_log_group_arn))
80+
end
81+
82+
# rubocop:disable RSpec/ExampleLength
83+
it 'logs everything' do
84+
expect(api_gateway_stage.access_log_settings.format)
85+
.to(eq(
86+
'{' \
87+
'"accountId": "$context.accountId", ' \
88+
'"apiId": "$context.apiId", ' \
89+
'"authorizer.claims.property": ' \
90+
'"$context.authorizer.claims.property", ' \
91+
'"authorizer.error": "$context.authorizer.error", ' \
92+
'"authorizer.principalId": ' \
93+
'"$context.authorizer.principalId", ' \
94+
'"authorizer.property": "$context.authorizer.property", ' \
95+
'"awsEndpointRequestId": ' \
96+
'"$context.awsEndpointRequestId", ' \
97+
'"awsEndpointRequestId2": ' \
98+
'"$context.awsEndpointRequestId2", ' \
99+
'"customDomain.basePathMatched": ' \
100+
'"$context.customDomain.basePathMatched", ' \
101+
'"dataProcessed": $context.dataProcessed, ' \
102+
'"domainName": "$context.domainName", ' \
103+
'"domainPrefix": "$context.domainPrefix", ' \
104+
'"error.message": "$context.error.message", ' \
105+
'"error.messageString": "$context.error.messageString", ' \
106+
'"error.responseType": "$context.error.responseType", ' \
107+
'"extendedRequestId": "$context.extendedRequestId", ' \
108+
'"httpMethod": "$context.httpMethod", ' \
109+
'"identity.accountId": "$context.identity.accountId", ' \
110+
'"identity.caller": "$context.identity.caller", ' \
111+
'"identity.cognitoAuthenticationProvider": ' \
112+
'"$context.identity.cognitoAuthenticationProvider", ' \
113+
'"identity.cognitoAuthenticationType": ' \
114+
'"$context.identity.cognitoAuthenticationType", ' \
115+
'"identity.cognitoIdentityId": ' \
116+
'"$context.identity.cognitoIdentityId", ' \
117+
'"identity.cognitoIdentityPoolId": ' \
118+
'"$context.identity.cognitoIdentityPoolId", ' \
119+
'"identity.principalOrgId": ' \
120+
'"$context.identity.principalOrgId", ' \
121+
'"identity.clientCert.clientCertPem": ' \
122+
'"$context.identity.clientCert.clientCertPem", ' \
123+
'"identity.clientCert.subjectDN": ' \
124+
'"$context.identity.clientCert.subjectDN", ' \
125+
'"identity.clientCert.issuerDN": ' \
126+
'"$context.identity.clientCert.issuerDN", ' \
127+
'"identity.clientCert.serialNumber": ' \
128+
'"$context.identity.clientCert.serialNumber", ' \
129+
'"identity.clientCert.validity.notBefore": ' \
130+
'"$context.identity.clientCert.validity.notBefore", ' \
131+
'"identity.clientCert.validity.notAfter": ' \
132+
'"$context.identity.clientCert.validity.notAfter", ' \
133+
'"identity.sourceIp": "$context.identity.sourceIp", ' \
134+
'"identity.user": "$context.identity.user", ' \
135+
'"identity.userAgent": "$context.identity.userAgent", ' \
136+
'"identity.userArn": "$context.identity.userArn", ' \
137+
'"integration.error": "$context.integration.error", ' \
138+
'"integration.integrationStatus": ' \
139+
'$context.integration.integrationStatus, ' \
140+
'"integration.latency": $context.integration.latency, ' \
141+
'"integration.requestId": ' \
142+
'"$context.integration.requestId", ' \
143+
'"integration.status": $context.integration.status, ' \
144+
'"integrationErrorMessage": ' \
145+
'"$context.integrationErrorMessage", ' \
146+
'"integrationLatency": $context.integrationLatency, ' \
147+
'"integrationStatus": $context.integrationStatus, ' \
148+
'"path": "$context.path", ' \
149+
'"protocol": "$context.protocol", ' \
150+
'"requestId": "$context.requestId", ' \
151+
'"requestTime": "$context.requestTime", ' \
152+
'"requestTimeEpoch": "$context.requestTimeEpoch", ' \
153+
'"responseLatency": $context.responseLatency, ' \
154+
'"responseLength": $context.responseLength, ' \
155+
'"routeKey": "$context.routeKey", ' \
156+
'"stage": "$context.stage", ' \
157+
'"status": $context.status' \
158+
'}'
159+
))
160+
end
161+
# rubocop:enable RSpec/ExampleLength
162+
end
163+
end

0 commit comments

Comments
 (0)