CloudTrail is the audit log for your AWS account. It records AWS account activity such as supported API calls, console sign-ins, and resource changes. Without CloudTrail, you are flying blind when it comes to security investigations, compliance audits, and understanding who did what in your environment. Setting it up properly with Terraform ensures consistent logging across your accounts and regions.
This guide walks through a complete CloudTrail implementation with Terraform, from the basic trail to advanced configurations like multi-region logging and real-time alerting.
Core Architecture Requirements
CloudTrail needs an S3 bucket to store log files.
The trail will be stored in an s3 bucket of your choice.
S3 bucket
KMS Key Management System for S3 objects encryption
CloudTrail
Prerequisite:
An AWS account with permissions to create CloudTrail resources
AWS cli configured
Terraform installed
Create the S3 Bucket for Logs
CloudTrail needs an S3 bucket to store log files
This bucket needs a specific bucket policy to allow CloudTrail to write to it.
S3 Bucket Configuration for CloudTrail Logs
The S3 bucket for CloudTrail logs is provisioned with versioning, encryption, public access blocks, and lifecycle management.
S3 bucket for CloudTrail logs
resource "awss3bucket" "cloudtraillogs" {
bucket = "${var.project}-cloudtrail-logs-${data.awscalleridentity.current.accountid}"
tags = {
Name = "CloudTrail Logs"
Environment = var.environment
ManagedBy = "terraform"
}
}
Enable versioning to prevent log tampering
resource "awss3bucketversioning" "cloudtraillogs" {
bucket = awss3bucket.cloudtraillogs.id
versioningconfiguration {
status = "Enabled"
}
}
Enable server-side encryption with KMS
resource "awss3bucketserversideencryptionconfiguration" "cloudtraillogs" {
bucket = awss3bucket.cloudtraillogs.id
rule {
applyserversideencryptionbydefault {
ssealgorithm = "aws:kms"
kmsmasterkeyid = awskmskey.cloudtrail.arn
}
bucketkey_enabled = true
}
}
Block all public access
resource "awss3bucketpublicaccessblock" "cloudtraillogs" {
bucket = awss3bucket.cloudtraillogs.id
blockpublicacls = true
blockpublicpolicy = true
ignorepublicacls = true
restrictpublic_buckets = true
}
Lifecycle policy to manage storage costs
resource "awss3bucketlifecycleconfiguration" "cloudtraillogs" {
bucket = awss3bucket.cloudtraillogs.id
rule {
id = "archive-old-logs"
status = "Enabled"
transition {
days = 90
storageclass = "GLACIER"
}
transition {
days = 365
storageclass = "DEEP_ARCHIVE"
}
Keep logs for 7 years common compliance requirement
expiration {
days = 2555
}
}
}
This creates a private, versioned, and encrypted S3 bucket that stores all CloudTrail and VPC Flow Logs. The random suffix ensures globally unique names. Logs are stored securely here and later queried by Athena.
Step 1) Create S3 Buckets for Logs and Athena Results
resource "awss3bucket" "logs" {
bucket = "athena-logs-${data.awscalleridentity.me.accountid}-${randomid.suffix.hex}"
tags = var.tags
}
Project Structure and Provider Setup
cloudtrail #this is a folder
---> providers.tf
---> s3.tf
---> main.tf
---> kms.tf
Create a new directory cloudtrail and navigate into it
mkdir cloudtrail
cd cloudtrail
providers.tf
terraform {
requiredversion = "~> 1.6"
requiredproviders {
aws = {
source = "hashicorp/aws"
}
}
}
Configure the AWS Provider
provider "aws" {
region = "eu-west-1"
default_tags {
tags = {
Environment = terraform.workspace,
ManagedBy = "Terraform"
}
}
}
Initialize your project
terraform init
A successful initialization should look like the image below.
Terraform CloudTrail Resource Definition
Copy the code below to your main.tf file
main.tf
resource "awscloudtrail" "cloudtrail" {
name = "cloudtrail-tutorial"
s3bucketname = awss3bucket.cloudtrails3.id
kmskeyid = awskmskey.cloudtrailkmskey.arn
enablelogfilevalidation = true
ismultiregiontrail = true
enablelogging = true
dependson = [
awss3bucket.cloudtrails3,
data.awsiampolicydocument.cloudtrails3policy,
awskmskey.cloudtrailkmskey
]
}
Copy the code below to your s3.tf file
s3.tf
resource "awss3bucket" "cloudtrail_s3" {
bucket =
}
This enables CloudTrail, which tracks all API activity in your AWS account — who did what, when, and from where
Every component from CloudTrail log delivery to Athena table creation is defined as Infrastructure as Code IaC in Terraform, ensuring automation, reproducibility, and cost efficiency across the entire setup.
Objective
To build a serverless AWS log analysis system that automatically:
Step 2) Enable AWS CloudTrail
resource "awscloudtrail" "trail" {
name = "org-trail-athena"
s3bucketname = awss3bucket.logs.bucket
enablelogging = true
}
Module Based Deployment Patterns
Terraform module to provision an AWS CloudTrail.
The module accepts an encrypted S3 bucket with versioning to store CloudTrail logs.
The bucket could be from the same AWS account or from a different account.
This is useful if an organization uses a number of separate AWS accounts to isolate the Audit environment from other environments production, staging, development.
In this case, you create CloudTrail in the production environment production AWS account, while the S3 bucket to store the CloudTrail logs is created in the Audit AWS account, restricting access to the logs only to the users/groups from the Audit account.
Tip
module "cloudtrail" {
source = "cloudposse/cloudtrail/aws"
Cloud Posse recommends pinning every module to a specific version
version = "x.x.x"
namespace = "eg"
stage = "dev"
name = "cluster"
enablelogfilevalidation = true
includeglobalserviceevents = true
ismultiregiontrail = false
enablelogging = true
s3bucketname = "my-cloudtrail-logs-bucket"
}
NOTE: To create an S3 bucket for CloudTrail logs, use terraform-aws-cloudtrail-s3-bucket module
Business Scenario
Our company increased its use of AWS services across multiple accounts and environments, and leadership has identified significant challenges in meeting security, compliance, and audit requirements. To reduce this pain point, our company or security department needs a secure, automated, and repeatable solution that will collect API activity logs for every AWS account and region. More so, store those logs centrally, with encryption and proper access controls. This is where we come in to build this out!
Prerequisities
AWS Knowledge and AWS Account
Terraform Knowledge
IDE: I used VSCode
Curiosity and Determination ALWAYS
Step 1: Setting up our files
For this project, we want to setup the directory for it. That includes the module folder and files that are created for this project.
From here, I will explain the files and the purpose of each section within those files. Let’s start with the main.tf file
Main.tf
For our root main.tf file, we are setting up our module block, which is passing in a set of variables that Terraform will be calling from
module "awscloudtrail" {
source = "trussworks/cloudtrail/aws"
s3bucketname = "my-company-cloudtrail-logs"
logretention_days = 90
}
CloudPosse Module Parameters
| Parameter | Description |
|---|---|
| namespace | Namespace identifier for resources |
| stage | Deployment stage e.g., dev |
| name | Logical name for the trail |
| enablelogfile_validation | Enable log file validation |
| includeglobalservice_events | Include global service events |
| ismultiregion_trail | Enable multi-region logging |
| enable_logging | Enable logging |
| s3bucketname | Name of S3 bucket for logs |
Trussworks Module Parameters
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| s3bucketname | The name of the AWS S3 bucket. | string | n/a | yes |
| advancedeventselectors | A list of advanced event selectors for the trail. | list(object({ name = string fieldselectors = list(object({ field = string equals = optional(list(string)) startswith = optional(list(string)) endswith = optional(list(string)) notequals = optional(list(string)) notstartswith = optional(list(string)) notendswith = optional(list(string)) })) })) | [] | no |
| apicallrate_insight | A measurement of write-only management API calls that occur per minute against a baseline API call volume. | bool | false | no |
| apierrorrate_insight | A measurement of management API calls that result in error codes. The error is shown if the API call is unsuccessful | bool | false | no |
Name |
Version |
terraform |
= 1.0 |
aws |
= 3.0 |
Upgrade Instructions for v2 -> v3
Starting in v3, encryption is not optional and will be on for both logs delivered to S3 and Cloudwatch Logs. The KMS key resource created this module will be used to encrypt both S3 and Cloudwatch-based logs.
Because of this change, remove the encrypt_cloudtrail parameter from previous invocations of the module prior to upgrading the version.
This module creates AWS CloudTrail and configures it so that logs go to cloudwatch.
Multi-Account and Centralized Logging
The module accepts an encrypted S3 bucket with versioning to store CloudTrail logs.
The bucket could be from the same AWS account or from a different account.
This is useful if an organization uses a number of separate AWS accounts to isolate the Audit environment from other environments production, staging, development.
In this case, you create CloudTrail in the production environment production AWS account, while the S3 bucket to store the CloudTrail logs is created in the Audit AWS account, restricting access to the logs only to the users/groups from the Audit account.
To read more on CloudTrail, please visit the documentation page.
In this technical post, we'll walk through the steps to provision CloudTrail using Terraform.
Log Analysis Integration
. Every component from CloudTrail log delivery to Athena table creation is defined as Infrastructure as Code IaC in Terraform, ensuring automation, reproducibility, and cost efficiency across the entire setup.
Objective
To build a serverless AWS log analysis system that automatically:
Step 1) Create S3 Buckets for Logs and Athena Results
resource "awss3bucket" "logs" {
bucket = "athena-logs-${data.awscalleridentity.me.accountid}-${randomid.suffix.hex}"
tags = var.tags
}
This creates a private, versioned, and encrypted S3 bucket that stores all CloudTrail and VPC Flow Logs. The random suffix ensures globally unique names. Logs are stored securely here and later queried by Athena.
Step 2) Enable AWS CloudTrail
resource "awscloudtrail" "trail" {
name = "org-trail-athena"
s3bucketname = awss3bucket.logs.bucket
enablelogging = true
}
This enables CloudTrail, which tracks all API activity in your AWS account — who did what, when, and from where
Conclusion
Terraform implementation of AWS CloudTrail centers on provisioning an S3 destination with versioning, server-side encryption via KMS, public access blocks, and lifecycle transitions, then binding the trail to that bucket with enablelogfilevalidation and ismultiregiontrail settings.
Module usage provides repeatable patterns for namespace, stage, name, enablelogfilevalidation, includeglobalserviceevents, ismultiregiontrail, enablelogging, and s3bucketname parameters, with the option to source the bucket from a separate Audit account to isolate production workloads from log storage.
For organizations requiring centralised audit, the S3 bucket can be created in an Audit AWS account while CloudTrail is created in production, restricting log access to Audit account principals. Multi-region trails ensure consistent API activity capture across regions, and lifecycle policies with Glacier and Deep Archive transitions manage compliance retention for up to seven years while controlling cost.
IaC definitions also extend to serverless log analysis where CloudTrail delivery to S3 feeds Athena queries, with the entire pipeline defined as Terraform for automation, reproducibility, and cost efficiency.
Sources
- https://oneuptime.com/blog/post/2026-02-23-how-to-implement-cloudtrail-logging-with-terraform/view
- https://github.com/cloudposse/terraform-aws-cloudtrail
- https://dev.to/aws-builders/provisioning-aws-cloudtrail-using-terraform-step-by-step-hn4
- https://aws.plainenglish.io/project-trail-using-terraform-to-deploy-aws-cloudtrail-8f60d4a48a0a
- https://www.linkedin.com/pulse/automating-aws-cloudtrail-log-analysis-athena-rajamahendram-5aahc
- https://github.com/trussworks/terraform-aws-cloudtrail