
Safely Running Kiro CLI's use_aws Tool with Lambda + IAM Roles
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.
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/taskis 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.
