Safely Running Kiro CLI's use_aws Tool with Lambda + IAM Roles

Safely Running Kiro CLI's use_aws Tool with Lambda + IAM Roles

Kiro CLI headless mode runs with --trust-all-tools, which is risky, but Lambda plus an IAM role limits AWS actions to least privilege. Dockerizing cut cold starts to 22 seconds. Shared as a 2-stack CloudFormation template.
2026.05.21

This page has been translated by machine translation. View original

Kiro CLI 2.0 adds a headless mode. You can run it without interaction by using the KIRO_API_KEY environment variable and the --no-interactive and --trust-all-tools flags.

https://dev.classmethod.jp/articles/kiro-cli-2-0-headless-mode-api-key-auth/

Kiro CLI has a dedicated tool called use_aws, which runs the aws command internally to operate AWS APIs. If you run it on Lambda, you can automate AWS operations in an event-driven way, and the key point is that the scope of operations can be controlled with an IAM role.

The official CI/CD example (GitHub Actions) installs it with curl | bash every time, but on Lambda that means downloading it every time, resulting in a cold start of about 40 seconds. I tested whether containerizing with Docker can shorten this.

I considered three patterns for deploying to Lambda.

Method Cold start Warm ECR required Deployment
Install to /tmp each time 41 sec 3.5 sec ❌ zip
Docker (ECR) 22 sec 3.2 sec ✅ Image
Lambda Layer Not possible (452MB > 250MB limit) - - -
  • Measurement conditions: Memory 512MB / ARM64 / us-east-1 / kiro-cli 2.3.0 / Prompt "What is 2+2?"

What I Tested

Dockerfile

The base image is public.ecr.aws/lambda/python:3.12. The key points are as follows.

  • Kiro CLI is installed at build time and copied to /usr/local/bin. The Lambda execution environment is non-root, so /root/.local/bin cannot be accessed directly
  • ENV PATH is for the install script at build time. At Lambda runtime, handler.py calls it by full path, so there is no dependency on PATH at runtime
  • KIRO_CLI_SKIP_SETUP=1 skips the install script's initial setup (interactive configuration)
  • AWS CLI v2 is also installed. It is required because the use_aws tool spawns the aws command internally
Full Dockerfile
FROM public.ecr.aws/lambda/python:3.12

ENV PATH="/root/.local/bin:${PATH}"

RUN dnf install -y tar gzip unzip && \
    curl -fsSL https://cli.kiro.dev/install | KIRO_CLI_SKIP_SETUP=1 bash && \
    cp /root/.local/bin/kiro-cli* /usr/local/bin/ && \
    curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o /tmp/awscliv2.zip && \
    unzip -q /tmp/awscliv2.zip -d /tmp && /tmp/aws/install && rm -rf /tmp/aws /tmp/awscliv2.zip && \
    dnf clean all

RUN pip install boto3

COPY handler.py ${LAMBDA_TASK_ROOT}/

CMD ["handler.handler"]

handler.py

This is the entry point of the Lambda function.

  • Retrieves the Kiro API key from SSM Parameter Store (SecureString) and uses a cache when warm
  • Sets HOME=/tmp. kiro-cli creates a SQLite DB on first launch, so a writable path is required (/var/task is read-only)

--trust-all-tools is a flag that allows execution of all tools, but on Lambda the AWS API calls made by use_aws are bound by the permissions of the IAM role. In this case, only ec2:DescribeRegions and ec2:DescribeAvailabilityZones are allowed, so even if the agent tried to launch an EC2 instance, it would get Permission Denied. If you split Lambdas by use case, you can achieve least privilege per role.

For the steps to issue an API key, see the previous article.

Full handler.py
import subprocess
import json
import os
import time

import boto3

ssm = boto3.client("ssm", region_name=os.environ.get("AWS_REGION", "us-east-1"))
_api_key_cache = None

def get_api_key():
    global _api_key_cache
    if not _api_key_cache:
        _api_key_cache = os.environ.get("KIRO_API_KEY")
        if not _api_key_cache:
            resp = ssm.get_parameter(Name="/kiro/headless/api-key", WithDecryption=True)
            _api_key_cache = resp["Parameter"]["Value"]
    return _api_key_cache

def handler(event, context):
    timings = {}
    t0 = time.time()

    prompt = event.get("prompt", "")
    if not prompt or len(prompt.strip()) < 10:
        return {"statusCode": 400, "error": "Prompt too short (min 10 chars)"}

    api_key = get_api_key()
    env = {**os.environ, "KIRO_API_KEY": api_key, "HOME": "/tmp"}
    kiro_bin = "/usr/local/bin/kiro-cli"

    cmd = [kiro_bin, "chat", "--no-interactive", "--trust-all-tools"]
    # model = event.get("model")  # e.g. "claude-sonnet-4.6"
    # if model:
    #     cmd += ["--model", model]
    cmd.append(prompt)

    start = time.time()
    result = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        timeout=280,
        env=env,
    )
    timings["kiro_execution"] = round(time.time() - start, 2)
    timings["total"] = round(time.time() - t0, 2)

    if result.returncode != 0:
        return {
            "statusCode": 500,
            "error": result.stderr.strip() or f"kiro-cli exited with code {result.returncode}",
            "timings": timings,
        }

    return {
        "statusCode": 200,
        "prompt": prompt,
        "result": result.stdout.strip(),
        "timings": timings,
    }

/tmp method (for reference)

As a comparison with the Docker method, I also tested the method of installing to /tmp each time. It downloads and extracts Kiro CLI and AWS CLI using only the Python standard library (urllib + zipfile). It requires no ECR and suits a PoC, but the cold start takes 41 seconds and it depends on the network, so I recommend the Docker method for production use.

Full handler_tmp.py
import subprocess
import json
import os
import time
import zipfile
import urllib.request

import boto3

ssm = boto3.client("ssm", region_name=os.environ.get("AWS_REGION", "us-east-1"))
_api_key_cache = None

KIRO_BIN = "/tmp/.local/bin/kiro-cli"
AWS_CLI_BIN = "/tmp/aws-bin/aws"

def get_api_key():
    global _api_key_cache
    if not _api_key_cache:
        _api_key_cache = os.environ.get("KIRO_API_KEY")
        if not _api_key_cache:
            resp = ssm.get_parameter(Name="/kiro/headless/api-key", WithDecryption=True)
            _api_key_cache = resp["Parameter"]["Value"]
    return _api_key_cache

def download_and_extract(url, dest_dir):
    """Download zip and extract to dest_dir."""
    zip_path = f"/tmp/{os.path.basename(dest_dir)}.zip"
    urllib.request.urlretrieve(url, zip_path)
    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(dest_dir)
    os.remove(zip_path)

def install_kiro():
    if os.path.exists(KIRO_BIN):
        return 0
    start = time.time()
    arch = "aarch64" if os.uname().machine == "aarch64" else "x86_64"
    url = f"https://desktop-release.q.us-east-1.amazonaws.com/latest/kirocli-{arch}-linux.zip"
    download_and_extract(url, "/tmp/kirocli-pkg")
    os.makedirs("/tmp/.local/bin", exist_ok=True)
    for name in ["kiro-cli", "kiro-cli-chat", "kiro-cli-term"]:
        src = f"/tmp/kirocli-pkg/kirocli/bin/{name}"
        dst = f"/tmp/.local/bin/{name}"
        if os.path.exists(src):
            subprocess.run(["cp", src, dst], check=True)
            os.chmod(dst, 0o755)
    subprocess.run(["rm", "-rf", "/tmp/kirocli-pkg"], capture_output=True)
    return round(time.time() - start, 2)

def install_aws_cli():
    if os.path.exists(AWS_CLI_BIN):
        return 0
    start = time.time()
    arch = "aarch64" if os.uname().machine == "aarch64" else "x86_64"
    url = f"https://awscli.amazonaws.com/awscli-exe-linux-{arch}.zip"
    download_and_extract(url, "/tmp/aws-install")
    installer = "/tmp/aws-install/aws/install"
    os.chmod(installer, 0o755)
    subprocess.run(
        [installer, "--install-dir", "/tmp/aws-cli", "--bin-dir", "/tmp/aws-bin"],
        check=True, capture_output=True, text=True,
    )
    subprocess.run(["rm", "-rf", "/tmp/aws-install"], capture_output=True)
    return round(time.time() - start, 2)

def handler(event, context):
    timings = {}

    t0 = time.time()
    try:
        timings["kiro_install"] = install_kiro()
    except Exception as e:
        return {"statusCode": 500, "error": f"kiro install failed: {e}", "timings": timings}
    try:
        timings["aws_cli_install"] = install_aws_cli()
    except Exception as e:
        return {"statusCode": 500, "error": f"aws cli install failed: {e}", "timings": timings}
    timings["total_install"] = round(time.time() - t0, 2)

    prompt = event.get("prompt", "")
    if not prompt or len(prompt.strip()) < 10:
        return {"statusCode": 400, "error": "Prompt too short (min 10 chars)", "timings": timings}

    api_key = get_api_key()
    env = {
        **os.environ,
        "KIRO_API_KEY": api_key,
        "HOME": "/tmp",
        "PATH": f"/tmp/.local/bin:/tmp/aws-bin:{os.environ.get('PATH', '')}",
    }

    start = time.time()
    result = subprocess.run(
        [KIRO_BIN, "chat", "--no-interactive", "--trust-all-tools", prompt],
        capture_output=True, text=True, timeout=280, env=env,
    )
    timings["kiro_execution"] = round(time.time() - start, 2)
    timings["total"] = round(time.time() - t0, 2)

    if result.returncode != 0:
        return {
            "statusCode": 500,
            "error": result.stderr.strip() or f"exit code {result.returncode}",
            "timings": timings,
        }

    return {
        "statusCode": 200,
        "prompt": prompt,
        "result": result.stdout.strip(),
        "timings": timings,
    }

CloudFormation + CodeBuild

This design completes the environment setup with just two templates, without S3 or GitHub integration.

Stack Contents
Stack1 ECR repository + CodeBuild project
Stack2 Lambda function (deployed after the image is pushed in Stack1)

CodeBuild uses the NO_SOURCE type, and the Dockerfile and handler.py are embedded in the buildspec as base64. It uses an ARM64 build environment (aws/codebuild/amazonlinux-aarch64-standard:3.0).

Full stack1-ecr-codebuild.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Kiro Headless Lambda - Stack 1: ECR + CodeBuild'

Resources:
  EcrRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: kiro-headless-lambda
      ImageScanningConfiguration:
        ScanOnPush: true
      LifecyclePolicy:
        LifecyclePolicyText: |
          {"rules":[{"rulePriority":1,"selection":{"tagStatus":"untagged","countType":"imageCountMoreThan","countNumber":3},"action":{"type":"expire"}}]}

  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: CodeBuildPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: '*'
              - Effect: Allow
                Action: ecr:GetAuthorizationToken
                Resource: '*'
              - Effect: Allow
                Action:
                  - ecr:BatchCheckLayerAvailability
                  - ecr:GetDownloadUrlForLayer
                  - ecr:BatchGetImage
                  - ecr:PutImage
                  - ecr:InitiateLayerUpload
                  - ecr:UploadLayerPart
                  - ecr:CompleteLayerUpload
                Resource: !GetAtt EcrRepository.Arn

  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: kiro-headless-lambda-build
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Artifacts:
        Type: NO_ARTIFACTS
      Environment:
        Type: ARM_CONTAINER
        Image: aws/codebuild/amazonlinux-aarch64-standard:3.0
        ComputeType: BUILD_GENERAL1_SMALL
        PrivilegedMode: true
        EnvironmentVariables:
          - Name: ECR_REPO_URI
            Value: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepository}'
      Source:
        Type: NO_SOURCE
        BuildSpec: !Sub |
          version: 0.2
          phases:
            pre_build:
              commands:
                - aws ecr get-login-password | docker login --username AWS --password-stdin "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com"
            build:
              commands:
                - echo "RlJPTSBwdWJsaWMuZWNyLmF3cy9sYW1iZGEvcHl0aG9uOjMuMTIKCkVOViBQQVRIPSIvcm9vdC8ubG9jYWwvYmluOiR7UEFUSH0iCgpSVU4gZG5mIGluc3RhbGwgLXkgdGFyIGd6aXAgdW56aXAgJiYgXAogICAgY3VybCAtZnNTTCBodHRwczovL2NsaS5raXJvLmRldi9pbnN0YWxsIHwgS0lST19DTElfU0tJUF9TRVRVUD0xIGJhc2ggJiYgXAogICAgY3AgL3Jvb3QvLmxvY2FsL2Jpbi9raXJvLWNsaSogL3Vzci9sb2NhbC9iaW4vICYmIFwKICAgIGN1cmwgImh0dHBzOi8vYXdzY2xpLmFtYXpvbmF3cy5jb20vYXdzY2xpLWV4ZS1saW51eC1hYXJjaDY0LnppcCIgLW8gL3RtcC9hd3NjbGl2Mi56aXAgJiYgXAogICAgdW56aXAgLXEgL3RtcC9hd3NjbGl2Mi56aXAgLWQgL3RtcCAmJiAvdG1wL2F3cy9pbnN0YWxsICYmIHJtIC1yZiAvdG1wL2F3cyAvdG1wL2F3c2NsaXYyLnppcCAmJiBcCiAgICBkbmYgY2xlYW4gYWxsCgpSVU4gcGlwIGluc3RhbGwgYm90bzMKCkNPUFkgaGFuZGxlci5weSAke0xBTUJEQV9UQVNLX1JPT1R9LwoKQ01EIFsiaGFuZGxlci5oYW5kbGVyIl0K" | base64 -d > Dockerfile
                - echo "aW1wb3J0IHN1YnByb2Nlc3MKaW1wb3J0IGpzb24KaW1wb3J0IG9zCmltcG9ydCB0aW1lCgppbXBvcnQgYm90bzMKCnNzbSA9IGJvdG8zLmNsaWVudCgic3NtIiwgcmVnaW9uX25hbWU9b3MuZW52aXJvbi5nZXQoIkFXU19SRUdJT04iLCAidXMtZWFzdC0xIikpCl9hcGlfa2V5X2NhY2hlID0gTm9uZQoKCmRlZiBnZXRfYXBpX2tleSgpOgogICAgZ2xvYmFsIF9hcGlfa2V5X2NhY2hlCiAgICBpZiBub3QgX2FwaV9rZXlfY2FjaGU6CiAgICAgICAgX2FwaV9rZXlfY2FjaGUgPSBvcy5lbnZpcm9uLmdldCgiS0lST19BUElfS0VZIikKICAgICAgICBpZiBub3QgX2FwaV9rZXlfY2FjaGU6CiAgICAgICAgICAgIHJlc3AgPSBzc20uZ2V0X3BhcmFtZXRlcihOYW1lPSIva2lyby9oZWFkbGVzcy9hcGkta2V5IiwgV2l0aERlY3J5cHRpb249VHJ1ZSkKICAgICAgICAgICAgX2FwaV9rZXlfY2FjaGUgPSByZXNwWyJQYXJhbWV0ZXIiXVsiVmFsdWUiXQogICAgcmV0dXJuIF9hcGlfa2V5X2NhY2hlCgoKZGVmIGhhbmRsZXIoZXZlbnQsIGNvbnRleHQpOgogICAgdGltaW5ncyA9IHt9CiAgICB0MCA9IHRpbWUudGltZSgpCgogICAgcHJvbXB0ID0gZXZlbnQuZ2V0KCJwcm9tcHQiLCAiIikKICAgIGlmIG5vdCBwcm9tcHQgb3IgbGVuKHByb21wdC5zdHJpcCgpKSA8IDEwOgogICAgICAgIHJldHVybiB7InN0YXR1c0NvZGUiOiA0MDAsICJlcnJvciI6ICJQcm9tcHQgdG9vIHNob3J0IChtaW4gMTAgY2hhcnMpIn0KCiAgICBhcGlfa2V5ID0gZ2V0X2FwaV9rZXkoKQogICAgZW52ID0geyoqb3MuZW52aXJvbiwgIktJUk9fQVBJX0tFWSI6IGFwaV9rZXksICJIT01FIjogIi90bXAifQogICAga2lyb19iaW4gPSAiL3Vzci9sb2NhbC9iaW4va2lyby1jbGkiCgogICAgc3RhcnQgPSB0aW1lLnRpbWUoKQogICAgcmVzdWx0ID0gc3VicHJvY2Vzcy5ydW4oCiAgICAgICAgW2tpcm9fYmluLCAiY2hhdCIsICItLW5vLWludGVyYWN0aXZlIiwgIi0tdHJ1c3QtYWxsLXRvb2xzIiwgcHJvbXB0XSwKICAgICAgICBjYXB0dXJlX291dHB1dD1UcnVlLAogICAgICAgIHRleHQ9VHJ1ZSwKICAgICAgICB0aW1lb3V0PTI4MCwKICAgICAgICBlbnY9ZW52LAogICAgKQogICAgdGltaW5nc1sia2lyb19leGVjdXRpb24iXSA9IHJvdW5kKHRpbWUudGltZSgpIC0gc3RhcnQsIDIpCiAgICB0aW1pbmdzWyJ0b3RhbCJdID0gcm91bmQodGltZS50aW1lKCkgLSB0MCwgMikKCiAgICBpZiByZXN1bHQucmV0dXJuY29kZSAhPSAwOgogICAgICAgIHJldHVybiB7CiAgICAgICAgICAgICJzdGF0dXNDb2RlIjogNTAwLAogICAgICAgICAgICAiZXJyb3IiOiByZXN1bHQuc3RkZXJyLnN0cmlwKCkgb3IgZiJraXJvLWNsaSBleGl0ZWQgd2l0aCBjb2RlIHtyZXN1bHQucmV0dXJuY29kZX0iLAogICAgICAgICAgICAidGltaW5ncyI6IHRpbWluZ3MsCiAgICAgICAgfQoKICAgIHJldHVybiB7CiAgICAgICAgInN0YXR1c0NvZGUiOiAyMDAsCiAgICAgICAgInByb21wdCI6IHByb21wdCwKICAgICAgICAicmVzdWx0IjogcmVzdWx0LnN0ZG91dC5zdHJpcCgpLAogICAgICAgICJ0aW1pbmdzIjogdGltaW5ncywKICAgIH0K" | base64 -d > handler.py
                - docker build --platform linux/arm64 -t "$ECR_REPO_URI:latest" .
            post_build:
              commands:
                - docker push "$ECR_REPO_URI:latest"

Outputs:
  EcrRepositoryUri:
    Value: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepository}'
    Export:
      Name: KiroHeadlessEcrUri
  CodeBuildProjectName:
    Value: !Ref CodeBuildProject
Full stack2-lambda.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Kiro Headless Lambda - Stack 2: Lambda Function'

Parameters:
  ImageUri:
    Type: String
    Description: ECR Image URI (from Stack 1 CodeBuild output)
  SsmParameterName:
    Type: String
    Default: /kiro/headless/api-key
    Description: handler.py内のパラメータ名と一致させること

Resources:
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: KiroHeadlessPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Sid: KiroApiKey
                Effect: Allow
                Action: ssm:GetParameter
                Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter${SsmParameterName}'
              - Sid: KmsDecrypt
                Effect: Allow
                Action: kms:Decrypt
                Resource: '*'
              - Sid: SafeReadOnly
                Effect: Allow
                Action:
                  - ec2:DescribeRegions
                  - ec2:DescribeAvailabilityZones
                Resource: '*'

  KiroLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: kiro-headless
      PackageType: Image
      Code:
        ImageUri: !Ref ImageUri
      Architectures:
        - arm64
      Role: !GetAtt LambdaRole.Arn
      Timeout: 360
      MemorySize: 512

Outputs:
  LambdaFunction:
    Value: !Ref KiroLambda
  LambdaArn:
    Value: !GetAtt KiroLambda.Arn

Deployment Steps

If you haven't obtained an API key yet, issue a Kiro API key by following the previous article.

# 1. Save the API key to SSM
aws ssm put-parameter \
  --name "/kiro/headless/api-key" \
  --type SecureString \
  --value "YOUR_KIRO_API_KEY" \
  --overwrite \
  --region us-east-1

# 2. Deploy Stack 1 (ECR + CodeBuild)
aws cloudformation deploy \
  --template-file stack1-ecr-codebuild.yaml \
  --stack-name KiroHeadlessStack1 \
  --capabilities CAPABILITY_IAM \
  --region us-east-1

# 3. Build the image with CodeBuild (about 2 minutes)
aws codebuild start-build \
  --project-name kiro-headless-lambda-build \
  --region us-east-1

# Wait for the build to finish (loop until SUCCEEDED)
BUILD_ID=$(aws codebuild list-builds-for-project \
  --project-name kiro-headless-lambda-build \
  --region us-east-1 \
  --query 'ids[0]' --output text)

while true; do
  STATUS=$(aws codebuild batch-get-builds \
    --ids "$BUILD_ID" \
    --region us-east-1 \
    --query 'builds[0].buildStatus' --output text)
  echo "Build status: $STATUS"
  [ "$STATUS" = "SUCCEEDED" ] && break
  [ "$STATUS" = "FAILED" ] && echo "Build failed!" && exit 1
  sleep 15
done

# 4. Deploy Stack 2 (Lambda)
ECR_URI=$(aws cloudformation describe-stacks \
  --stack-name KiroHeadlessStack1 \
  --region us-east-1 \
  --query 'Stacks[0].Outputs[?OutputKey==`EcrRepositoryUri`].OutputValue' --output text)

aws cloudformation deploy \
  --template-file stack2-lambda.yaml \
  --stack-name KiroHeadlessStack2 \
  --capabilities CAPABILITY_IAM \
  --parameter-overrides "ImageUri=${ECR_URI}:latest" \
  --region us-east-1

Verification

Allowed operation: Listing regions

aws lambda invoke \
  --function-name kiro-headless \
  --region us-east-1 \
  --payload '{"prompt": "アジアパシフィックのAWSリージョンを一覧して"}' \
  --cli-read-timeout 360 \
  result.json && cat result.json | sed 's/\x1b\[[0-9;]*m//g'

The output contains ANSI color codes, so they are removed with sed.

Here is an example of the result. The use_aws tool called ec2:DescribeRegions and responded in Japanese.

{
  "statusCode": 200,
  "prompt": "アジアパシフィックのAWSリージョンを一覧して",
  "result": "アジアパシフィックのAWSリージョン一覧:\n\n| リージョン名 | リージョンコード |\n|---|---|\n| アジアパシフィック (東京) | ap-northeast-1 |\n| アジアパシフィック (大阪) | ap-northeast-3 |\n| アジアパシフィック (ソウル) | ap-northeast-2 |\n| アジアパシフィック (ムンバイ) | ap-south-1 |\n| アジアパシフィック (ハイデラバード) | ap-south-2 |\n| アジアパシフィック (シンガポール) | ap-southeast-1 |\n| アジアパシフィック (シドニー) | ap-southeast-2 |\n| アジアパシフィック (ジャカルタ) | ap-southeast-3 |\n| アジアパシフィック (メルボルン) | ap-southeast-4 |\n| アジアパシフィック (香港) | ap-east-1 |\n| アジアパシフィック (マレーシア) | ap-southeast-5 |\n| アジアパシフィック (タイ) | ap-southeast-7 |",
  "timings": {
    "kiro_execution": 8.94,
    "total": 9.05
  }
}

On a cold start, Lambda's INIT Duration (about 22 seconds) is added. It is recorded in CloudWatch Logs as follows.

REPORT Duration: 18920.00 ms  Billed Duration: 19521 ms  Memory Size: 512 MB  Max Memory Used: 358 MB  Init Duration: 22045.12 ms

When warm, the value of timings.total is the response time as is.

Denied operation: Listing EC2 instances

To confirm the effect of the IAM role, I try an operation that is not allowed.

aws lambda invoke \
  --function-name kiro-headless \
  --region us-east-1 \
  --payload '{"prompt": "us-east-1のEC2インスタンス一覧を取得して表示して"}' \
  --cli-read-timeout 360 \
  result.json && cat result.json
{
  "statusCode": 200,
  "prompt": "us-east-1のEC2インスタンス一覧を取得して表示して",
  "result": "現在の実行環境(IAMロール KiroHeadlessStack2-LambdaRole)には ec2:DescribeInstances の権限が付与されていないため、EC2インスタンス一覧を取得できませんでした。\n\n対処方法:\n- このIAMロールに ec2:DescribeInstances 権限を追加する\n- または、適切な権限を持つAWSプロファイルを指定して再実行する",
  "timings": {
    "kiro_execution": 28.77,
    "total": 28.77
  }
}

Since ec2:DescribeInstances is not allowed, the IAM role blocks it even when the use_aws tool runs it. The agent detects the lack of permissions and even guides you on how to resolve it. While --trust-all-tools permits execution of all tools themselves, the actual AWS operations are controlled to least privilege by the IAM policy.

Furthermore, for high-risk operations such as launching an EC2 instance, the agent's own safety guardrails ask for confirmation even before IAM comes into play. This gives defense in depth with the IAM role plus the agent's guardrails.

Summary

By using --trust-all-tools while restricting the IAM role to ec2:Describe* only, I was able to run the use_aws tool safely on Lambda. Docker containerization also reduced the cold start from 41 seconds with the /tmp method to 22 seconds. Since it is templated with CloudFormation + CodeBuild, it can be reproduced in a few minutes with an AWS account and a Kiro API key.

AWS operations via the AWS MCP server are also an option, but Kiro's use_aws tool covers all AWS CLI operations, and combining it with Lambda + IAM roles makes permission control simple. Please give it a try.

Share this article

AWSのお困り事はクラスメソッドへ