The aws:cloudtrail/trail:Trail resource, part of the Pulumi AWS provider, defines a CloudTrail trail that captures AWS API activity and delivers logs to S3 or CloudWatch Logs. This guide focuses on three capabilities: management event capture to S3, data event filtering with basic and advanced selectors, and CloudWatch Logs integration.
Trails require S3 buckets with CloudTrail-compatible policies and optionally CloudWatch Log Groups with IAM roles for delivery. The examples are intentionally small. Combine them with your own bucket policies, encryption keys, and notification targets.
Capture management events to S3
Most deployments begin by capturing management events (API calls that create, modify, or delete resources) and storing them in S3 for compliance.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const exampleBucket = new aws.s3.Bucket("example", {
bucket: "my-test-trail",
forceDestroy: true,
});
const current = aws.getCallerIdentity({});
const currentGetPartition = aws.getPartition({});
const currentGetRegion = aws.getRegion({});
const example = aws.iam.getPolicyDocumentOutput({
statements: [
{
sid: "AWSCloudTrailAclCheck",
effect: "Allow",
principals: [{
type: "Service",
identifiers: ["cloudtrail.amazonaws.com"],
}],
actions: ["s3:GetBucketAcl"],
resources: [exampleBucket.arn],
conditions: [{
test: "StringEquals",
variable: "aws:SourceArn",
values: [Promise.all([currentGetPartition, currentGetRegion, current]).then(([currentGetPartition, currentGetRegion, current]) => `arn:${currentGetPartition.partition}:cloudtrail:${currentGetRegion.region}:${current.accountId}:trail/example`)],
}],
},
{
sid: "AWSCloudTrailWrite",
effect: "Allow",
principals: [{
type: "Service",
identifiers: ["cloudtrail.amazonaws.com"],
}],
actions: ["s3:PutObject"],
resources: [Promise.all([exampleBucket.arn, current]).then(([arn, current]) => `${arn}/prefix/AWSLogs/${current.accountId}/*`)],
conditions: [
{
test: "StringEquals",
variable: "s3:x-amz-acl",
values: ["bucket-owner-full-control"],
},
{
test: "StringEquals",
variable: "aws:SourceArn",
values: [Promise.all([currentGetPartition, currentGetRegion, current]).then(([currentGetPartition, currentGetRegion, current]) => `arn:${currentGetPartition.partition}:cloudtrail:${currentGetRegion.region}:${current.accountId}:trail/example`)],
},
],
},
],
});
const exampleBucketPolicy = new aws.s3.BucketPolicy("example", {
bucket: exampleBucket.id,
policy: example.apply(example => example.json),
});
const exampleTrail = new aws.cloudtrail.Trail("example", {
name: "example",
s3BucketName: exampleBucket.id,
s3KeyPrefix: "prefix",
includeGlobalServiceEvents: false,
}, {
dependsOn: [exampleBucketPolicy],
});
import pulumi
import pulumi_aws as aws
example_bucket = aws.s3.Bucket("example",
bucket="my-test-trail",
force_destroy=True)
current = aws.get_caller_identity()
current_get_partition = aws.get_partition()
current_get_region = aws.get_region()
example = aws.iam.get_policy_document_output(statements=[
{
"sid": "AWSCloudTrailAclCheck",
"effect": "Allow",
"principals": [{
"type": "Service",
"identifiers": ["cloudtrail.amazonaws.com"],
}],
"actions": ["s3:GetBucketAcl"],
"resources": [example_bucket.arn],
"conditions": [{
"test": "StringEquals",
"variable": "aws:SourceArn",
"values": [f"arn:{current_get_partition.partition}:cloudtrail:{current_get_region.region}:{current.account_id}:trail/example"],
}],
},
{
"sid": "AWSCloudTrailWrite",
"effect": "Allow",
"principals": [{
"type": "Service",
"identifiers": ["cloudtrail.amazonaws.com"],
}],
"actions": ["s3:PutObject"],
"resources": [example_bucket.arn.apply(lambda arn: f"{arn}/prefix/AWSLogs/{current.account_id}/*")],
"conditions": [
{
"test": "StringEquals",
"variable": "s3:x-amz-acl",
"values": ["bucket-owner-full-control"],
},
{
"test": "StringEquals",
"variable": "aws:SourceArn",
"values": [f"arn:{current_get_partition.partition}:cloudtrail:{current_get_region.region}:{current.account_id}:trail/example"],
},
],
},
])
example_bucket_policy = aws.s3.BucketPolicy("example",
bucket=example_bucket.id,
policy=example.json)
example_trail = aws.cloudtrail.Trail("example",
name="example",
s3_bucket_name=example_bucket.id,
s3_key_prefix="prefix",
include_global_service_events=False,
opts = pulumi.ResourceOptions(depends_on=[example_bucket_policy]))
package main
import (
"fmt"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/cloudtrail"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
exampleBucket, err := s3.NewBucket(ctx, "example", &s3.BucketArgs{
Bucket: pulumi.String("my-test-trail"),
ForceDestroy: pulumi.Bool(true),
})
if err != nil {
return err
}
current, err := aws.GetCallerIdentity(ctx, &aws.GetCallerIdentityArgs{}, nil)
if err != nil {
return err
}
currentGetPartition, err := aws.GetPartition(ctx, &aws.GetPartitionArgs{}, nil)
if err != nil {
return err
}
currentGetRegion, err := aws.GetRegion(ctx, &aws.GetRegionArgs{}, nil)
if err != nil {
return err
}
example := iam.GetPolicyDocumentOutput(ctx, iam.GetPolicyDocumentOutputArgs{
Statements: iam.GetPolicyDocumentStatementArray{
&iam.GetPolicyDocumentStatementArgs{
Sid: pulumi.String("AWSCloudTrailAclCheck"),
Effect: pulumi.String("Allow"),
Principals: iam.GetPolicyDocumentStatementPrincipalArray{
&iam.GetPolicyDocumentStatementPrincipalArgs{
Type: pulumi.String("Service"),
Identifiers: pulumi.StringArray{
pulumi.String("cloudtrail.amazonaws.com"),
},
},
},
Actions: pulumi.StringArray{
pulumi.String("s3:GetBucketAcl"),
},
Resources: pulumi.StringArray{
exampleBucket.Arn,
},
Conditions: iam.GetPolicyDocumentStatementConditionArray{
&iam.GetPolicyDocumentStatementConditionArgs{
Test: pulumi.String("StringEquals"),
Variable: pulumi.String("aws:SourceArn"),
Values: pulumi.StringArray{
pulumi.Sprintf("arn:%v:cloudtrail:%v:%v:trail/example", currentGetPartition.Partition, currentGetRegion.Region, current.AccountId),
},
},
},
},
&iam.GetPolicyDocumentStatementArgs{
Sid: pulumi.String("AWSCloudTrailWrite"),
Effect: pulumi.String("Allow"),
Principals: iam.GetPolicyDocumentStatementPrincipalArray{
&iam.GetPolicyDocumentStatementPrincipalArgs{
Type: pulumi.String("Service"),
Identifiers: pulumi.StringArray{
pulumi.String("cloudtrail.amazonaws.com"),
},
},
},
Actions: pulumi.StringArray{
pulumi.String("s3:PutObject"),
},
Resources: pulumi.StringArray{
exampleBucket.Arn.ApplyT(func(arn string) (string, error) {
return fmt.Sprintf("%v/prefix/AWSLogs/%v/*", arn, current.AccountId), nil
}).(pulumi.StringOutput),
},
Conditions: iam.GetPolicyDocumentStatementConditionArray{
&iam.GetPolicyDocumentStatementConditionArgs{
Test: pulumi.String("StringEquals"),
Variable: pulumi.String("s3:x-amz-acl"),
Values: pulumi.StringArray{
pulumi.String("bucket-owner-full-control"),
},
},
&iam.GetPolicyDocumentStatementConditionArgs{
Test: pulumi.String("StringEquals"),
Variable: pulumi.String("aws:SourceArn"),
Values: pulumi.StringArray{
pulumi.Sprintf("arn:%v:cloudtrail:%v:%v:trail/example", currentGetPartition.Partition, currentGetRegion.Region, current.AccountId),
},
},
},
},
},
}, nil)
exampleBucketPolicy, err := s3.NewBucketPolicy(ctx, "example", &s3.BucketPolicyArgs{
Bucket: exampleBucket.ID(),
Policy: pulumi.String(example.ApplyT(func(example iam.GetPolicyDocumentResult) (*string, error) {
return &example.Json, nil
}).(pulumi.StringPtrOutput)),
})
if err != nil {
return err
}
_, err = cloudtrail.NewTrail(ctx, "example", &cloudtrail.TrailArgs{
Name: pulumi.String("example"),
S3BucketName: exampleBucket.ID(),
S3KeyPrefix: pulumi.String("prefix"),
IncludeGlobalServiceEvents: pulumi.Bool(false),
}, pulumi.DependsOn([]pulumi.Resource{
exampleBucketPolicy,
}))
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var exampleBucket = new Aws.S3.Bucket("example", new()
{
BucketName = "my-test-trail",
ForceDestroy = true,
});
var current = Aws.GetCallerIdentity.Invoke();
var currentGetPartition = Aws.GetPartition.Invoke();
var currentGetRegion = Aws.GetRegion.Invoke();
var example = Aws.Iam.GetPolicyDocument.Invoke(new()
{
Statements = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementInputArgs
{
Sid = "AWSCloudTrailAclCheck",
Effect = "Allow",
Principals = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalInputArgs
{
Type = "Service",
Identifiers = new[]
{
"cloudtrail.amazonaws.com",
},
},
},
Actions = new[]
{
"s3:GetBucketAcl",
},
Resources = new[]
{
exampleBucket.Arn,
},
Conditions = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementConditionInputArgs
{
Test = "StringEquals",
Variable = "aws:SourceArn",
Values = new[]
{
$"arn:{currentGetPartition.Apply(getPartitionResult => getPartitionResult.Partition)}:cloudtrail:{currentGetRegion.Apply(getRegionResult => getRegionResult.Region)}:{current.Apply(getCallerIdentityResult => getCallerIdentityResult.AccountId)}:trail/example",
},
},
},
},
new Aws.Iam.Inputs.GetPolicyDocumentStatementInputArgs
{
Sid = "AWSCloudTrailWrite",
Effect = "Allow",
Principals = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalInputArgs
{
Type = "Service",
Identifiers = new[]
{
"cloudtrail.amazonaws.com",
},
},
},
Actions = new[]
{
"s3:PutObject",
},
Resources = new[]
{
$"{exampleBucket.Arn}/prefix/AWSLogs/{current.Apply(getCallerIdentityResult => getCallerIdentityResult.AccountId)}/*",
},
Conditions = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementConditionInputArgs
{
Test = "StringEquals",
Variable = "s3:x-amz-acl",
Values = new[]
{
"bucket-owner-full-control",
},
},
new Aws.Iam.Inputs.GetPolicyDocumentStatementConditionInputArgs
{
Test = "StringEquals",
Variable = "aws:SourceArn",
Values = new[]
{
$"arn:{currentGetPartition.Apply(getPartitionResult => getPartitionResult.Partition)}:cloudtrail:{currentGetRegion.Apply(getRegionResult => getRegionResult.Region)}:{current.Apply(getCallerIdentityResult => getCallerIdentityResult.AccountId)}:trail/example",
},
},
},
},
},
});
var exampleBucketPolicy = new Aws.S3.BucketPolicy("example", new()
{
Bucket = exampleBucket.Id,
Policy = example.Apply(getPolicyDocumentResult => getPolicyDocumentResult.Json),
});
var exampleTrail = new Aws.CloudTrail.Trail("example", new()
{
Name = "example",
S3BucketName = exampleBucket.Id,
S3KeyPrefix = "prefix",
IncludeGlobalServiceEvents = false,
}, new CustomResourceOptions
{
DependsOn =
{
exampleBucketPolicy,
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.s3.Bucket;
import com.pulumi.aws.s3.BucketArgs;
import com.pulumi.aws.AwsFunctions;
import com.pulumi.aws.inputs.GetCallerIdentityArgs;
import com.pulumi.aws.inputs.GetPartitionArgs;
import com.pulumi.aws.inputs.GetRegionArgs;
import com.pulumi.aws.iam.IamFunctions;
import com.pulumi.aws.iam.inputs.GetPolicyDocumentArgs;
import com.pulumi.aws.s3.BucketPolicy;
import com.pulumi.aws.s3.BucketPolicyArgs;
import com.pulumi.aws.cloudtrail.Trail;
import com.pulumi.aws.cloudtrail.TrailArgs;
import com.pulumi.resources.CustomResourceOptions;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var exampleBucket = new Bucket("exampleBucket", BucketArgs.builder()
.bucket("my-test-trail")
.forceDestroy(true)
.build());
final var current = AwsFunctions.getCallerIdentity(GetCallerIdentityArgs.builder()
.build());
final var currentGetPartition = AwsFunctions.getPartition(GetPartitionArgs.builder()
.build());
final var currentGetRegion = AwsFunctions.getRegion(GetRegionArgs.builder()
.build());
final var example = IamFunctions.getPolicyDocument(GetPolicyDocumentArgs.builder()
.statements(
GetPolicyDocumentStatementArgs.builder()
.sid("AWSCloudTrailAclCheck")
.effect("Allow")
.principals(GetPolicyDocumentStatementPrincipalArgs.builder()
.type("Service")
.identifiers("cloudtrail.amazonaws.com")
.build())
.actions("s3:GetBucketAcl")
.resources(exampleBucket.arn())
.conditions(GetPolicyDocumentStatementConditionArgs.builder()
.test("StringEquals")
.variable("aws:SourceArn")
.values(String.format("arn:%s:cloudtrail:%s:%s:trail/example", currentGetPartition.partition(),currentGetRegion.region(),current.accountId()))
.build())
.build(),
GetPolicyDocumentStatementArgs.builder()
.sid("AWSCloudTrailWrite")
.effect("Allow")
.principals(GetPolicyDocumentStatementPrincipalArgs.builder()
.type("Service")
.identifiers("cloudtrail.amazonaws.com")
.build())
.actions("s3:PutObject")
.resources(exampleBucket.arn().applyValue(_arn -> String.format("%s/prefix/AWSLogs/%s/*", _arn,current.accountId())))
.conditions(
GetPolicyDocumentStatementConditionArgs.builder()
.test("StringEquals")
.variable("s3:x-amz-acl")
.values("bucket-owner-full-control")
.build(),
GetPolicyDocumentStatementConditionArgs.builder()
.test("StringEquals")
.variable("aws:SourceArn")
.values(String.format("arn:%s:cloudtrail:%s:%s:trail/example", currentGetPartition.partition(),currentGetRegion.region(),current.accountId()))
.build())
.build())
.build());
var exampleBucketPolicy = new BucketPolicy("exampleBucketPolicy", BucketPolicyArgs.builder()
.bucket(exampleBucket.id())
.policy(example.applyValue(_example -> _example.json()))
.build());
var exampleTrail = new Trail("exampleTrail", TrailArgs.builder()
.name("example")
.s3BucketName(exampleBucket.id())
.s3KeyPrefix("prefix")
.includeGlobalServiceEvents(false)
.build(), CustomResourceOptions.builder()
.dependsOn(exampleBucketPolicy)
.build());
}
}
resources:
exampleTrail:
type: aws:cloudtrail:Trail
name: example
properties:
name: example
s3BucketName: ${exampleBucket.id}
s3KeyPrefix: prefix
includeGlobalServiceEvents: false
options:
dependsOn:
- ${exampleBucketPolicy}
exampleBucket:
type: aws:s3:Bucket
name: example
properties:
bucket: my-test-trail
forceDestroy: true
exampleBucketPolicy:
type: aws:s3:BucketPolicy
name: example
properties:
bucket: ${exampleBucket.id}
policy: ${example.json}
variables:
example:
fn::invoke:
function: aws:iam:getPolicyDocument
arguments:
statements:
- sid: AWSCloudTrailAclCheck
effect: Allow
principals:
- type: Service
identifiers:
- cloudtrail.amazonaws.com
actions:
- s3:GetBucketAcl
resources:
- ${exampleBucket.arn}
conditions:
- test: StringEquals
variable: aws:SourceArn
values:
- arn:${currentGetPartition.partition}:cloudtrail:${currentGetRegion.region}:${current.accountId}:trail/example
- sid: AWSCloudTrailWrite
effect: Allow
principals:
- type: Service
identifiers:
- cloudtrail.amazonaws.com
actions:
- s3:PutObject
resources:
- ${exampleBucket.arn}/prefix/AWSLogs/${current.accountId}/*
conditions:
- test: StringEquals
variable: s3:x-amz-acl
values:
- bucket-owner-full-control
- test: StringEquals
variable: aws:SourceArn
values:
- arn:${currentGetPartition.partition}:cloudtrail:${currentGetRegion.region}:${current.accountId}:trail/example
current:
fn::invoke:
function: aws:getCallerIdentity
arguments: {}
currentGetPartition:
fn::invoke:
function: aws:getPartition
arguments: {}
currentGetRegion:
fn::invoke:
function: aws:getRegion
arguments: {}
CloudTrail writes logs to the S3 bucket specified in s3BucketName, organizing them under the s3KeyPrefix path. The bucket policy grants CloudTrail permission to check ACLs and write objects. The includeGlobalServiceEvents property controls whether IAM and other global service events are captured; set it to false if you only need regional events.
Log Lambda invocations with basic event selectors
Security teams often need visibility into Lambda function executions to track who invoked functions and what data they accessed.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const example = new aws.cloudtrail.Trail("example", {eventSelectors: [{
readWriteType: "All",
includeManagementEvents: true,
dataResources: [{
type: "AWS::Lambda::Function",
values: ["arn:aws:lambda"],
}],
}]});
import pulumi
import pulumi_aws as aws
example = aws.cloudtrail.Trail("example", event_selectors=[{
"read_write_type": "All",
"include_management_events": True,
"data_resources": [{
"type": "AWS::Lambda::Function",
"values": ["arn:aws:lambda"],
}],
}])
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/cloudtrail"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := cloudtrail.NewTrail(ctx, "example", &cloudtrail.TrailArgs{
EventSelectors: cloudtrail.TrailEventSelectorArray{
&cloudtrail.TrailEventSelectorArgs{
ReadWriteType: pulumi.String("All"),
IncludeManagementEvents: pulumi.Bool(true),
DataResources: cloudtrail.TrailEventSelectorDataResourceArray{
&cloudtrail.TrailEventSelectorDataResourceArgs{
Type: pulumi.String("AWS::Lambda::Function"),
Values: pulumi.StringArray{
pulumi.String("arn:aws:lambda"),
},
},
},
},
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var example = new Aws.CloudTrail.Trail("example", new()
{
EventSelectors = new[]
{
new Aws.CloudTrail.Inputs.TrailEventSelectorArgs
{
ReadWriteType = "All",
IncludeManagementEvents = true,
DataResources = new[]
{
new Aws.CloudTrail.Inputs.TrailEventSelectorDataResourceArgs
{
Type = "AWS::Lambda::Function",
Values = new[]
{
"arn:aws:lambda",
},
},
},
},
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.cloudtrail.Trail;
import com.pulumi.aws.cloudtrail.TrailArgs;
import com.pulumi.aws.cloudtrail.inputs.TrailEventSelectorArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var example = new Trail("example", TrailArgs.builder()
.eventSelectors(TrailEventSelectorArgs.builder()
.readWriteType("All")
.includeManagementEvents(true)
.dataResources(TrailEventSelectorDataResourceArgs.builder()
.type("AWS::Lambda::Function")
.values("arn:aws:lambda")
.build())
.build())
.build());
}
}
resources:
example:
type: aws:cloudtrail:Trail
properties:
eventSelectors:
- readWriteType: All
includeManagementEvents: true
dataResources:
- type: AWS::Lambda::Function
values:
- arn:aws:lambda
The eventSelectors property enables data event logging. Each selector specifies a readWriteType (All, ReadOnly, or WriteOnly) and dataResources that define which service events to capture. The type “AWS::Lambda::Function” with value “arn:aws:lambda” captures all Lambda invocations across the account.
Filter S3 events with advanced event selectors
Advanced event selectors provide fine-grained control over which data events to capture, allowing you to exclude noisy buckets while monitoring everything else.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const not_important_bucket_1 = aws.s3.getBucket({
bucket: "not-important-bucket-1",
});
const not_important_bucket_2 = aws.s3.getBucket({
bucket: "not-important-bucket-2",
});
const example = new aws.cloudtrail.Trail("example", {advancedEventSelectors: [
{
name: "Log all S3 objects events except for two S3 buckets",
fieldSelectors: [
{
field: "eventCategory",
equals: ["Data"],
},
{
field: "resources.ARN",
notStartsWiths: [
not_important_bucket_1.then(not_important_bucket_1 => `${not_important_bucket_1.arn}/`),
not_important_bucket_2.then(not_important_bucket_2 => `${not_important_bucket_2.arn}/`),
],
},
{
field: "resources.type",
equals: ["AWS::S3::Object"],
},
],
},
{
name: "Log readOnly and writeOnly management events",
fieldSelectors: [{
field: "eventCategory",
equals: ["Management"],
}],
},
]});
import pulumi
import pulumi_aws as aws
not_important_bucket_1 = aws.s3.get_bucket(bucket="not-important-bucket-1")
not_important_bucket_2 = aws.s3.get_bucket(bucket="not-important-bucket-2")
example = aws.cloudtrail.Trail("example", advanced_event_selectors=[
{
"name": "Log all S3 objects events except for two S3 buckets",
"field_selectors": [
{
"field": "eventCategory",
"equals": ["Data"],
},
{
"field": "resources.ARN",
"not_starts_withs": [
f"{not_important_bucket_1.arn}/",
f"{not_important_bucket_2.arn}/",
],
},
{
"field": "resources.type",
"equals": ["AWS::S3::Object"],
},
],
},
{
"name": "Log readOnly and writeOnly management events",
"field_selectors": [{
"field": "eventCategory",
"equals": ["Management"],
}],
},
])
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/cloudtrail"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
not_important_bucket_1, err := s3.LookupBucket(ctx, &s3.LookupBucketArgs{
Bucket: "not-important-bucket-1",
}, nil)
if err != nil {
return err
}
not_important_bucket_2, err := s3.LookupBucket(ctx, &s3.LookupBucketArgs{
Bucket: "not-important-bucket-2",
}, nil)
if err != nil {
return err
}
_, err = cloudtrail.NewTrail(ctx, "example", &cloudtrail.TrailArgs{
AdvancedEventSelectors: cloudtrail.TrailAdvancedEventSelectorArray{
&cloudtrail.TrailAdvancedEventSelectorArgs{
Name: pulumi.String("Log all S3 objects events except for two S3 buckets"),
FieldSelectors: cloudtrail.TrailAdvancedEventSelectorFieldSelectorArray{
&cloudtrail.TrailAdvancedEventSelectorFieldSelectorArgs{
Field: pulumi.String("eventCategory"),
Equals: pulumi.StringArray{
pulumi.String("Data"),
},
},
&cloudtrail.TrailAdvancedEventSelectorFieldSelectorArgs{
Field: pulumi.String("resources.ARN"),
NotStartsWiths: pulumi.StringArray{
pulumi.Sprintf("%v/", not_important_bucket_1.Arn),
pulumi.Sprintf("%v/", not_important_bucket_2.Arn),
},
},
&cloudtrail.TrailAdvancedEventSelectorFieldSelectorArgs{
Field: pulumi.String("resources.type"),
Equals: pulumi.StringArray{
pulumi.String("AWS::S3::Object"),
},
},
},
},
&cloudtrail.TrailAdvancedEventSelectorArgs{
Name: pulumi.String("Log readOnly and writeOnly management events"),
FieldSelectors: cloudtrail.TrailAdvancedEventSelectorFieldSelectorArray{
&cloudtrail.TrailAdvancedEventSelectorFieldSelectorArgs{
Field: pulumi.String("eventCategory"),
Equals: pulumi.StringArray{
pulumi.String("Management"),
},
},
},
},
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var not_important_bucket_1 = Aws.S3.GetBucket.Invoke(new()
{
Bucket = "not-important-bucket-1",
});
var not_important_bucket_2 = Aws.S3.GetBucket.Invoke(new()
{
Bucket = "not-important-bucket-2",
});
var example = new Aws.CloudTrail.Trail("example", new()
{
AdvancedEventSelectors = new[]
{
new Aws.CloudTrail.Inputs.TrailAdvancedEventSelectorArgs
{
Name = "Log all S3 objects events except for two S3 buckets",
FieldSelectors = new[]
{
new Aws.CloudTrail.Inputs.TrailAdvancedEventSelectorFieldSelectorArgs
{
Field = "eventCategory",
Equals = new[]
{
"Data",
},
},
new Aws.CloudTrail.Inputs.TrailAdvancedEventSelectorFieldSelectorArgs
{
Field = "resources.ARN",
NotStartsWiths = new[]
{
not_important_bucket_1.Apply(not_important_bucket_1 => $"{not_important_bucket_1.Apply(getBucketResult => getBucketResult.Arn)}/"),
not_important_bucket_2.Apply(not_important_bucket_2 => $"{not_important_bucket_2.Apply(getBucketResult => getBucketResult.Arn)}/"),
},
},
new Aws.CloudTrail.Inputs.TrailAdvancedEventSelectorFieldSelectorArgs
{
Field = "resources.type",
Equals = new[]
{
"AWS::S3::Object",
},
},
},
},
new Aws.CloudTrail.Inputs.TrailAdvancedEventSelectorArgs
{
Name = "Log readOnly and writeOnly management events",
FieldSelectors = new[]
{
new Aws.CloudTrail.Inputs.TrailAdvancedEventSelectorFieldSelectorArgs
{
Field = "eventCategory",
Equals = new[]
{
"Management",
},
},
},
},
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.s3.S3Functions;
import com.pulumi.aws.s3.inputs.GetBucketArgs;
import com.pulumi.aws.cloudtrail.Trail;
import com.pulumi.aws.cloudtrail.TrailArgs;
import com.pulumi.aws.cloudtrail.inputs.TrailAdvancedEventSelectorArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
final var not-important-bucket-1 = S3Functions.getBucket(GetBucketArgs.builder()
.bucket("not-important-bucket-1")
.build());
final var not-important-bucket-2 = S3Functions.getBucket(GetBucketArgs.builder()
.bucket("not-important-bucket-2")
.build());
var example = new Trail("example", TrailArgs.builder()
.advancedEventSelectors(
TrailAdvancedEventSelectorArgs.builder()
.name("Log all S3 objects events except for two S3 buckets")
.fieldSelectors(
TrailAdvancedEventSelectorFieldSelectorArgs.builder()
.field("eventCategory")
.equals("Data")
.build(),
TrailAdvancedEventSelectorFieldSelectorArgs.builder()
.field("resources.ARN")
.notStartsWiths(
String.format("%s/", not_important_bucket_1.arn()),
String.format("%s/", not_important_bucket_2.arn()))
.build(),
TrailAdvancedEventSelectorFieldSelectorArgs.builder()
.field("resources.type")
.equals("AWS::S3::Object")
.build())
.build(),
TrailAdvancedEventSelectorArgs.builder()
.name("Log readOnly and writeOnly management events")
.fieldSelectors(TrailAdvancedEventSelectorFieldSelectorArgs.builder()
.field("eventCategory")
.equals("Management")
.build())
.build())
.build());
}
}
resources:
example:
type: aws:cloudtrail:Trail
properties:
advancedEventSelectors:
- name: Log all S3 objects events except for two S3 buckets
fieldSelectors:
- field: eventCategory
equals:
- Data
- field: resources.ARN
notStartsWiths:
- ${["not-important-bucket-1"].arn}/
- ${["not-important-bucket-2"].arn}/
- field: resources.type
equals:
- AWS::S3::Object
- name: Log readOnly and writeOnly management events
fieldSelectors:
- field: eventCategory
equals:
- Management
variables:
not-important-bucket-1:
fn::invoke:
function: aws:s3:getBucket
arguments:
bucket: not-important-bucket-1
not-important-bucket-2:
fn::invoke:
function: aws:s3:getBucket
arguments:
bucket: not-important-bucket-2
Advanced event selectors use fieldSelectors to build complex filtering logic. The notStartsWiths operator excludes specific bucket ARNs, while equals filters by eventCategory, resources.type, and other dimensions. You can combine multiple selectors to capture different event patterns in a single trail.
Stream events to CloudWatch Logs
CloudWatch Logs integration enables real-time analysis and alerting on CloudTrail events using metric filters and alarms.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const example = new aws.cloudwatch.LogGroup("example", {name: "Example"});
const exampleTrail = new aws.cloudtrail.Trail("example", {cloudWatchLogsGroupArn: pulumi.interpolate`${example.arn}:*`});
import pulumi
import pulumi_aws as aws
example = aws.cloudwatch.LogGroup("example", name="Example")
example_trail = aws.cloudtrail.Trail("example", cloud_watch_logs_group_arn=example.arn.apply(lambda arn: f"{arn}:*"))
package main
import (
"fmt"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/cloudtrail"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/cloudwatch"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
example, err := cloudwatch.NewLogGroup(ctx, "example", &cloudwatch.LogGroupArgs{
Name: pulumi.String("Example"),
})
if err != nil {
return err
}
_, err = cloudtrail.NewTrail(ctx, "example", &cloudtrail.TrailArgs{
CloudWatchLogsGroupArn: example.Arn.ApplyT(func(arn string) (string, error) {
return fmt.Sprintf("%v:*", arn), nil
}).(pulumi.StringOutput),
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var example = new Aws.CloudWatch.LogGroup("example", new()
{
Name = "Example",
});
var exampleTrail = new Aws.CloudTrail.Trail("example", new()
{
CloudWatchLogsGroupArn = example.Arn.Apply(arn => $"{arn}:*"),
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.cloudwatch.LogGroup;
import com.pulumi.aws.cloudwatch.LogGroupArgs;
import com.pulumi.aws.cloudtrail.Trail;
import com.pulumi.aws.cloudtrail.TrailArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var example = new LogGroup("example", LogGroupArgs.builder()
.name("Example")
.build());
var exampleTrail = new Trail("exampleTrail", TrailArgs.builder()
.cloudWatchLogsGroupArn(example.arn().applyValue(_arn -> String.format("%s:*", _arn)))
.build());
}
}
resources:
example:
type: aws:cloudwatch:LogGroup
properties:
name: Example
exampleTrail:
type: aws:cloudtrail:Trail
name: example
properties:
cloudWatchLogsGroupArn: ${example.arn}:*
The cloudWatchLogsGroupArn property points to a CloudWatch Log Group where CloudTrail delivers events. Note the :* suffix, which CloudTrail requires to represent the log stream wildcard. You’ll also need to configure cloudWatchLogsRoleArn (not shown) with an IAM role that grants CloudTrail permission to write to the log group.
Beyond these examples
These snippets focus on specific trail-level features: management and data event capture, basic and advanced event selectors, and S3 and CloudWatch Logs destinations. They’re intentionally minimal rather than full audit logging solutions.
The examples may reference pre-existing infrastructure such as S3 buckets with CloudTrail-compatible policies, CloudWatch Log Groups, and IAM roles for CloudWatch Logs delivery. They focus on configuring the trail rather than provisioning everything around it.
To keep things focused, common trail patterns are omitted, including:
- Multi-region trails (isMultiRegionTrail)
- Organization trails (isOrganizationTrail)
- Log file encryption (kmsKeyId)
- Log file validation (enableLogFileValidation)
- SNS notifications (snsTopicName)
- Insight selectors for anomaly detection
These omissions are intentional: the goal is to illustrate how each trail feature is wired, not provide drop-in audit modules. See the CloudTrail Trail resource reference for all available configuration options.
Let's configure AWS CloudTrail Audit Trails
Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.
Try Pulumi Cloud for FREEFrequently Asked Questions
Setup & Configuration
CloudTrail requires two permissions on the S3 bucket:
s3:GetBucketAclon the bucket itselfs3:PutObjecton the log prefix path (e.g.,bucket-arn/prefix/AWSLogs/account-id/*)
dependsOn to ensure the bucket policy is created first.name property is immutable and cannot be changed after the trail is created.Event Logging & Selectors
includeGlobalServiceEvents to true (the default) to capture events from global services like IAM.eventSelectors) use simple resource type matching, while advanced event selectors (advancedEventSelectors) support complex filtering with field conditions like notStartsWith and equals. You cannot use both simultaneously.You have two options:
- Basic selectors - Use
eventSelectorswithtype: "AWS::S3::Object"and bucket ARNs - Advanced selectors - Use
advancedEventSelectorswithfield: "resources.type"equals"AWS::S3::Object"for fine-grained filtering
eventSelectors with type: "AWS::Lambda::Function" and values: ["arn:aws:lambda"].CloudWatch Integration
${logGroup.arn}:*.Trail Properties & Defaults
CloudTrail trails have the following defaults:
enableLogging:true(logging is active)includeGlobalServiceEvents:true(captures IAM events)isMultiRegionTrail:false(single region)isOrganizationTrail:false(not an organization trail)enableLogFileValidation:false(integrity validation disabled)