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 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 FREE

Frequently Asked Questions

Setup & Configuration
Where must I create a multi-region CloudTrail trail?
Multi-region trails must be created in the trail’s home region. Creating the resource in a different region will fail.
Where must I create an organization trail?
Organization trails must be created in the organization’s master account. They cannot be created in member accounts.
What S3 bucket permissions does CloudTrail require?

CloudTrail requires two permissions on the S3 bucket:

  1. s3:GetBucketAcl on the bucket itself
  2. s3:PutObject on the log prefix path (e.g., bucket-arn/prefix/AWSLogs/account-id/*)
Why should I create the S3 bucket policy before the trail?
The trail creation will fail if the bucket policy doesn’t exist. Use dependsOn to ensure the bucket policy is created first.
Can I rename a CloudTrail trail after creation?
No, the name property is immutable and cannot be changed after the trail is created.
Event Logging & Selectors
How do I capture IAM events with CloudTrail?
Set includeGlobalServiceEvents to true (the default) to capture events from global services like IAM.
What's the difference between basic and advanced event selectors?
Basic event selectors (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.
How do I log S3 data events?

You have two options:

  1. Basic selectors - Use eventSelectors with type: "AWS::S3::Object" and bucket ARNs
  2. Advanced selectors - Use advancedEventSelectors with field: "resources.type" equals "AWS::S3::Object" for fine-grained filtering
How do I log Lambda function invocations?
Use eventSelectors with type: "AWS::Lambda::Function" and values: ["arn:aws:lambda"].
CloudWatch Integration
What format should I use for the CloudWatch Logs group ARN?
CloudTrail requires the Log Stream wildcard appended to the log group ARN. Use the format ${logGroup.arn}:*.
Trail Properties & Defaults
What are the default settings for CloudTrail trails?

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)

Using a different cloud?

Explore security guides for other cloud providers: