Configure AWS CloudTrail Audit Trails

The aws:cloudtrail/trail:Trail resource, part of the Pulumi AWS provider, defines a CloudTrail trail that captures AWS API activity and delivers audit logs to S3 or CloudWatch Logs. This guide focuses on three capabilities: management and data event capture, basic and advanced event filtering, and S3 and CloudWatch Logs delivery.

Trails require S3 buckets with CloudTrail write policies, and optionally CloudWatch log groups with IAM roles for real-time delivery. The examples are intentionally small. Combine them with your own storage infrastructure and monitoring setup.

Capture management events to S3

Most deployments begin by capturing management events (API calls that create, modify, or delete resources) and delivering them to S3 for long-term storage.

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 log files to the S3 bucket specified in s3BucketName, organizing them under the s3KeyPrefix path. The bucket policy grants CloudTrail permission to check bucket ACLs and write log files. 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

Beyond management events, CloudTrail captures data events like Lambda function invocations to track execution patterns.

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 defines what data events to log. Each selector specifies a readWriteType (All, ReadOnly, or WriteOnly) and dataResources that identify the service and scope. The type “AWS::Lambda::Function” with value “arn:aws:lambda” captures all Lambda invocations across your account.

Log all S3 object operations with basic selectors

S3 object-level logging captures every GetObject, PutObject, and DeleteObject call for security monitoring.

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::S3::Object",
        values: ["arn:aws:s3"],
    }],
}]});
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::S3::Object",
        "values": ["arn:aws:s3"],
    }],
}])
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::S3::Object"),
							Values: pulumi.StringArray{
								pulumi.String("arn:aws:s3"),
							},
						},
					},
				},
			},
		})
		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::S3::Object",
                        Values = new[]
                        {
                            "arn:aws:s3",
                        },
                    },
                },
            },
        },
    });

});
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::S3::Object")
                    .values("arn:aws:s3")
                    .build())
                .build())
            .build());

    }
}
resources:
  example:
    type: aws:cloudtrail:Trail
    properties:
      eventSelectors:
        - readWriteType: All
          includeManagementEvents: true
          dataResources:
            - type: AWS::S3::Object
              values:
                - arn:aws:s3

This configuration uses the same eventSelectors structure but targets S3 objects instead of Lambda functions. The type “AWS::S3::Object” with value “arn:aws:s3” logs all object operations across all buckets. For individual buckets, specify the full bucket ARN with a trailing slash.

Filter S3 events with advanced event selectors

Advanced event selectors provide fine-grained control over which events to capture using field-based filtering.

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 (
	"fmt"

	"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 filters. Each selector has a name and multiple field conditions. The notStartsWiths operator excludes specific bucket ARNs from logging. You can combine multiple selectors to log different event categories with different filters; here, one selector handles S3 data events while another captures all management events.

Stream events to CloudWatch Logs

For real-time monitoring, CloudTrail can deliver events to CloudWatch Logs where you can create 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 specifies the destination log group. CloudTrail requires the ARN to end with :* (the Log Stream wildcard). You must also provide 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 logging, basic and advanced event selectors, and S3 and CloudWatch Logs delivery. They’re intentionally minimal rather than full audit solutions.

The examples may reference pre-existing infrastructure such as S3 buckets with CloudTrail write 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 FREE

Frequently Asked Questions

Trail Setup & Configuration
Where should I create my CloudTrail trail resource?
For multi-region trails, create the resource in the trail’s home region. For organization trails, create it in the organization’s master account.
What S3 bucket permissions does CloudTrail require?
The S3 bucket policy must allow CloudTrail to perform s3:GetBucketAcl and s3:PutObject actions. The Basic example demonstrates the required policy statements with proper conditions.
What properties can't I change after creating a trail?
The name property is immutable and cannot be changed after creation.
What are the default settings for a new trail?
By default, enableLogging is true (logging starts immediately), includeGlobalServiceEvents is true (captures IAM events), isMultiRegionTrail is false, and enableLogFileValidation is false.
How do I send CloudTrail logs to CloudWatch Logs?
Set cloudWatchLogsGroupArn to your log group ARN with the :* wildcard appended (e.g., ${logGroup.arn}:*). CloudTrail requires the Log Stream wildcard.
Event Logging & Selectors
What's the difference between eventSelectors and advancedEventSelectors?
eventSelectors provides basic data event logging, while advancedEventSelectors enables complex filtering by event name, resource ARN patterns, and other fields. These two properties conflict and cannot be used together.
How do I log S3 object events for specific buckets?
Use eventSelectors with type AWS::S3::Object and specify bucket ARNs (e.g., ${bucket.arn}/), or use advancedEventSelectors to filter by resource ARN patterns.
How do I log Lambda function invocations?
Configure eventSelectors with type set to AWS::Lambda::Function and values set to ["arn:aws:lambda"] to capture all Lambda invocations.
Can I exclude specific S3 buckets from data event logging?
Yes, use advancedEventSelectors with notStartsWith on the resources.ARN field to exclude specific bucket ARNs.
How do I capture events from global services like IAM?
Set includeGlobalServiceEvents to true (the default). This is required for logging events from global services.
Security & Compliance
How do I enable log file integrity validation?
Set enableLogFileValidation to true. This feature is disabled by default.
Can I encrypt CloudTrail logs with KMS?
Yes, specify a KMS key ARN in the kmsKeyId property to encrypt logs delivered by CloudTrail.

Using a different cloud?

Explore security guides for other cloud providers: