Skip to main content

Overview

IAM Roles grant AWS service permissions to your applications. Fjall automatically creates and manages IAM roles when you use ComputeFactory - you rarely need to create them manually.

Automatic Management

Fjall’s ComputeFactory automatically:
  • Creates IAM roles for ECS tasks and Lambda functions
  • Grants CloudWatch Logs permissions
  • Configures ECR pull permissions for ECS
  • Adds permissions for services in the connections array
// Fjall creates IAM roles automatically
const api = app.addCompute(
  ComputeFactory.build("API", {
    type: "ecs",
    ecsType: "fargate",
    // IAM role created with CloudWatch + ECR permissions
  })
);

Lambda Function Roles

Automatic Permissions

Lambda functions get automatic access to:
  • CloudWatch Logs (write logs)
  • X-Ray (tracing, if enabled)
const lambda = app.addCompute(
  ComputeFactory.build("Function", {
    type: "lambda",
    handler: "index.handler",
    runtime: Runtime.NODEJS_20_X,
    code: Code.fromAsset("./lambda"),
    // Role created with CloudWatch Logs access
  })
);

Custom Permissions via inlinePolicy

Add AWS service permissions:
import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
import { PolicyDocument } from "aws-cdk-lib/aws-iam";

const lambda = app.addCompute(
  ComputeFactory.build("S3Processor", {
    type: "lambda",
    handler: "index.handler",
    runtime: Runtime.NODEJS_20_X,
    code: Code.fromAsset("./lambda"),
    inlinePolicy: {
      S3Access: new PolicyDocument({
        statements: [
          new PolicyStatement({
            effect: Effect.ALLOW,
            actions: ["s3:GetObject", "s3:PutObject"],
            resources: ["arn:aws:s3:::my-bucket/*"],
          }),
        ],
      }),
    },
  })
);

Grant Methods

Use resource grant methods for type-safe permissions:
import { Bucket } from "aws-cdk-lib/aws-s3";

const bucket = new Bucket(this, "Uploads");

const lambda = app.addCompute(
  ComputeFactory.build("Uploader", {
    type: "lambda",
    handler: "index.handler",
    runtime: Runtime.NODEJS_20_X,
    code: Code.fromAsset("./lambda"),
    inlinePolicy: {}, // Required even if empty
  })
);

// Grant bucket access
bucket.grantReadWrite(lambda.role);

ECS Task Roles

Task Role vs Execution Role

Fjall creates both:
  • Execution Role: Used by ECS to pull images, write logs
  • Task Role: Used by your application code to access AWS services
const api = app.addCompute(
  ComputeFactory.build("API", {
    type: "ecs",
    ecsType: "fargate",
  })
);

// Access roles
api.taskRole         // For application (S3, DynamoDB, etc.)
api.executionRole    // For ECS (ECR, CloudWatch)

Grant Permissions to ECS

import { Bucket } from "aws-cdk-lib/aws-s3";
import { Table } from "aws-cdk-lib/aws-dynamodb";

const bucket = new Bucket(this, "Data");
const table = new Table(this, "Users", {
  partitionKey: { name: "id", type: AttributeType.STRING },
});

const api = app.addCompute(
  ComputeFactory.build("API", {
    type: "ecs",
    ecsType: "fargate",
  })
);

// Grant access to task role
bucket.grantReadWrite(api.taskRole);
table.grantReadWriteData(api.taskRole);

Database Access

Automatic via connections Array

When you add databases to connections, Fjall automatically:
  • Grants network access (security groups)
  • Grants Secrets Manager read access for credentials
const database = app.addDatabase(
  DatabaseFactory.build("DB", {
    type: "Instance",
    databaseName: "mydb",
  })
);

const api = app.addCompute(
  ComputeFactory.build("API", {
    type: "ecs",
    connections: [database],
    containerSecretsImport: {
      DB_PASSWORD: database.getCredentials(),
    },
    // Fjall automatically grants Secrets Manager read access
  })
);

Common Permission Patterns

S3 Access

import { Bucket } from "aws-cdk-lib/aws-s3";

const bucket = new Bucket(this, "Assets");

// Lambda
bucket.grantRead(lambda.role);
bucket.grantWrite(lambda.role);
bucket.grantReadWrite(lambda.role);
bucket.grantDelete(lambda.role);

// ECS
bucket.grantReadWrite(api.taskRole);

DynamoDB Access

import { Table } from "aws-cdk-lib/aws-dynamodb";

const table = new Table(this, "Data", {
  partitionKey: { name: "id", type: AttributeType.STRING },
});

table.grantReadData(lambda.role);
table.grantWriteData(lambda.role);
table.grantReadWriteData(api.taskRole);

SQS Access

import { Queue } from "aws-cdk-lib/aws-sqs";

const queue = new Queue(this, "Tasks");

queue.grantSendMessages(api.taskRole);
queue.grantConsumeMessages(worker.role);

SNS Access

import { Topic } from "aws-cdk-lib/aws-sns";

const topic = new Topic(this, "Notifications");

topic.grantPublish(lambda.role);

Secrets Manager Access

import { Secret } from "aws-cdk-lib/aws-secretsmanager";

const apiKey = new Secret(this, "ApiKey");

apiKey.grantRead(lambda.role);
apiKey.grantWrite(rotator.role);

Cross-Account Access

Assume Role

Allow access from another AWS account:
import { Role, AccountPrincipal, PolicyStatement } from "aws-cdk-lib/aws-iam";

const crossAccountRole = new Role(this, "CrossAccountRole", {
  assumedBy: new AccountPrincipal("123456789012"),
});

// Grant permissions
bucket.grantRead(crossAccountRole);

Service-to-Service

import { ServicePrincipal } from "aws-cdk-lib/aws-iam";

const serviceRole = new Role(this, "ServiceRole", {
  assumedBy: new ServicePrincipal("events.amazonaws.com"),
});

lambda.grantInvoke(serviceRole);

Managed Policies

AWS Managed Policies

import { ManagedPolicy } from "aws-cdk-lib/aws-iam";

// Add managed policy to role
lambda.role.addManagedPolicy(
  ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMReadOnlyAccess")
);

Common Managed Policies

  • AWSLambdaBasicExecutionRole - CloudWatch Logs (auto-added by Fjall)
  • AmazonS3ReadOnlyAccess - Read all S3 buckets
  • AmazonDynamoDBReadOnlyAccess - Read all DynamoDB tables
  • SecretsManagerReadWrite - Secrets Manager access
  • AmazonSSMReadOnlyAccess - Parameter Store read

Custom Policy Statements

Inline Policy via PolicyStatement

import { PolicyStatement, PolicyDocument } from "aws-cdk-lib/aws-iam";

const lambda = app.addCompute(
  ComputeFactory.build("Custom", {
    type: "lambda",
    handler: "index.handler",
    runtime: Runtime.NODEJS_20_X,
    code: Code.fromAsset("./lambda"),
    inlinePolicy: {
      CustomAccess: new PolicyDocument({
        statements: [
          new PolicyStatement({
            actions: [
              "ec2:DescribeInstances",
              "ec2:DescribeVolumes",
            ],
            resources: ["*"],
          }),
        ],
      }),
    },
  })
);

Multiple Policies

const lambda = app.addCompute(
  ComputeFactory.build("MultiAccess", {
    type: "lambda",
    handler: "index.handler",
    runtime: Runtime.NODEJS_20_X,
    code: Code.fromAsset("./lambda"),
    inlinePolicy: {
      S3Policy: new PolicyDocument({
        statements: [
          new PolicyStatement({
            actions: ["s3:GetObject"],
            resources: ["arn:aws:s3:::my-bucket/*"],
          }),
        ],
      }),
      DynamoPolicy: new PolicyDocument({
        statements: [
          new PolicyStatement({
            actions: ["dynamodb:Query", "dynamodb:Scan"],
            resources: ["arn:aws:dynamodb:*:*:table/MyTable"],
          }),
        ],
      }),
    },
  })
);

Accessing Role Information

Get Role ARN

console.log(lambda.role.roleArn);
// arn:aws:iam::123456789012:role/MyFunction-Role-ABC123

console.log(api.taskRole.roleArn);
// arn:aws:iam::123456789012:role/API-TaskRole-XYZ789

Use in Environment Variables

const lambda = app.addCompute(
  ComputeFactory.build("Function", {
    type: "lambda",
    handler: "index.handler",
    runtime: Runtime.NODEJS_20_X,
    code: Code.fromAsset("./lambda"),
    environment: {
      ROLE_ARN: lambda.role.roleArn, // Wait, this is circular!
    },
    inlinePolicy: {},
  })
);

Best Practices

  • Use grant methods instead of manual policy statements when possible
  • Follow least privilege - only grant required permissions
  • Let Fjall create roles for standard deployments
  • Use connections array for database access
  • Separate policies by purpose for clarity
  • Avoid wildcards in resources when possible
  • Test permissions after deployment
  • Monitor with CloudTrail for access patterns

Troubleshooting

Access Denied Errors

  1. Check role has permission for the action
  2. Verify resource ARN matches
  3. Look for explicit denies in policies
  4. Check resource policies (S3, KMS, etc.)
  5. Review CloudTrail logs for detailed error

Role Not Found

  • Verify role was created (check api.taskRole or lambda.role)
  • Ensure ComputeFactory call succeeded
  • Check IAM console for role existence

Permissions Too Broad

// Too broad ❌
new PolicyStatement({
  actions: ["s3:*"],
  resources: ["*"],
});

// Better ✅
new PolicyStatement({
  actions: ["s3:GetObject", "s3:PutObject"],
  resources: ["arn:aws:s3:::my-bucket/*"],
});

See Also