
KiroCrew公式DockerイメージをEC2で動かしてTailscale経由の接続を試してみた
はじめに
前回はEC2へKiroCrewを直接インストールし、SSHトンネルでアクセスしました。今回はKiroCrewの公式Dockerイメージをコンテナで起動し、Tailscale Serve経由でtailnet内からアクセスする構成を確認しました。
Tailscale Serveは、tailnet内の端末へローカルサービスを公開する機能です。Dockerのポート公開はループバックに限定し、tailnet内の別端末から実際につながるところまで通しました。あわせて、セッションと設定を保持するnamed volumeの構成と、アクセスログの記録内容も見ています。
検証内容
検証環境
| 項目 | 構成 |
|---|---|
| インフラ | CloudFormationで構築したAmazon EC2 |
| EC2 | t4g.medium / arm64 / Amazon Linux 2023 / ap-northeast-1 |
| ソフトウェア | Docker 25.0.16 / Compose v5.4.0 / Tailscale 1.102.2 / KiroCrew 0.1.3 |
| ストレージ | named volume(/home/kirocrew) |
| KiroCrew | ghcr.io/kirodotdev/kirocrew:stable(Docker Compose起動) |
| アクセス経路 | Tailscale Serve経由のtailnet内アクセス |
EC2インスタンス、VPC、セキュリティグループはCloudFormationテンプレートで作成し、インスタンスへの接続はSSMセッションマネージャーに寄せています。
KiroCrewをDockerコンテナで隔離する
KiroCrewは /opt/kirocrew/compose.yaml のDocker Composeで起動します。ポートの公開先はループバックアドレスに限定しました。tailnetへの公開はTailscale Serveだけが担います。
services:
kirocrew:
image: ghcr.io/kirodotdev/kirocrew:stable
ports:
- "127.0.0.1:5478:5476"
environment:
- KIROCREW_ALLOW_UNSANDBOXED=1
起動後にポートの公開状態を確認すると、意図どおりループバックだけにバインドされていました。
127.0.0.1:5478->5476/tcp
KIROCREW_ALLOW_UNSANDBOXED=1 は、隔離境界をDockerコンテナに置く今回の構成の前提として設定したもので、KiroCrew全般の安全性を保証するものではありません。
SSMで初期設定を行う
初期設定にはTailscaleの認証、Kiro CLIのdevice flow認証、kirocrew setup、Tailscale Serveの設定が含まれます。いずれも認証URLの表示やコードの入力といった対話操作を伴うため、これらの初期設定は自動化せず、SSMセッションマネージャー経由のターミナル操作としました。
aws ssm start-session --target "<INSTANCE_ID>"
cd /opt/kirocrew
# Tailscaleの認証(表示されたURLをブラウザで開いて承認)
sudo tailscale up
# コンテナ内でKiro CLIとKiroCrewの初期設定
sudo docker compose exec kirocrew kiro-cli login --use-device-flow
sudo docker compose exec kirocrew kirocrew setup
ループバックに公開したポートをTailscale Serveの転送先に指定します。
sudo tailscale serve --bg --yes "http://127.0.0.1:5478"
sudo tailscale serve status
https://<HOSTNAME>.<TAILNET_NAME>.ts.net (tailnet only)
|-- / proxy http://127.0.0.1:5478
tailnet only は、公開範囲をtailnet内に限定する設定です。今回の検証ではfunnelは使用しません。
Serve URLを後続の設定で使うため、tailscale serve statusの出力から取得します。
SERVE_URL="$(sudo tailscale serve status | awk '$1 ~ /^https:\/\// {print $1; exit}')"
case "$SERVE_URL" in
https://*) echo "Serve URL: $SERVE_URL" ;;
*) echo "ERROR: Tailscale Serve URL not found" >&2; exit 1 ;;
esac
取得したServe URLをKiroCrewへ登録して再起動します。
sudo docker compose exec -T kirocrew kirocrew config set \
dashboard.url "$SERVE_URL"
sudo docker compose restart kirocrew
sudo docker compose exec -T kirocrew kirocrew config get dashboard.url
sudo docker compose ps
curl --fail --silent http://127.0.0.1:5478/api/health
設定値、コンテナ状態、ローカルhealthの応答がいずれも期待どおりでした。
Tailscale Serve経由で接続する
Tailscaleに接続済みの別端末から、Serveで割り当てられたURLへヘルスチェックを投げました。
curl "https://<HOSTNAME>.<TAILNET_NAME>.ts.net/api/health"
{"ok": true}
ダッシュボードのアクセストークンを一時保存する
ダッシュボードを開く場合は、コンテナ内でダッシュボードトークンを発行します。tokenはチャット・ログ・Gitへ出力せず、EC2ローカルの一時ファイルへ保存します。
cd /opt/kirocrew
SERVE_URL="$(sudo tailscale serve status | awk '$1 ~ /^https:\/\// {print $1; exit}')"
TOKEN=$(sudo docker compose exec -T kirocrew kirocrew token --ttl 2h)
sudo install -m 600 /dev/null /run/kirocrew-dashboard-token
printf '%s\n' "$TOKEN" | sudo tee /run/kirocrew-dashboard-token > /dev/null
echo "Token saved to /run/kirocrew-dashboard-token (mode 600)"
echo "URL pattern: ${SERVE_URL}?token=<token>"
保存内容を確認するときは、tokenとホスト名の一部をマスクして表示します。これにより、トークンファイルの内容、パーミッション、ダッシュボード接続用URLを、実値を出さずに確認できます。
TOKEN_FROM_FILE="$(sudo cat /run/kirocrew-dashboard-token)"
MASKED_TOKEN="$(printf '%s' "$TOKEN_FROM_FILE" | sed -E 's/^(.{4}).*(.{4})$/\1...\2/')"
SERVE_HOST="${SERVE_URL#https://}"
MASKED_HOST="$(printf '%s' "$SERVE_HOST" | sed -E 's/^(.{4}).*(.{4})$/\1...\2/')"
printf 'Token file content (masked): %s\n' "$MASKED_TOKEN"
printf 'Token file mode: %s\n' "$(sudo stat -c '%a' /run/kirocrew-dashboard-token)"
printf 'Dashboard URL (masked): https://%s?token=%s\n' "$MASKED_HOST" "$MASKED_TOKEN"
ブラウザーアクセス
tailnetに接続した端末のブラウザーで、次のURLを開きます。URLの token には、/run/kirocrew-dashboard-token に一時保存した値を使用します。
https://<HOSTNAME>.<TAILNET_NAME>.ts.net?token=<token>
ブラウザーでのアクセスが終わったら、EC2上の一時ファイルを削除します。
sudo rm -f /run/kirocrew-dashboard-token
セッションと設定の永続化
Kiro CLIの認証情報とKiroCrewの設定は /home/kirocrew 以下に書き出されます。/home/kirocrew をnamed volumeとしてマウントし、コンテナの書き込み先を1箇所へ集約しています。
services:
kirocrew:
volumes:
- kirocrew-home:/home/kirocrew
volumes:
kirocrew-home:
access.log もKiroCrewが同じディレクトリへ書き出します。ログ関連の設定(PYTHONPATH と sitecustomize.py のマウント)は付録の compose.yaml を参照してください。ブラウザーからアクセスした後、パーミッションと直近の記録を見てみます。
cd /opt/kirocrew
sudo docker compose exec -T kirocrew stat -c '%a %n' /home/kirocrew/access.log
sudo docker compose exec -T kirocrew sh -c 'tail -n 50 /home/kirocrew/access.log'
実際の出力を抜粋します。
600 /home/kirocrew/access.log
2026-08-06T02:34:55+0000 INFO aiohttp.access: http_request method=POST path=/api/chat status=200 duration_ms=3.4
2026-08-06T02:35:15+0000 INFO aiohttp.access: http_request method=GET path=/api/health status=200 duration_ms=0.5
ダッシュボードトークンはクエリ文字列で渡しますが、access.log には残っていませんでした。本検証では独自のアクセスロガー(付録の sitecustomize.py)を PYTHONPATH 経由で読み込ませており、その結果として記録されたのは method、path、status、duration_ms の4項目です。ファイルのパーミッションは600です。
まとめ
Dockerコンテナを隔離境界にし、Tailscale Serveでtailnet内へ接続する構成にすると、EC2上のKiroCrewへSSHトンネルに依存しない経路でアクセスできます。複数の端末から同じKiroCrewをtailnet内に公開範囲を限定したトンネル経由で利用したい場合は、この構成を試してみてください。
付録: 再現用ファイル
本文で使用した構成を再現するためのファイルをまとめます。テンプレートとスクリプトには検証時の実ファイルを掲載しています。認証URL、device code、authkey、dashboard tokenなどの秘密情報は含めていません。
CloudFormationテンプレート
templates/kirocrew-ec2.yaml の全文です。
CloudFormationテンプレート(全文)
AWSTemplateFormatVersion: "2010-09-09"
Description: >
Kiro Crew EC2 (ARM64/t4g.medium/AL2023) in ap-northeast-1.
Tailscale + Docker + KiroCrew + KiroCLI via UserData.
SSM-only management. No inbound ports. IMDSv2 required.
Self-contained: VPC, Subnet, IGW, RouteTable, SG, IAM Role/Profile, EC2.
# ====================================================================
# Parameters
# ====================================================================
Parameters:
InstanceType:
Type: String
Default: t4g.medium
AllowedValues: [t4g.small, t4g.medium, t4g.large]
Description: EC2 instance type (ARM64 Graviton2)
AmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64
Description: Amazon Linux 2023 ARM64 AMI (SSM Parameter)
RootVolumeSize:
Type: Number
Default: 20
MinValue: 20
MaxValue: 100
Description: Root EBS volume size in GiB
ProjectName:
Type: String
Default: kirocrew-cfn
Description: Project name used for resource naming and tags
DeployDate:
Type: String
Default: "20260806"
Description: Deployment date suffix (YYYYMMDD) for resource naming
# ====================================================================
# Resources
# ====================================================================
Resources:
# ------------------------------------------------------------------
# VPC & Network
# ------------------------------------------------------------------
KiroCrewVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/24
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub "${ProjectName}-vpc-${DeployDate}"
- Key: Project
Value: !Ref ProjectName
- Key: ManagedBy
Value: CloudFormation
KiroCrewInternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub "${ProjectName}-igw-${DeployDate}"
- Key: Project
Value: !Ref ProjectName
KiroCrewVPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref KiroCrewVPC
InternetGatewayId: !Ref KiroCrewInternetGateway
KiroCrewSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref KiroCrewVPC
CidrBlock: 10.0.0.0/24
AvailabilityZone: ap-northeast-1a
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub "${ProjectName}-subnet-${DeployDate}"
- Key: Project
Value: !Ref ProjectName
KiroCrewRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref KiroCrewVPC
Tags:
- Key: Name
Value: !Sub "${ProjectName}-rtb-${DeployDate}"
- Key: Project
Value: !Ref ProjectName
KiroCrewDefaultRoute:
Type: AWS::EC2::Route
DependsOn: KiroCrewVPCGatewayAttachment
Properties:
RouteTableId: !Ref KiroCrewRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref KiroCrewInternetGateway
KiroCrewSubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref KiroCrewSubnet
RouteTableId: !Ref KiroCrewRouteTable
# ------------------------------------------------------------------
# Security Group: inbound完全クローズ, outboundはTailscale/Docker/SSM用HTTPS
# ------------------------------------------------------------------
KiroCrewSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub "${ProjectName}-sg-${DeployDate}"
GroupDescription: "Kiro Crew EC2: No inbound. Outbound HTTPS SSM Docker Tailscale"
VpcId: !Ref KiroCrewVPC
# Inbound: 完全クローズ (SSMはポート不要)
SecurityGroupIngress: []
# Outbound: HTTPS + Tailscale UDP
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: HTTPS for SSM endpoints, Docker pull, Tailscale control
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Description: HTTP for package mirrors (dnf)
- IpProtocol: udp
FromPort: 41641
ToPort: 41641
CidrIp: 0.0.0.0/0
Description: Tailscale WireGuard UDP
- IpProtocol: udp
FromPort: 3478
ToPort: 3478
CidrIp: 0.0.0.0/0
Description: Tailscale DERP/STUN
Tags:
- Key: Name
Value: !Sub "${ProjectName}-sg-${DeployDate}"
- Key: Project
Value: !Ref ProjectName
- Key: ManagedBy
Value: CloudFormation
# ------------------------------------------------------------------
# IAM Role: SSM Core + ECR (KiroCrew image pull)
# ------------------------------------------------------------------
KiroCrewEC2Role:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub "${ProjectName}-ec2-role-${DeployDate}"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
# SSMによるインスタンス管理
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
Tags:
- Key: Project
Value: !Ref ProjectName
- Key: ManagedBy
Value: CloudFormation
KiroCrewEC2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub "${ProjectName}-ec2-profile-${DeployDate}"
Roles:
- !Ref KiroCrewEC2Role
# ------------------------------------------------------------------
# EC2 Instance
# ------------------------------------------------------------------
KiroCrewEC2Instance:
Type: AWS::EC2::Instance
DependsOn:
- KiroCrewVPCGatewayAttachment
- KiroCrewDefaultRoute
Properties:
InstanceType: !Ref InstanceType
ImageId: !Ref AmiId
SubnetId: !Ref KiroCrewSubnet
IamInstanceProfile: !Ref KiroCrewEC2InstanceProfile
SecurityGroupIds:
- !Ref KiroCrewSecurityGroup
# IMDSv2必須: HopLimit=2 (コンテナ内からメタデータ取得可能)
MetadataOptions:
HttpTokens: required
HttpPutResponseHopLimit: 2
HttpEndpoint: enabled
# ルートボリューム: 暗号化gp3
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeType: gp3
VolumeSize: !Ref RootVolumeSize
Encrypted: true
DeleteOnTermination: true
# UserDataはbootstrap.shをbase64エンコードして埋め込む
UserData:
Fn::Base64: !Sub |
#!/bin/bash
# Kiro Crew EC2 ARM64 bootstrap: SSM + Docker + Tailscale + KiroCrew
# CFN Stack: ${AWS::StackName} / Region: ${AWS::Region}
# Date: ${DeployDate}
#
# Human steps (NOT automated here):
# 1. sudo tailscale up
# 2. sudo docker compose exec kirocrew kiro-cli login --use-device-flow
# 3. sudo docker compose exec kirocrew kirocrew setup
# 4. sudo tailscale serve --bg --yes http://127.0.0.1:5478
# -> See runbooks/human-operations-runbook.md for full details
set -euo pipefail
LOG=/var/log/kirocrew-bootstrap.log
exec > >(tee -a "$LOG") 2>&1
echo "===== Kiro Crew bootstrap started: $(date -Is) ====="
echo "Stack: ${AWS::StackName}"
echo "Region: ${AWS::Region}"
# AL2023のSSM Agentを有効化
systemctl enable --now amazon-ssm-agent || true
# ssm-user作成 + sudo権限付与
useradd -m -s /bin/bash ssm-user 2>/dev/null || true
echo 'ssm-user ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ssm-agent-users
chmod 440 /etc/sudoers.d/ssm-agent-users
# Docker
dnf install -y docker
systemctl enable --now docker
usermod -aG docker ssm-user || true
# Docker Compose ARM64プラグイン (pinned v5.4.0)
COMPOSE_VERSION=v5.4.0
install -d -m 0755 /usr/local/lib/docker/cli-plugins
curl --fail --silent --show-error --location \
"https://github.com/docker/compose/releases/download/${!COMPOSE_VERSION}/docker-compose-linux-aarch64" \
--output /usr/local/lib/docker/cli-plugins/docker-compose
chmod 0755 /usr/local/lib/docker/cli-plugins/docker-compose
# swapfile (メモリ補助 1GiB)
if ! swapon --show --noheadings | grep -q . && [ ! -e /swapfile ]; then
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
# Tailscale (認証は人間が実施)
curl -fsSL https://tailscale.com/install.sh | sh
systemctl enable --now tailscaled
# KiroCrew作業ディレクトリ
install -d -m 0755 /opt/kirocrew
# HTTP access log: クエリ文字列・認証情報を記録しない専用ロガー
cat > /opt/kirocrew/kirocrew-safe-access-log-sitecustomize.py <<'PY'
"""Safe query-free aiohttp access logging for Kiro Crew gateway."""
from __future__ import annotations
import logging, os
from logging.handlers import RotatingFileHandler
try:
from aiohttp import web_log
from aiohttp.web_runner import AppRunner
class SafeAccessLogger(web_log.AbstractAccessLogger):
def log(self, request, response, time):
path = getattr(request, "path", "<unknown>")
if not isinstance(path, str): path = "<unknown>"
self.logger.info("http_request method=%s path=%s status=%s duration_ms=%.1f",
getattr(request,"method","<unknown>"), path[:512],
getattr(response,"status","<unknown>"), time*1000.0)
_orig = AppRunner.__init__
def _safe_init(self, app, *, handle_signals=False, access_log_class=SafeAccessLogger, **kw):
return _orig(self, app, handle_signals=handle_signals, access_log_class=access_log_class, **kw)
if not getattr(AppRunner,"_kirocrew_safe_access_logging",False):
AppRunner.__init__ = _safe_init
AppRunner._kirocrew_safe_access_logging = True
al = logging.getLogger("aiohttp.access")
al.setLevel(logging.INFO); al.propagate = True
lp = os.environ.get("KIROCREW_ACCESS_LOG_FILE","/home/kirocrew/access.log")
if not any(getattr(h,"_kirocrew_safe_access",False) for h in al.handlers):
h = RotatingFileHandler(lp, maxBytes=2*1024*1024, backupCount=3, encoding="utf-8")
h.setLevel(logging.INFO)
h.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s",datefmt="%Y-%m-%dT%H:%M:%S%z"))
h._kirocrew_safe_access = True; al.addHandler(h)
try: os.chmod(lp, 0o600)
except OSError: pass
except Exception: pass
PY
chmod 0644 /opt/kirocrew/kirocrew-safe-access-log-sitecustomize.py
# Docker Composeファイル
cat > /opt/kirocrew/compose.yaml <<'COMPOSE'
name: kirocrew-ec2
services:
kirocrew:
image: ghcr.io/kirodotdev/kirocrew:stable
container_name: kirocrew-ec2-gateway
restart: unless-stopped
ports:
- "127.0.0.1:${!KIROCREW_HOST_PORT:?Set KIROCREW_HOST_PORT}:5476"
volumes:
- kirocrew-ec2-home:/home/kirocrew
- ./kirocrew-safe-access-log-sitecustomize.py:/opt/kirocrew/patches/sitecustomize.py:ro
environment:
PYTHONPATH: /opt/kirocrew/patches
KIROCREW_ACCESS_LOG_FILE: /home/kirocrew/access.log
KIROCREW_ALLOW_UNSANDBOXED: ${!KIROCREW_ALLOW_UNSANDBOXED:?Set KIROCREW_ALLOW_UNSANDBOXED=1 after operator approval}
volumes:
kirocrew-ec2-home:
name: ${!KIROCREW_VOLUME_NAME:?Set KIROCREW_VOLUME_NAME}
COMPOSE
# 環境変数ファイル
VOLUME_SUFFIX=$(date +%Y%m%d)
cat > /opt/kirocrew/.env <<ENV
KIROCREW_HOST_PORT=5478
KIROCREW_VOLUME_NAME=kirocrew-ec2-home-cfn-${!VOLUME_SUFFIX}
KIROCREW_ALLOW_UNSANDBOXED=1
ENV
chmod 600 /opt/kirocrew/.env
# コンテナ起動
cd /opt/kirocrew
docker compose config --quiet
docker compose pull
docker compose up -d --force-recreate
# ヘルスチェック (最大120秒)
for attempt in $(seq 1 60); do
if curl --fail --silent http://127.0.0.1:5478/api/health >/dev/null 2>&1; then
echo "Kiro Crew health: OK (attempt=${!attempt})"
break
fi
if [ "${!attempt}" -eq 60 ]; then
echo "ERROR: health check failed"
docker compose logs --tail=100 kirocrew || true
exit 1
fi
sleep 2
done
# sandbox設定
docker compose exec -T kirocrew kirocrew config set \
agent.sandbox_allow_unsandboxed_exec true || true
echo "--- versions ---"
docker --version
docker compose version
tailscale version
echo "--- container status ---"
docker compose ps
echo "===== bootstrap completed: $(date -Is) ====="
Tags:
- Key: Name
Value: !Sub "${ProjectName}-ec2-${DeployDate}"
- Key: Project
Value: !Ref ProjectName
- Key: ManagedBy
Value: CloudFormation
- Key: SandboxPosture
Value: container-only
- Key: BootstrapDate
Value: !Ref DeployDate
# ====================================================================
# Outputs
# ====================================================================
Outputs:
InstanceId:
Description: EC2 Instance ID
Value: !Ref KiroCrewEC2Instance
Export:
Name: !Sub "${AWS::StackName}-InstanceId"
VpcId:
Description: VPC ID
Value: !Ref KiroCrewVPC
Export:
Name: !Sub "${AWS::StackName}-VpcId"
SubnetId:
Description: Subnet ID
Value: !Ref KiroCrewSubnet
Export:
Name: !Sub "${AWS::StackName}-SubnetId"
SecurityGroupId:
Description: Security Group ID
Value: !Ref KiroCrewSecurityGroup
Export:
Name: !Sub "${AWS::StackName}-SecurityGroupId"
IAMRoleArn:
Description: IAM Role ARN
Value: !GetAtt KiroCrewEC2Role.Arn
SSMConnectCommand:
Description: SSM Session Manager connect command
Value: !Sub "aws ssm start-session --target ${KiroCrewEC2Instance} --region ${AWS::Region}"
BootstrapLogCommand:
Description: Bootstrap log tail command via SSM
Value: !Sub "aws ssm send-command --instance-ids ${KiroCrewEC2Instance} --document-name AWS-RunShellScript --parameters commands='tail -100 /var/log/kirocrew-bootstrap.log' --region ${AWS::Region}"
Docker Composeファイル
UserDataで /opt/kirocrew/compose.yaml に配置するCompose定義です。Composeは ghcr.io/kirodotdev/kirocrew:stable を使用し、ホスト側の公開先をloopbackに限定します。
compose.yaml
name: kirocrew-ec2
services:
kirocrew:
image: ghcr.io/kirodotdev/kirocrew:stable
container_name: kirocrew-ec2-gateway
restart: unless-stopped
ports:
- "127.0.0.1:${KIROCREW_HOST_PORT:?Set KIROCREW_HOST_PORT}:5476"
volumes:
- kirocrew-ec2-home:/home/kirocrew
- ./kirocrew-safe-access-log-sitecustomize.py:/opt/kirocrew/patches/sitecustomize.py:ro
environment:
PYTHONPATH: /opt/kirocrew/patches
KIROCREW_ACCESS_LOG_FILE: /home/kirocrew/access.log
# AL2023/Docker runtimeではsandbox無効化が必要。コンテナが唯一の隔離境界。
KIROCREW_ALLOW_UNSANDBOXED: ${KIROCREW_ALLOW_UNSANDBOXED:?Set KIROCREW_ALLOW_UNSANDBOXED=1 after operator approval}
volumes:
kirocrew-ec2-home:
name: ${KIROCREW_VOLUME_NAME:?Set KIROCREW_VOLUME_NAME}
UserData bootstrap
EC2初期構築時に実行する userdata/bootstrap.sh の全文です。
userdata/bootstrap.sh(全文)
#!/bin/bash
# Kiro Crew EC2 ARM64 bootstrap: SSM + Docker + Tailscale + KiroCrew
# 2026-08-06 ap-northeast-1 CloudFormation用 UserData
#
# 人間が実施する手順 (UserDataでは行わない):
# - sudo tailscale up # Tailscale認証
# - sudo docker compose exec kirocrew kiro-cli login --use-device-flow # KiroCLI認証
# - sudo docker compose exec kirocrew kirocrew setup # KiroCrew初期設定
# - sudo tailscale serve --bg --yes http://127.0.0.1:5478 # Tailscale Serve設定
# - Runbook: runbooks/human-operations-runbook.md 参照
#
# セキュリティポスチャ:
# - IMDSv2必須 (MetadataOptions: HttpTokens=required)
# - SSMのみ管理アクセス (inboundポート不要)
# - DockerはloopbackのみPublish (127.0.0.1:5478)
# - Tailscale ServeはtailnetのみPublish (funnel不使用)
set -euo pipefail
LOG=/var/log/kirocrew-bootstrap.log
exec > >(tee -a "$LOG") 2>&1
echo "===== Kiro Crew bootstrap started: $(date -Is) ====="
# AL2023にはSSM Agentが含まれている。起動を確認する。
systemctl enable --now amazon-ssm-agent || true
# ssm-userを作成してDockerグループに追加・sudo権限付与
useradd -m -s /bin/bash ssm-user 2>/dev/null || true
echo 'ssm-user ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ssm-agent-users
chmod 440 /etc/sudoers.d/ssm-agent-users
# Dockerをインストールする。AL2023はdnfを使う。
dnf install -y docker
systemctl enable --now docker
usermod -aG docker ssm-user || true
# Docker Compose ARM64プラグイン (固定バージョン)
COMPOSE_VERSION=v5.4.0
install -d -m 0755 /usr/local/lib/docker/cli-plugins
curl --fail --silent --show-error --location \
"https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-aarch64" \
--output /usr/local/lib/docker/cli-plugins/docker-compose
chmod 0755 /usr/local/lib/docker/cli-plugins/docker-compose
# swapfile (4GiBインスタンスのメモリ補助)
if ! swapon --show --noheadings | grep -q . && [ ! -e /swapfile ]; then
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
fi
# Tailscaleをインストールするが認証は人間が実施する
curl -fsSL https://tailscale.com/install.sh | sh
systemctl enable --now tailscaled
# KiroCrew作業ディレクトリ作成
install -d -m 0755 /opt/kirocrew
# HTTP access log: クエリ文字列・認証情報を記録しない専用ロガー
cat > /opt/kirocrew/kirocrew-safe-access-log-sitecustomize.py <<'PY'
"""Safe query-free aiohttp access logging for the Kiro Crew gateway.
PYTHONPATH/sitecustomize.py 経由でロードされる。
記録対象: HTTP method, path(クエリなし), status, duration_ms のみ。
query, header, cookie, authorization, IP, body は記録しない。
"""
from __future__ import annotations
import logging
import os
from logging.handlers import RotatingFileHandler
try:
from aiohttp import web_log
from aiohttp.web_runner import AppRunner
class SafeAccessLogger(web_log.AbstractAccessLogger):
def log(self, request, response, time):
path = getattr(request, "path", "<unknown>")
if not isinstance(path, str):
path = "<unknown>"
self.logger.info(
"http_request method=%s path=%s status=%s duration_ms=%.1f",
getattr(request, "method", "<unknown>"),
path[:512],
getattr(response, "status", "<unknown>"),
time * 1000.0,
)
_original_init = AppRunner.__init__
def _safe_app_runner_init(self, app, *, handle_signals=False, access_log_class=SafeAccessLogger, **kwargs):
return _original_init(self, app, handle_signals=handle_signals, access_log_class=access_log_class, **kwargs)
if not getattr(AppRunner, "_kirocrew_safe_access_logging", False):
AppRunner.__init__ = _safe_app_runner_init
AppRunner._kirocrew_safe_access_logging = True
access_logger = logging.getLogger("aiohttp.access")
access_logger.setLevel(logging.INFO)
access_logger.propagate = True
log_path = os.environ.get("KIROCREW_ACCESS_LOG_FILE", "/home/kirocrew/access.log")
if not any(getattr(h, "_kirocrew_safe_access", False) for h in access_logger.handlers):
handler = RotatingFileHandler(log_path, maxBytes=2 * 1024 * 1024, backupCount=3, encoding="utf-8")
handler.setLevel(logging.INFO)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S%z"))
handler._kirocrew_safe_access = True
access_logger.addHandler(handler)
try:
os.chmod(log_path, 0o600)
except OSError:
pass
except Exception:
pass
PY
chmod 0644 /opt/kirocrew/kirocrew-safe-access-log-sitecustomize.py
# Docker Composeファイル
cat > /opt/kirocrew/compose.yaml <<'COMPOSE'
name: kirocrew-ec2
services:
kirocrew:
image: ghcr.io/kirodotdev/kirocrew:stable
container_name: kirocrew-ec2-gateway
restart: unless-stopped
ports:
- "127.0.0.1:${KIROCREW_HOST_PORT:?Set KIROCREW_HOST_PORT}:5476"
volumes:
- kirocrew-ec2-home:/home/kirocrew
- ./kirocrew-safe-access-log-sitecustomize.py:/opt/kirocrew/patches/sitecustomize.py:ro
environment:
PYTHONPATH: /opt/kirocrew/patches
KIROCREW_ACCESS_LOG_FILE: /home/kirocrew/access.log
# AL2023/Docker runtimeではsandbox無効化が必要。コンテナが唯一の隔離境界。
KIROCREW_ALLOW_UNSANDBOXED: ${KIROCREW_ALLOW_UNSANDBOXED:?Set KIROCREW_ALLOW_UNSANDBOXED=1 after operator approval}
volumes:
kirocrew-ec2-home:
name: ${KIROCREW_VOLUME_NAME:?Set KIROCREW_VOLUME_NAME}
COMPOSE
# 環境変数ファイル (mode 600)
VOLUME_SUFFIX=$(date +%Y%m%d)
cat > /opt/kirocrew/.env <<ENV
KIROCREW_HOST_PORT=5478
KIROCREW_VOLUME_NAME=kirocrew-ec2-home-cfn-${VOLUME_SUFFIX}
# AL2023/Docker runtimeでKiro CLI・agentを動かすために必要。
# コンテナが唯一の隔離境界となることをoperatorが明示承認。
KIROCREW_ALLOW_UNSANDBOXED=1
ENV
chmod 600 /opt/kirocrew/.env
# コンテナ起動
cd /opt/kirocrew
docker compose config --quiet
docker compose pull
docker compose up -d --force-recreate
# ヘルスチェック (最大120秒待機)
for attempt in $(seq 1 60); do
if curl --fail --silent http://127.0.0.1:5478/api/health >/dev/null 2>&1; then
echo "Kiro Crew health: OK (attempt=${attempt})"
break
fi
if [ "$attempt" -eq 60 ]; then
echo "ERROR: Kiro Crew health check failed after 120s"
docker compose logs --tail=100 kirocrew || true
exit 1
fi
sleep 2
done
# sandbox設定
docker compose exec -T kirocrew kirocrew config set \
agent.sandbox_allow_unsandboxed_exec true || true
# バージョン確認ログ
echo "--- versions ---"
docker --version
docker compose version
tailscale version
echo "--- container status ---"
docker compose ps
printf '%s\n' '====================================='
printf '%s\n' 'Bootstrap completed. Human steps:'
printf '%s\n' ' 1. sudo tailscale up'
printf '%s\n' ' 2. sudo tailscale status'
printf '%s\n' ' 3. cd /opt/kirocrew && sudo docker compose exec kirocrew kiro-cli login --use-device-flow'
printf '%s\n' ' 4. sudo docker compose exec kirocrew kirocrew setup'
printf '%s\n' ' 5. sudo tailscale serve --bg --yes http://127.0.0.1:5478'
printf '%s\n' ' 6. See runbooks/human-operations-runbook.md for full details'
printf '%s\n' '====================================='
echo "===== Kiro Crew bootstrap completed: $(date -Is) ====="
SSM有人操作Runbook
SSMセッション開始後の認証、初期設定、Tailscale Serveの設定、ダッシュボードトークンの取得、接続確認、access.log確認までの手順です。Tailscale ACLの具体例は今回の検証範囲に含めません。
human-operations-runbook.md
# Kiro Crew EC2 - 人間操作 Runbook
CloudFormationでデプロイしたEC2に対して、人間がSSMセッション経由で実施する手順です。
## 対象環境
| 項目 | 値 |
|---|---|
| リージョン | ap-northeast-1 |
| スタック名 | `kirocrew-cfn-20260806`(デプロイ時に指定したもの) |
| Docker host port | `127.0.0.1:5478` |
| Compose directory | `/opt/kirocrew` |
| Bootstrap log | `/var/log/kirocrew-bootstrap.log` |
> **注意**: 認証URL、device code、Tailscaleトークン、dashboard tokenをこのRunbook、チャット、Gitへ保存しないでください。
---
## 事前確認: Bootstrap完了とSSM Online
CFnデプロイ後、EC2がSSM Onlineになるまで2〜5分待ちます。
```bash
# InstanceIdはCFnスタックのOutputsから取得
STACK_NAME=kirocrew-cfn-20260806
REGION=ap-northeast-1
INSTANCE_ID=$(aws cloudformation describe-stacks \
--stack-name "$STACK_NAME" \
--region "$REGION" \
--query 'Stacks[0].Outputs[?OutputKey==`InstanceId`].OutputValue' \
--output text)
echo "InstanceId: $INSTANCE_ID"
# SSM Online確認 (Online になるまで繰り返す)
aws ssm describe-instance-information \
--region "$REGION" \
--filters "Key=InstanceIds,Values=$INSTANCE_ID" \
--query 'InstanceInformationList[0].{Id:InstanceId,PingStatus:PingStatus,PlatformType:PlatformType}' \
--output table
```
SSMセッションを開きます。
```bash
aws ssm start-session --target "$INSTANCE_ID" --region "$REGION"
```
Bootstrap完了を確認します(UserDataの最終行)。
```bash
sudo tail -30 /var/log/kirocrew-bootstrap.log
```
期待する末尾:
```
===== bootstrap completed: 2026-08-06T...
```
コンテナとヘルスを確認します。
```bash
cd /opt/kirocrew
sudo docker compose ps
curl --fail --silent http://127.0.0.1:5478/api/health
```
期待値: `{"ok": true}`
---
## Step 1: Tailscale 認証
### 1-1. Tailscale up (認証URL表示)
SSMセッション内で実行します。
```bash
sudo tailscale up
```
表示された認証URLをブラウザで開き、ログインを完了します。
> 認証URLをこのRunbook・チャット・Gitへ保存しないでください。
### 1-2. Tailscale status 確認
```bash
sudo tailscale status
sudo tailscale version
```
`Connected` と表示されることを確認します。
---
## Step 2: Kiro CLI ライセンス認証
### 2-1. device-flow認証
SSMセッション内で実行します。
```bash
cd /opt/kirocrew
sudo docker compose exec kirocrew kiro-cli login --use-device-flow
```
表示されたURLとdevice codeを使ってブラウザでログインします。
> 認証URL、device codeをこのRunbook・チャット・Gitへ保存しないでください。
### 2-2. 認証確認
```bash
sudo docker compose exec -T kirocrew kiro-cli whoami 2>/dev/null || \
sudo docker compose exec -T kirocrew kiro-cli status 2>/dev/null || true
```
---
## Step 3: Kiro Crew 初期設定
### 3-1. kirocrew setup
SSMセッション内で実行します(対話操作が必要)。
```bash
sudo docker compose exec kirocrew kirocrew setup
```
推奨設定:
| 項目 | 推奨値 |
|---|---|
| Workspace | デフォルト(そのままEnter) |
| Slack | スキップ(no) |
| Slash command | `kirocrew` |
| Timezone | `Asia/Tokyo` |
| Playwright MCP | インストールする(yes) |
| AWS cloud launch | スキップ(no) |
### 3-2. Kiro Crew doctor
```bash
sudo docker compose exec -T kirocrew kirocrew doctor
sudo docker compose ps
```
---
## Step 4: Tailscale Serve 設定
### 4-1. Serve 起動 (tailnet only)
```bash
sudo tailscale serve --bg --yes "http://127.0.0.1:5478"
```
### 4-2. Serve URL 取得
```bash
sudo tailscale serve status
```
期待値の例(hostnameは各環境で異なる):
```
https://<TAILSCALE_HOSTNAME> (tailnet only)
|-- / proxy http://127.0.0.1:5478
```
`tailnet only` と表示されることを確認します。`funnel` は使用しません。
### 4-3. Serve URL を変数に設定
```bash
SERVE_URL="$(sudo tailscale serve status | awk '$1 ~ /^https:\/\// {print $1; exit}')"
case "$SERVE_URL" in
https://*) echo "Serve URL: $SERVE_URL" ;;
*) echo "ERROR: Tailscale Serve URL not found" >&2; exit 1 ;;
esac
```
---
## Step 5: Kiro Crew に Serve URL を登録
```bash
cd /opt/kirocrew
sudo docker compose exec -T kirocrew kirocrew config set \
dashboard.url "$SERVE_URL"
sudo docker compose restart kirocrew
```
設定を確認します。
```bash
sudo docker compose exec -T kirocrew kirocrew config get dashboard.url
sudo docker compose ps
curl --fail --silent http://127.0.0.1:5478/api/health
```
---
## Step 6: Dashboard Token 取得
### 6-1. 短期トークン発行
```bash
cd /opt/kirocrew
TOKEN=$(sudo docker compose exec -T kirocrew kirocrew token --ttl 2h)
```
### 6-2. ブラウザ接続用URLをファイルへ一時保存
> tokenをチャット・ログ・Gitへ出力しないでください。
```bash
# EC2ローカルのみに一時保存 (mode 600)
echo "$TOKEN" | sudo tee /run/kirocrew-dashboard-token > /dev/null
sudo chmod 600 /run/kirocrew-dashboard-token
echo "Token saved to /run/kirocrew-dashboard-token (mode 600)"
echo "URL pattern: ${SERVE_URL}?token=<token>"
```
### 6-3. token読み取り・使用・削除
SSMセッション内で実行します。
```bash
# 読み取り (token値はこのRunbookへコピーしない)
sudo cat /run/kirocrew-dashboard-token
# ブラウザでアクセス後、即時削除
sudo rm -f /run/kirocrew-dashboard-token
```
---
## Step 7: 接続確認
### 7-1. EC2ホスト上
```bash
curl --fail --silent "http://127.0.0.1:5478/api/health"
sudo docker compose ps
sudo tailscale serve status
```
### 7-2. Tailscale接続済みの別端末から
tailnetに接続したMac等から実行します(SSHトンネル不要)。
```bash
SERVE_URL="<tailscale serve statusで確認したURL>"
curl --connect-timeout 5 --max-time 10 --fail "${SERVE_URL}/api/health"
```
期待値: `{"ok": true}`
### 7-3. ブラウザアクセス
tailnetに接続した端末のブラウザで以下を開きます。
```
https://<TAILSCALE_HOSTNAME>?token=<token>
```
---
## Step 8: Access Log 確認
```bash
cd /opt/kirocrew
# ファイルパーミッション確認
sudo docker compose exec -T kirocrew stat -c '%a %n' /home/kirocrew/access.log
# 直近50行確認 (method, path, status, duration_msのみ記録)
sudo docker compose exec -T kirocrew sh -c 'tail -n 50 /home/kirocrew/access.log'
```
access logはmethod・queryなしpath・HTTP status・duration_msのみを記録します。
query string、header、cookie、authorization、IP、bodyは記録しません。
---
## トラブルシューティング
### `Host header not allowed.`
```bash
cd /opt/kirocrew
SERVE_URL="$(sudo tailscale serve status | awk '$1 ~ /^https:\/\// {print $1; exit}')"
sudo docker compose exec -T kirocrew kirocrew config set dashboard.url "$SERVE_URL"
sudo docker compose restart kirocrew
curl --fail --silent "http://127.0.0.1:5478/api/health"
```
### Bootstrap未完了
```bash
sudo tail -50 /var/log/kirocrew-bootstrap.log
sudo systemctl status docker
sudo docker compose -f /opt/kirocrew/compose.yaml ps
```
### コンテナが起動しない
```bash
cd /opt/kirocrew
cat .env # シークレット値は含まれない
sudo docker compose logs --tail=50 kirocrew
sudo docker compose up -d --force-recreate
```
---
## 停止・ロールバック
Tailscale Serve のみ停止する場合:
```bash
sudo tailscale serve --https=443 off
```
Tailscale Serve を停止してlocalhostのみに戻す場合:
```bash
cd /opt/kirocrew
sudo tailscale serve --https=443 off
sudo docker compose exec -T kirocrew kirocrew config set \
dashboard.url "http://localhost:5478"
sudo docker compose restart kirocrew
```
---
## セキュリティ上の注意
- Tailscale Serve は `tailnet only` で使用し、`tailscale funnel` は使用しない
- Docker のポートを `0.0.0.0:5478` へ変更しない(loopbackのみ)
- Tailscale ACL でアクセス可能なユーザー・デバイスを制限する
- Kiro Crew の短期ダッシュボードトークンを共有・保存しない
- `KIROCREW_ALLOW_UNSANDBOXED=1` はコンテナが唯一の隔離境界となることを意味する。認証情報のコンテナ内保存リスクをoperatorが承認したうえで運用する
- SSM セッションは操作後に `exit` で切断する










