こんにちは、広野です。
以前、code-server を nginx で HTTPS 化して Amazon Cognito で MFA 認証をさせる構成を紹介しました。本記事では、同じ Amazon EC2 インスタンス上で AI エージェントの Kiro CLI と Kiro Crew を立ち上げる構成を紹介します。AWS Systems Manager (SSM) ドキュメント実装編です。
実装は 3つの AWS CloudFormation テンプレートに分けてデプロイします。本記事はその 3つ目で、EC2 インスタンスの中にソフトウェアを導入する SSM ドキュメントを作るテンプレートを紹介します。シリーズの最終回です。
- Cognito 編
- EC2 インスタンス編
- SSM ドキュメント編 (本記事)
作成物
このテンプレートをデプロイすると、EC2 インスタンス編でデプロイしたインスタンス限定で使用できる SSM ドキュメント (Command タイプ) が 6つ作成されます。ドキュメント名は <SystemName>-<SubName>- で始まります。
各ドキュメントが導入するものと、再実行したときにバージョンアップされるかどうかを示します。表の列の意味は、次のとおりです。
- バージョン
- 指定: ドキュメントを実行するときのパラメータで、バージョンを指定します。テンプレートのパラメータで、既定値も変えられます。
- 最新: 実行した時点の最新版が入ります。
- OS の版:
dnf installで入ります。AMI を作成した時点のリポジトリの版になります。 -: バージョンの概念がないものです。
- 再実行時の更新
- される: 再実行すると、バージョンアップされます。「指定」のものは、別のバージョンを指定して再実行すると、そのバージョンに切り替わります。
- されない: 導入済みの場合は何もしません。バージョンアップするには、別の方法が必要です。
Setup-CodeServer
事前に実行が必要なドキュメント: なし
| 導入するもの | バージョン | 再実行時の更新 |
|---|---|---|
| code-server | 最新 | される |
| oauth2-proxy | 指定 | される |
| nginx、certbot | OS の版 | されない |
| VS Code 拡張機能 | 最新 | されない |
| Let’s Encrypt の証明書、git の設定 | – | されない |
Setup-NodePython
事前に実行が必要なドキュメント: なし
| 導入するもの | バージョン | 再実行時の更新 |
|---|---|---|
| nvm | 指定 | される |
| Node.js | 指定 | される |
| npm、npm-check-updates、license-checker-rseidelsohn | 最新 | される |
| pyenv | 最新 | されない |
| Python | 指定 | される |
| pytest | 最新 | される |
Setup-PythonModules
事前に実行が必要なドキュメント: Setup-NodePython
| 導入するもの | バージョン | 再実行時の更新 |
|---|---|---|
| 特定の Python パッケージ | 最新 | される |
| Playwright と Chromium | 最新 | される |
Setup-KiroCli
事前に実行が必要なドキュメント: なし
| 導入するもの | バージョン | 再実行時の更新 |
|---|---|---|
| Kiro CLI | 最新 | されない |
| uv | 最新 | されない |
MCP サーバーの設定、仮のステアリングファイル AGENTS.md |
– | されない |
Setup-KiroCrew
事前に実行が必要なドキュメント: Setup-CodeServer、Setup-NodePython、Setup-KiroCli
| 導入するもの | バージョン | 再実行時の更新 |
|---|---|---|
| Kiro Crew | 最新 | されない |
| 専用の証明書、専用の oauth2-proxy、nginx の設定 | – | されない |
Setup-LibreOffice
事前に実行が必要なドキュメント: なし
| 導入するもの | バージョン | 再実行時の更新 |
|---|---|---|
| LibreOffice | 最新の安定版 | される |
SSM ドキュメントでセットアップする利点
以前の構成では、ソフトウェアのインストールを EC2 の UserData にすべて書いていました。主な理由、つまり利点は以下の通りです。
- UserData は、インスタンスの初回起動時にしか実行されない。途中で失敗すると、やり直しがきかず、手動でのリカバリが大変だった。SSM ドキュメントにすることで、何度でも再実行できるのが魅力。
- UserData には 16KB のサイズ上限がある。SSM ドキュメントであれば分割してコマンドを実行できるので、上限を気にせず済む。
- ソフトウェアのカテゴリにドキュメントを分けていて、必要なものだけを選んで実行できる。例えば Kiro Crew を使わない人は、
Setup-KiroCrewを実行しなくてよい。 - 実行するタイミングを自分で決められる。後から必要になったから追加する、でもよい。
- 今回の設計では、元々 EC2 インスタンスの構築時に環境情報を
environment.jsonに保存している。それを読み込むことで、SSM ドキュメントに環境固有の設定を含めるまたはインプットにすることなく、同じドキュメントを再利用できる。 - 今回の設計では、ソフトウェアのバージョンを、ドキュメントのパラメータとして実行時に指定できる。常時最新をインストールするものもあり、バージョンアップにも使用できる。
- SSH で接続する必要がない。Run Command は、インスタンスの SSM エージェントが AWS と通信してコマンドを実行可能。
今回の設計では、実行は手動にしています。EC2 インスタンスの初回構築時に自動実行もできるのですが、任意で必要なものを選択できる利便性を優先しました。
前提事項
- Cognito 編と EC2 インスタンス編のスタックが作成済みであること。
設計のポイント
共通するブロック
ドキュメントは、スクリプトの冒頭に共通するブロックを持っています。テンプレート中、
# ===== Common block
から
# ===== End of common block =====
までの部分です。ドキュメント名を示す DOC_NAME の値だけが異なります。このブロックでは、以下を実施しています。
- エラーがあれば停止する。(
set -eEuo pipefail) - 秘密情報が表示されないように、
set -xは使用しない。 - root で実行されていることを確認する。
- ログを
/var/log/ide-setup/<ドキュメント名>-<日時>.logに保存する。同じ内容を標準出力にも出すので、Run Command の画面にも表示される。ログは CloudWatch エージェントが CloudWatch Logs のide-setupロググループに送信する。 - 一時フォルダを作成し、終了時に必ず削除する。
- エラーが起きたら、スクリプトの何行目で失敗したかを出力する。
environment.jsonを検証する。ファイルが存在すること、JSON として正しいこと、schemaVersionが一致すること、必要な項目がすべて入っていること、の 4点。どれかが満たされなければ、何も変更せずに失敗する。検証後、environment.jsonの値をシェル変数に取り込む。
schemaVersion の確認は、次の部分です。
[ "$(env_get schemaVersion)" = "$SUPPORTED_SCHEMA_VERSION" ] \
|| fail "schemaVersion mismatch (JSON: $(env_get schemaVersion), this document: ${SUPPORTED_SCHEMA_VERSION}). The PerPerson template and this document are out of sync."
EC2 インスタンス編のテンプレートと、このテンプレートのずれを検知するための仕掛けです。environment.json の項目を変更したときには、environment.json および SSM ドキュメントの両方の値を更新します。
再実行可能にする (冪等性)
すべてのドキュメントは、何度実行しても同じ結果になるように作られています。(予期せぬエラー以外は)
- すでに同じバージョンがインストールされているものはスキップする。例えば、同じバージョンの Python があれば、再インストールしない。
- 発行済みの証明書は、再発行しない。Let’s Encrypt には発行枚数の上限があるため、再実行で消費しない。
- 設定ファイルは内容が変わったときだけ書き換えて、サービスを再起動する。
- 開発者が編集するファイルは、すでに存在すれば上書きしない。対象は、code-server の
settings.json、git の設定、Kiro CLI のmcp.jsonとAGENTS.md。 - 秘密情報も引き継ぐ。oauth2-proxy の Cookie シークレットは、既存のものがあれば同じ値を使用する。再実行によって、ログイン中のユーザーのセッションが無効にならないようにするため。
AWS CloudFormation テンプレート
パラメータの DefaultOauth2ProxyVersion、DefaultNvmVersion、DefaultNodeVersion、DefaultPythonVersion は、ドキュメント実行時のパラメータの既定値になります。実行時に別のバージョンを指定することもできます。ソフトウェアのバージョンは、本テンプレート作成時点のものです。
AWSTemplateFormatVersion: "2010-09-09"
Description: The CloudFormation template that creates the account-common SSM Command documents for setting up the code-server environment.
# ------------------------------------------------------------#
# General Information
#
# Last update: 2026-10-30
# Author: Hirono
# ------------------------------------------------------------#
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
SystemName:
Type: String
Description: System name. use lower case only. (e.g. example)
Default: example
MaxLength: 10
MinLength: 1
AllowedPattern: "^[a-z0-9]+$"
SubName:
Type: String
Description: System sub name. use lower case only. (e.g. prod or dev)
Default: dev
MaxLength: 10
MinLength: 1
AllowedPattern: "^[a-z0-9]+$"
DefaultOauth2ProxyVersion:
Type: String
Description: Default oauth2-proxy version for Setup-CodeServer. The latest is 7.15.5 as of 2026-10-15. (https://github.com/oauth2-proxy/oauth2-proxy/releases)
Default: "7.15.5"
AllowedPattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
DefaultNvmVersion:
Type: String
Description: Default nvm version for Setup-NodePython. The latest is 0.40.8 as of 2026-10-15. (https://github.com/nvm-sh/nvm/releases)
Default: "0.40.8"
AllowedPattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
DefaultNodeVersion:
Type: String
Description: Default Node.js version for Setup-NodePython. The latest LTS is 24.22.0 as of 2026-10-17. (https://nodejs.org/en/about/previous-releases)
Default: "24.22.0"
AllowedPattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
DefaultPythonVersion:
Type: String
Description: Default Python version for Setup-NodePython. The latest 3.14 series is 3.14.8 as of 2026-10-17. (https://www.python.org/downloads/)
Default: "3.14.8"
AllowedPattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "General Configuration"
Parameters:
- SystemName
- SubName
- Label:
default: "Default Software Versions"
Parameters:
- DefaultOauth2ProxyVersion
- DefaultNvmVersion
- DefaultNodeVersion
- DefaultPythonVersion
cfn-lint:
config:
ignore_checks:
- "W3005"
Resources:
# ------------------------------------------------------------#
# SSM Documents
# ------------------------------------------------------------#
DocumentSetupCodeServer:
Type: AWS::SSM::Document
Properties:
Name: !Sub "${SystemName}-${SubName}-Setup-CodeServer"
DocumentType: Command
DocumentFormat: YAML
TargetType: /AWS::EC2::Instance
UpdateMethod: NewVersion
Content:
schemaVersion: "2.2"
description: "Sets up code-server, nginx, oauth2-proxy, certificates, git, and VS Code extensions."
parameters:
Oauth2ProxyVersion:
type: String
description: "oauth2-proxy version (e.g. 7.15.5). Check the latest at https://github.com/oauth2-proxy/oauth2-proxy/releases"
default: !Ref DefaultOauth2ProxyVersion
allowedPattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
mainSteps:
- action: aws:runShellScript
name: SetupCodeServer
inputs:
timeoutSeconds: "1800"
runCommand:
- |
#!/bin/bash
# shellcheck disable=SC2034 # variables from the common block are not all used by every document
# ===== Common block (identical in all Setup-* documents except DOC_NAME) =====
set -eEuo pipefail # Fail as failure. Never use "set -x" (it could print secrets).
umask 022
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DOC_NAME="Setup-CodeServer"
LOG_DIR="/var/log/ide-setup"
CONFIG_DIR="/etc/ide-setup-config"
ENV_JSON="${CONFIG_DIR}/environment.json"
SUPPORTED_SCHEMA_VERSION="1"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
fail() { log "[ERROR] $*" >&2; exit 1; }
step_begin() { log "[START] $*"; }
step_end() { log "[DONE] $*"; }
[ "$(id -u)" -eq 0 ] || { echo "[ERROR] This document must run as root." >&2; exit 1; }
# Make HOME explicit (it may be unset under Run Command).
# Work for ec2-user uses "sudo -u ec2-user -H".
export HOME=/root
mkdir -p "$LOG_DIR"
LOG_FILE="${LOG_DIR}/${DOC_NAME}-$(date '+%Y%m%d-%H%M%S').log"
exec > >(tee -a "$LOG_FILE") 2>&1
TMP_DIR=$(mktemp -d /tmp/ide-setup.XXXXXX)
trap 'rm -rf "$TMP_DIR"' EXIT
trap 'log "[ERROR] Command failed at line ${LINENO} (exit code: $?)" >&2' ERR
log "Starting ${DOC_NAME} (log file: ${LOG_FILE})"
step_begin "Check the environment info JSON"
[ -f "$ENV_JSON" ] || fail "${ENV_JSON} was not found. UserData may still be running. Wait a few minutes and run this document again. (Check: cloud-init status)"
jq -e . "$ENV_JSON" > /dev/null || fail "${ENV_JSON} is not valid JSON."
env_get() { jq -r --arg k "$1" '(.[$k] // empty) | tostring' "$ENV_JSON"; }
[ "$(env_get schemaVersion)" = "$SUPPORTED_SCHEMA_VERSION" ] \
|| fail "schemaVersion mismatch (JSON: $(env_get schemaVersion), this document: ${SUPPORTED_SCHEMA_VERSION}). The PerPerson template and this document are out of sync."
for key in systemName subName yourId yourEmail region domainName timeZone \
codeserverFqdn kirocrewFqdn userPoolId userPoolClientId \
userPoolProviderUrl cognitoDomain workDir; do
[ -n "$(env_get "$key")" ] || fail "Key '${key}' is missing or empty in ${ENV_JSON}."
done
SYSTEM_NAME=$(env_get systemName); SUB_NAME=$(env_get subName)
YOUR_ID=$(env_get yourId); YOUR_EMAIL=$(env_get yourEmail)
REGION=$(env_get region); DOMAIN_NAME=$(env_get domainName)
TIME_ZONE=$(env_get timeZone)
CODESERVER_FQDN=$(env_get codeserverFqdn); KIROCREW_FQDN=$(env_get kirocrewFqdn)
USER_POOL_ID=$(env_get userPoolId); USER_POOL_CLIENT_ID=$(env_get userPoolClientId)
USER_POOL_PROVIDER_URL=$(env_get userPoolProviderUrl)
COGNITO_DOMAIN=$(env_get cognitoDomain); WORK_DIR=$(env_get workDir)
step_end "Check the environment info JSON (${SYSTEM_NAME}-${SUB_NAME} / ${YOUR_ID} / ${REGION})"
# ===== End of common block =====
# ----- Check that the requested oauth2-proxy release exists (before any change) -----
# Document parameter (already validated by allowedPattern)
OAUTH2_PROXY_VERSION="{{ Oauth2ProxyVersion }}"
OAUTH2_PROXY_BASE_URL="https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v${OAUTH2_PROXY_VERSION}"
OAUTH2_PROXY_TARBALL="oauth2-proxy-v${OAUTH2_PROXY_VERSION}.linux-amd64.tar.gz"
OAUTH2_PROXY_BIN="/usr/local/bin/oauth2-proxy"
step_begin "Check that oauth2-proxy v${OAUTH2_PROXY_VERSION} exists"
curl -fsSL --retry 3 -o "${TMP_DIR}/${OAUTH2_PROXY_TARBALL}-sha256sum.txt" \
"${OAUTH2_PROXY_BASE_URL}/${OAUTH2_PROXY_TARBALL}-sha256sum.txt" \
|| fail "Could not download the checksum of oauth2-proxy v${OAUTH2_PROXY_VERSION}. The version may not exist, or GitHub is unreachable. Check https://github.com/oauth2-proxy/oauth2-proxy/releases and run again. Nothing has been changed."
step_end "oauth2-proxy v${OAUTH2_PROXY_VERSION} exists"
# ----- Packages -----
step_begin "Install packages (certbot, nginx)"
dnf install -y certbot python3-certbot-dns-route53 nginx
step_end "Install packages (certbot, nginx)"
# ----- Work folder -----
step_begin "Create the work folder ${WORK_DIR}"
install -d -o ec2-user -g ec2-user -m 755 "$WORK_DIR" # sets the owner of this folder only (no recursive chown)
step_end "Create the work folder ${WORK_DIR}"
# ----- code-server -----
step_begin "Install or update code-server"
CS_VERSION_BEFORE=""
# Read the version from the rpm database: running "code-server --version" as root creates /root/.config/code-server
# and mixes log lines into the output.
if rpm -q --quiet code-server; then
CS_VERSION_BEFORE=$(rpm -q --qf '%{VERSION}-%{RELEASE}' code-server)
fi
# The installer does not compare versions: "rpm -U" fails (exit code 2) when the same version is installed.
# So decide here, in the same way as the installer (the redirect target of releases/latest).
CS_LATEST=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/coder/code-server/releases/latest)
CS_LATEST="${CS_LATEST#https://github.com/coder/code-server/releases/tag/}"
CS_LATEST="${CS_LATEST#v}"
[[ "$CS_LATEST" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "Could not determine the latest code-server version (got: '${CS_LATEST}')."
CS_INSTALLED_VERSION=""
if rpm -q --quiet code-server; then
CS_INSTALLED_VERSION=$(rpm -q --qf '%{VERSION}' code-server)
fi
if [ "$CS_INSTALLED_VERSION" = "$CS_LATEST" ]; then
log "code-server ${CS_INSTALLED_VERSION} is already the latest. Skipping the installer."
else
log "Installing code-server ${CS_LATEST} (installed: ${CS_INSTALLED_VERSION:-none})."
curl -fsSL https://code-server.dev/install.sh | bash
fi
rpm -q --quiet code-server || fail "code-server is not installed as an rpm package."
CS_VERSION_AFTER=$(rpm -q --qf '%{VERSION}-%{RELEASE}' code-server)
log "code-server version: before=${CS_VERSION_BEFORE:-none} / after=${CS_VERSION_AFTER}"
step_end "Install or update code-server"
step_begin "Configure code-server and start the service"
CS_CONFIG_DIR=/home/ec2-user/.config/code-server
CS_USER_DIR=/home/ec2-user/.local/share/code-server/User
# Create as ec2-user so that parent folders (.config, .local, ...) are not owned by root
sudo -u ec2-user -H mkdir -p "$CS_CONFIG_DIR" "$CS_USER_DIR"
# config.yaml: always overwrite (part of the architecture). Restart only when the content changed.
cat > "${TMP_DIR}/config.yaml" << 'EOF'
bind-addr: 127.0.0.1:8008
auth: none
cert: false
EOF
CS_CONFIG_CHANGED=0
if ! cmp -s "${TMP_DIR}/config.yaml" "${CS_CONFIG_DIR}/config.yaml"; then
install -m 644 -o ec2-user -g ec2-user "${TMP_DIR}/config.yaml" "${CS_CONFIG_DIR}/config.yaml"
CS_CONFIG_CHANGED=1
log "Wrote ${CS_CONFIG_DIR}/config.yaml"
else
log "${CS_CONFIG_DIR}/config.yaml is unchanged."
fi
# settings.json: create only if it does not exist (the developer may edit it in the IDE)
if [ -e "${CS_USER_DIR}/settings.json" ]; then
log "${CS_USER_DIR}/settings.json already exists. Not changed."
else
cat > "${TMP_DIR}/settings.json" << EOF
{
"extensions.autoUpdate": false,
"extensions.autoCheckUpdates": false,
"terminal.integrated.cwd": "${WORK_DIR}",
"telemetry.telemetryLevel": "off",
"security.workspace.trust.startupPrompt": "never",
"security.workspace.trust.enabled": false,
"security.workspace.trust.banner": "never",
"security.workspace.trust.emptyWindow": false,
"editor.indentSize": "tabSize",
"editor.tabSize": 2,
"python.testing.pytestEnabled": true
}
EOF
install -m 644 -o ec2-user -g ec2-user "${TMP_DIR}/settings.json" "${CS_USER_DIR}/settings.json"
log "Created ${CS_USER_DIR}/settings.json"
fi
systemctl enable --now code-server@ec2-user
if systemctl is-active --quiet code-server@ec2-user \
&& { [ "$CS_CONFIG_CHANGED" -eq 1 ] || { [ -n "$CS_VERSION_BEFORE" ] && [ "$CS_VERSION_BEFORE" != "$CS_VERSION_AFTER" ]; }; }; then
log "Restarting code-server (config or version changed). Reload the browser tab afterwards."
systemctl restart code-server@ec2-user
fi
step_end "Configure code-server and start the service"
# ----- git -----
step_begin "Configure git"
git_set_default() { # Set a global value only when it is not set yet
local key="$1" value="$2" current
current=$(sudo -u ec2-user -H git config --global --get "$key" || true)
if [ -z "$current" ]; then
sudo -u ec2-user -H git config --global "$key" "$value"
log "git config ${key}: set"
else
log "git config ${key}: already set. Not changed."
fi
}
git_set_default user.name "$YOUR_ID"
git_set_default user.email "$YOUR_EMAIL"
git_set_default credential.helper '!aws codecommit credential-helper $@'
git_set_default credential.UseHttpPath true
git_set_default init.defaultBranch main
step_end "Configure git"
# ----- VS Code extensions -----
step_begin "Install VS Code extensions"
for ext in adamraichu.pdf-viewer bierner.markdown-mermaid shd101wyy.markdown-preview-enhanced; do
sudo -u ec2-user -H code-server --install-extension "$ext"
done
sudo -u ec2-user -H code-server --list-extensions --show-versions
step_end "Install VS Code extensions"
# ----- oauth2-proxy binary -----
step_begin "Install oauth2-proxy v${OAUTH2_PROXY_VERSION}"
INSTALLED_VERSION=""
if [ -x "$OAUTH2_PROXY_BIN" ]; then
INSTALLED_VERSION=$("$OAUTH2_PROXY_BIN" --version | awk '{print $2}' | sed 's/^v//') || true
fi
if [ "$INSTALLED_VERSION" = "$OAUTH2_PROXY_VERSION" ]; then
log "oauth2-proxy v${INSTALLED_VERSION} is already installed. Skipping."
else
curl -fsSL --retry 3 -o "${TMP_DIR}/${OAUTH2_PROXY_TARBALL}" "${OAUTH2_PROXY_BASE_URL}/${OAUTH2_PROXY_TARBALL}"
(cd "$TMP_DIR" && sha256sum -c "${OAUTH2_PROXY_TARBALL}-sha256sum.txt") # fails the document on mismatch
tar -xzf "${TMP_DIR}/${OAUTH2_PROXY_TARBALL}" -C "$TMP_DIR"
install -m 755 -o root -g root \
"${TMP_DIR}/oauth2-proxy-v${OAUTH2_PROXY_VERSION}.linux-amd64/oauth2-proxy" "${OAUTH2_PROXY_BIN}.new"
# Stop only this document's service, then swap atomically (cp over a running binary fails with "Text file busy")
if systemctl is-active --quiet oauth2-proxy; then systemctl stop oauth2-proxy; fi
mv -f "${OAUTH2_PROXY_BIN}.new" "$OAUTH2_PROXY_BIN"
log "Installed oauth2-proxy v${OAUTH2_PROXY_VERSION} (previous: ${INSTALLED_VERSION:-none})"
fi
mkdir -p "$CONFIG_DIR"
echo "$OAUTH2_PROXY_VERSION" > "${CONFIG_DIR}/oauth2-proxy.version"
step_end "Install oauth2-proxy v${OAUTH2_PROXY_VERSION}"
# ----- Certificate -----
step_begin "Issue the certificate for ${CODESERVER_FQDN}"
LE_LIVE="/etc/letsencrypt/live/${CODESERVER_FQDN}"
if [ -s "${LE_LIVE}/fullchain.pem" ] && [ -s "${LE_LIVE}/privkey.pem" ]; then
log "The certificate already exists. Not re-issued (use renew-cert-codeserver.sh to renew)."
else
certbot certonly --dns-route53 -d "$CODESERVER_FQDN" --agree-tos -m "$YOUR_EMAIL" \
--non-interactive --preferred-challenges dns-01
fi
step_end "Issue the certificate for ${CODESERVER_FQDN}"
# ----- oauth2-proxy service -----
step_begin "Configure the oauth2-proxy service"
OP_UNIT=/etc/systemd/system/oauth2-proxy.service
# Allowed email file (also read by oauth2-proxy-kirocrew)
install -d -o ec2-user -g ec2-user -m 700 /etc/oauth2-proxy
printf '%s\n' "$YOUR_EMAIL" > /etc/oauth2-proxy/allowed_emails.txt
chown ec2-user:ec2-user /etc/oauth2-proxy/allowed_emails.txt
chmod 600 /etc/oauth2-proxy/allowed_emails.txt
# Client secret: always fetched from Cognito (never printed)
CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client \
--user-pool-id "$USER_POOL_ID" --client-id "$USER_POOL_CLIENT_ID" --region "$REGION" \
--query 'UserPoolClient.ClientSecret' --output text)
{ [ -n "$CLIENT_SECRET" ] && [ "$CLIENT_SECRET" != "None" ]; } \
|| fail "Could not get the client secret of the Cognito app client."
# Cookie secret: keep the existing one, otherwise generate (URL-safe base64 of 32 bytes)
COOKIE_SECRET=""
if [ -f "$OP_UNIT" ]; then
COOKIE_SECRET=$(sed -n 's/^Environment=COOKIE_SECRET=//p' "$OP_UNIT" | head -1)
fi
if [ -z "$COOKIE_SECRET" ]; then
COOKIE_SECRET=$(openssl rand -base64 32 | tr '+/' '-_')
log "Generated a new cookie secret."
else
log "Kept the existing cookie secret."
fi
# shellcheck disable=SC2016 # the single-quoted $CLIENT_SECRET and $COOKIE_SECRET are expanded by systemd, not by the shell
OP_ARGS=(
--provider=oidc
"--oidc-issuer-url=${USER_POOL_PROVIDER_URL}"
"--client-id=${USER_POOL_CLIENT_ID}"
'--client-secret=${CLIENT_SECRET}' # expanded by systemd from Environment=
"--redirect-url=https://${CODESERVER_FQDN}/oauth2/callback"
"--login-url=https://${COGNITO_DOMAIN}/oauth2/authorize"
"--profile-url=https://${COGNITO_DOMAIN}/oauth2/userInfo"
"--redeem-url=https://${COGNITO_DOMAIN}/oauth2/token"
--scope=openid
--cookie-secure=true
--cookie-expire=72h
--cookie-refresh=1h
'--cookie-secret=${COOKIE_SECRET}' # expanded by systemd from Environment=
--authenticated-emails-file=/etc/oauth2-proxy/allowed_emails.txt
--http-address=127.0.0.1:4180
)
{
cat << 'EOF'
[Unit]
Description=oauth2-proxy
After=network.target
[Service]
Type=simple
EOF
printf 'Environment=CLIENT_SECRET=%s\n' "$CLIENT_SECRET"
printf 'Environment=COOKIE_SECRET=%s\n' "$COOKIE_SECRET"
printf 'ExecStart=%s' "$OAUTH2_PROXY_BIN"; printf ' %s' "${OP_ARGS[@]}"; printf '\n'
cat << 'EOF'
Restart=on-failure
User=ec2-user
Group=ec2-user
[Install]
WantedBy=multi-user.target
EOF
} > "${TMP_DIR}/oauth2-proxy.service"
OP_UNIT_CHANGED=0
if ! cmp -s "${TMP_DIR}/oauth2-proxy.service" "$OP_UNIT"; then
install -m 600 -o root -g root "${TMP_DIR}/oauth2-proxy.service" "$OP_UNIT" # 600: it contains secrets
OP_UNIT_CHANGED=1
log "Wrote ${OP_UNIT} (contents are not shown because they contain secrets)."
else
log "${OP_UNIT} is unchanged."
fi
systemctl daemon-reload
systemctl enable oauth2-proxy
# Restart only when the definition changed or the service is not running (e.g. stopped for a binary swap)
if [ "$OP_UNIT_CHANGED" -eq 1 ] || ! systemctl is-active --quiet oauth2-proxy; then
systemctl restart oauth2-proxy
fi
# Health check: /ping must return 200
PING_CODE=""
for _ in $(seq 1 30); do
PING_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:4180/ping || true)
[ "$PING_CODE" = "200" ] && break
sleep 1
done
[ "$PING_CODE" = "200" ] \
|| fail "oauth2-proxy /ping did not return 200 (last: ${PING_CODE:-none}). Check 'journalctl -u oauth2-proxy'. If the version is the cause, run this document again with another Oauth2ProxyVersion."
unset CLIENT_SECRET COOKIE_SECRET
step_end "Configure the oauth2-proxy service (version $(cat "${CONFIG_DIR}/oauth2-proxy.version"), /ping OK)"
# ----- nginx -----
step_begin "Configure nginx for code-server"
install -d -m 700 /etc/nginx/ssl
install -m 644 "${LE_LIVE}/fullchain.pem" /etc/nginx/ssl/nginx-chain.crt
install -m 600 "${LE_LIVE}/privkey.pem" /etc/nginx/ssl/nginx.key
cat > "${TMP_DIR}/codeserver.conf.tpl" << 'EOF'
server {
listen 443 ssl;
server_name __CODESERVER_FQDN__;
large_client_header_buffers 4 16k;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
ssl_certificate /etc/nginx/ssl/nginx-chain.crt;
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
proxy_pass http://127.0.0.1:8008;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /oauth2/ {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Auth-Request-Redirect $request_uri;
proxy_buffers 16 16k;
proxy_buffer_size 16k;
}
location = /oauth2/auth {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
proxy_buffers 16 16k;
proxy_buffer_size 16k;
}
}
EOF
NGINX_CONF=/etc/nginx/conf.d/codeserver.conf
if [ -f "$NGINX_CONF" ]; then cp -p "$NGINX_CONF" "${TMP_DIR}/codeserver.conf.bak"; fi
sed "s#__CODESERVER_FQDN__#${CODESERVER_FQDN}#g" "${TMP_DIR}/codeserver.conf.tpl" > "$NGINX_CONF"
if ! nginx -t; then # roll back so that a bad file does not break other documents' reload
if [ -f "${TMP_DIR}/codeserver.conf.bak" ]; then cp -p "${TMP_DIR}/codeserver.conf.bak" "$NGINX_CONF"; else rm -f "$NGINX_CONF"; fi
fail "nginx -t failed. The previous nginx configuration was restored."
fi
systemctl enable nginx
if systemctl is-active --quiet nginx; then systemctl reload nginx; else systemctl start nginx; fi
step_end "Configure nginx for code-server"
# ----- Certificate renewal script -----
step_begin "Create the certificate renewal script"
cat > "${TMP_DIR}/renew-cert-codeserver.sh.tpl" << 'EOF'
#!/bin/bash
# Renews the certificate of __CODESERVER_FQDN__ (code-server).
# Usage: sudo /home/ec2-user/renew-cert-codeserver.sh
set -euo pipefail
[ "$(id -u)" -eq 0 ] || { echo "Run this script with sudo." >&2; exit 1; }
FQDN="__CODESERVER_FQDN__"
LIVE="/etc/letsencrypt/live/${FQDN}"
certbot renew --cert-name "$FQDN"
if cmp -s "${LIVE}/fullchain.pem" /etc/nginx/ssl/nginx-chain.crt \
&& cmp -s "${LIVE}/privkey.pem" /etc/nginx/ssl/nginx.key; then
echo "The certificate is not due for renewal yet. Nothing was changed."
exit 0
fi
install -m 644 "${LIVE}/fullchain.pem" /etc/nginx/ssl/nginx-chain.crt
install -m 600 "${LIVE}/privkey.pem" /etc/nginx/ssl/nginx.key
nginx -t
systemctl reload nginx
echo "The certificate was renewed and nginx was reloaded."
EOF
sed "s#__CODESERVER_FQDN__#${CODESERVER_FQDN}#g" "${TMP_DIR}/renew-cert-codeserver.sh.tpl" > /home/ec2-user/renew-cert-codeserver.sh
chown ec2-user:ec2-user /home/ec2-user/renew-cert-codeserver.sh
chmod 755 /home/ec2-user/renew-cert-codeserver.sh
step_end "Create the certificate renewal script"
# ----- Sign-out link -----
step_begin "Create signout.md"
cat > "${WORK_DIR}/signout.md" << EOF
[Sign Out](https://${COGNITO_DOMAIN}/logout?client_id=${USER_POOL_CLIENT_ID}&logout_uri=https%3A%2F%2F${CODESERVER_FQDN}%2Foauth2%2Fsign_out)
EOF
chown ec2-user:ec2-user "${WORK_DIR}/signout.md"
chmod 644 "${WORK_DIR}/signout.md"
step_end "Create signout.md"
log "[COMPLETE] ${DOC_NAME} finished successfully."
Tags:
- Key: Cost
Value: !Sub "${SystemName}-${SubName}"
DocumentSetupNodePython:
Type: AWS::SSM::Document
Properties:
Name: !Sub "${SystemName}-${SubName}-Setup-NodePython"
DocumentType: Command
DocumentFormat: YAML
TargetType: /AWS::EC2::Instance
UpdateMethod: NewVersion
Content:
schemaVersion: "2.2"
description: "Sets up nvm, Node.js, npm tools, pyenv, Python, and pytest."
parameters:
NvmVersion:
type: String
description: "nvm version (e.g. 0.40.8). Check the latest at https://github.com/nvm-sh/nvm/releases"
default: !Ref DefaultNvmVersion
allowedPattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
NodeVersion:
type: String
description: "Node.js version (e.g. 24.22.0). Kiro Crew requires 22.12 or later. Check the LTS at https://nodejs.org/en/about/previous-releases"
default: !Ref DefaultNodeVersion
allowedPattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
PythonVersion:
type: String
description: "Python version (e.g. 3.14.8). Kiro Crew requires 3.12 or later. Check the latest at https://www.python.org/downloads/"
default: !Ref DefaultPythonVersion
allowedPattern: "^[0-9]+\\.[0-9]+\\.[0-9]+$"
mainSteps:
- action: aws:runShellScript
name: SetupNodePython
inputs:
timeoutSeconds: "1800"
runCommand:
- |
#!/bin/bash
# shellcheck disable=SC2034 # variables from the common block are not all used by every document
# ===== Common block (identical in all Setup-* documents except DOC_NAME) =====
set -eEuo pipefail # Fail as failure. Never use "set -x" (it could print secrets).
umask 022
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DOC_NAME="Setup-NodePython"
LOG_DIR="/var/log/ide-setup"
CONFIG_DIR="/etc/ide-setup-config"
ENV_JSON="${CONFIG_DIR}/environment.json"
SUPPORTED_SCHEMA_VERSION="1"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
fail() { log "[ERROR] $*" >&2; exit 1; }
step_begin() { log "[START] $*"; }
step_end() { log "[DONE] $*"; }
[ "$(id -u)" -eq 0 ] || { echo "[ERROR] This document must run as root." >&2; exit 1; }
# Make HOME explicit (it may be unset under Run Command).
# Work for ec2-user uses "sudo -u ec2-user -H".
export HOME=/root
mkdir -p "$LOG_DIR"
LOG_FILE="${LOG_DIR}/${DOC_NAME}-$(date '+%Y%m%d-%H%M%S').log"
exec > >(tee -a "$LOG_FILE") 2>&1
TMP_DIR=$(mktemp -d /tmp/ide-setup.XXXXXX)
trap 'rm -rf "$TMP_DIR"' EXIT
trap 'log "[ERROR] Command failed at line ${LINENO} (exit code: $?)" >&2' ERR
log "Starting ${DOC_NAME} (log file: ${LOG_FILE})"
step_begin "Check the environment info JSON"
[ -f "$ENV_JSON" ] || fail "${ENV_JSON} was not found. UserData may still be running. Wait a few minutes and run this document again. (Check: cloud-init status)"
jq -e . "$ENV_JSON" > /dev/null || fail "${ENV_JSON} is not valid JSON."
env_get() { jq -r --arg k "$1" '(.[$k] // empty) | tostring' "$ENV_JSON"; }
[ "$(env_get schemaVersion)" = "$SUPPORTED_SCHEMA_VERSION" ] \
|| fail "schemaVersion mismatch (JSON: $(env_get schemaVersion), this document: ${SUPPORTED_SCHEMA_VERSION}). The PerPerson template and this document are out of sync."
for key in systemName subName yourId yourEmail region domainName timeZone \
codeserverFqdn kirocrewFqdn userPoolId userPoolClientId \
userPoolProviderUrl cognitoDomain workDir; do
[ -n "$(env_get "$key")" ] || fail "Key '${key}' is missing or empty in ${ENV_JSON}."
done
SYSTEM_NAME=$(env_get systemName); SUB_NAME=$(env_get subName)
YOUR_ID=$(env_get yourId); YOUR_EMAIL=$(env_get yourEmail)
REGION=$(env_get region); DOMAIN_NAME=$(env_get domainName)
TIME_ZONE=$(env_get timeZone)
CODESERVER_FQDN=$(env_get codeserverFqdn); KIROCREW_FQDN=$(env_get kirocrewFqdn)
USER_POOL_ID=$(env_get userPoolId); USER_POOL_CLIENT_ID=$(env_get userPoolClientId)
USER_POOL_PROVIDER_URL=$(env_get userPoolProviderUrl)
COGNITO_DOMAIN=$(env_get cognitoDomain); WORK_DIR=$(env_get workDir)
step_end "Check the environment info JSON (${SYSTEM_NAME}-${SUB_NAME} / ${YOUR_ID} / ${REGION})"
# ===== End of common block =====
# Document parameters (already validated by allowedPattern)
NVM_VERSION="{{ NvmVersion }}"
NODE_VERSION="{{ NodeVersion }}"
PYTHON_VERSION="{{ PythonVersion }}"
EC2_HOME=/home/ec2-user
# ----- Check that the requested versions exist (no change is made) -----
step_begin "Check that the requested versions exist"
curl -fsSL --retry 3 -o "${TMP_DIR}/nvm-install.sh" \
"https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh" \
|| fail "nvm v${NVM_VERSION} was not found (or GitHub is unreachable). Check https://github.com/nvm-sh/nvm/releases. Nothing has been changed."
# Node.js: index.tab is the list that "nvm ls-remote" reads (nvm itself may not be installed yet)
curl -fsSL --retry 3 -o "${TMP_DIR}/node-index.tab" https://nodejs.org/dist/index.tab \
|| fail "Could not download the Node.js version list. Nothing has been changed."
awk -F'\t' -v v="v${NODE_VERSION}" '$1 == v && $3 ~ /(^|,)linux-x64(,|$)/ { found = 1 } END { exit !found }' \
"${TMP_DIR}/node-index.tab" \
|| fail "Node.js v${NODE_VERSION} (linux-x64) was not found. Check https://nodejs.org/en/about/previous-releases. Nothing has been changed."
# Python: coarse check on python.org (pyenv checks again after it is installed)
curl -fsSL --retry 3 -I -o /dev/null \
"https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz" \
|| fail "Python ${PYTHON_VERSION} was not found on python.org (or it is unreachable). Check https://www.python.org/downloads/. Nothing has been changed."
step_end "Check that the requested versions exist"
# ----- Packages (git is installed by UserData) -----
step_begin "Install the build dependencies for pyenv"
dnf install -y gcc make patch zlib-devel bzip2 bzip2-devel xz-devel libffi-devel \
readline-devel sqlite sqlite-devel openssl-devel tk-devel libuuid-devel gdbm-devel ncurses-devel
step_end "Install the build dependencies for pyenv"
# ----- nvm -----
step_begin "Install nvm v${NVM_VERSION}"
sudo -u ec2-user -H touch "${EC2_HOME}/.bashrc"
NVM_INSTALLED_VERSION=$(sudo -u ec2-user -H bash -c \
'[ -s "$HOME/.nvm/nvm.sh" ] && . "$HOME/.nvm/nvm.sh" && nvm --version' 2> /dev/null || true)
if [ "$NVM_INSTALLED_VERSION" = "$NVM_VERSION" ]; then
log "nvm v${NVM_INSTALLED_VERSION} is already installed. Skipping."
else
# Re-running the install script is the official way to install or update nvm
# shellcheck disable=SC2024 # the redirect must be opened by root: TMP_DIR is readable only by root
sudo -u ec2-user -H env PROFILE="${EC2_HOME}/.bashrc" bash < "${TMP_DIR}/nvm-install.sh"
log "Installed nvm v${NVM_VERSION} (previous: ${NVM_INSTALLED_VERSION:-none})"
fi
step_end "Install nvm v${NVM_VERSION}"
# ----- Node.js and global npm tools (runs as ec2-user) -----
step_begin "Install Node.js v${NODE_VERSION} and npm tools"
read -r -d '' NODE_SCRIPT << 'EOF' || true
set -eo pipefail # no "-u": nvm.sh is not nounset-safe
NODE_VERSION="$1"
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
if nvm version "$NODE_VERSION" > /dev/null 2>&1; then
echo "Node.js v${NODE_VERSION} is already installed. Skipping."
else
nvm install "$NODE_VERSION"
fi
nvm alias default "$NODE_VERSION"
nvm use "$NODE_VERSION"
npm install -g npm@latest
npm install -g npm-check-updates license-checker-rseidelsohn
echo "node $(node --version) / npm $(npm --version) / ncu $(ncu --version)"
command -v license-checker-rseidelsohn
EOF
sudo -u ec2-user -H bash -c "$NODE_SCRIPT" ide-setup "$NODE_VERSION"
step_end "Install Node.js v${NODE_VERSION} and npm tools"
# ----- pyenv, Python, pip, pytest (runs as ec2-user) -----
step_begin "Install pyenv, Python ${PYTHON_VERSION}, and pytest"
read -r -d '' PYTHON_SCRIPT << 'EOF' || true
set -eo pipefail # no "-u": "pyenv init" output is not nounset-safe
PYTHON_VERSION="$1"
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
if [ -x "$PYENV_ROOT/bin/pyenv" ]; then
echo "pyenv is already installed ($(pyenv --version))."
else
curl -fsSL https://pyenv.run | bash
fi
touch "$HOME/.bashrc"
if ! grep -q 'PYENV_ROOT' "$HOME/.bashrc"; then
cat >> "$HOME/.bashrc" << 'RCEOF'
# pyenv
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"
RCEOF
fi
eval "$(pyenv init - bash)"
py_known() { # capture the list first: "pyenv ... | grep -q" would fail with SIGPIPE under pipefail
local list
list=$(pyenv install -l)
grep -qE "^[[:space:]]*${PYTHON_VERSION//./\\.}[[:space:]]*$" <<< "$list"
}
if ! py_known; then
echo "pyenv does not know Python ${PYTHON_VERSION}. Running 'pyenv update' once."
pyenv update
py_known || { echo "[ERROR] pyenv does not know Python ${PYTHON_VERSION} even after 'pyenv update'." >&2; exit 1; }
fi
if [ -x "$PYENV_ROOT/versions/${PYTHON_VERSION}/bin/python" ]; then
echo "Python ${PYTHON_VERSION} is already installed. Skipping."
else
# -f: overwrite an incomplete folder left by an interrupted build
MAKE_OPTS="-j$(nproc)" pyenv install -f "$PYTHON_VERSION"
fi
pyenv global "$PYTHON_VERSION"
python --version
python -m pip install --upgrade pip pytest
pyenv rehash
pytest --version
EOF
sudo -u ec2-user -H bash -c "$PYTHON_SCRIPT" ide-setup "$PYTHON_VERSION"
step_end "Install pyenv, Python ${PYTHON_VERSION}, and pytest"
# ----- Record the installed versions -----
mkdir -p "$CONFIG_DIR"
echo "$NVM_VERSION" > "${CONFIG_DIR}/nvm.version"
echo "$NODE_VERSION" > "${CONFIG_DIR}/node.version"
echo "$PYTHON_VERSION" > "${CONFIG_DIR}/python.version"
log "[COMPLETE] ${DOC_NAME} finished successfully (nvm ${NVM_VERSION}, Node.js ${NODE_VERSION}, Python ${PYTHON_VERSION})."
Tags:
- Key: Cost
Value: !Sub "${SystemName}-${SubName}"
DocumentSetupPythonModules:
Type: AWS::SSM::Document
Properties:
Name: !Sub "${SystemName}-${SubName}-Setup-PythonModules"
DocumentType: Command
DocumentFormat: YAML
TargetType: /AWS::EC2::Instance
UpdateMethod: NewVersion
Content:
schemaVersion: "2.2"
description: "Installs the commonly used Python modules and Playwright with Chromium. Run Setup-NodePython first."
mainSteps:
- action: aws:runShellScript
name: SetupPythonModules
inputs:
timeoutSeconds: "1800"
runCommand:
- |
#!/bin/bash
# shellcheck disable=SC2034 # variables from the common block are not all used by every document
# ===== Common block (identical in all Setup-* documents except DOC_NAME) =====
set -eEuo pipefail # Fail as failure. Never use "set -x" (it could print secrets).
umask 022
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DOC_NAME="Setup-PythonModules"
LOG_DIR="/var/log/ide-setup"
CONFIG_DIR="/etc/ide-setup-config"
ENV_JSON="${CONFIG_DIR}/environment.json"
SUPPORTED_SCHEMA_VERSION="1"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
fail() { log "[ERROR] $*" >&2; exit 1; }
step_begin() { log "[START] $*"; }
step_end() { log "[DONE] $*"; }
[ "$(id -u)" -eq 0 ] || { echo "[ERROR] This document must run as root." >&2; exit 1; }
# Make HOME explicit (it may be unset under Run Command).
# Work for ec2-user uses "sudo -u ec2-user -H".
export HOME=/root
mkdir -p "$LOG_DIR"
LOG_FILE="${LOG_DIR}/${DOC_NAME}-$(date '+%Y%m%d-%H%M%S').log"
exec > >(tee -a "$LOG_FILE") 2>&1
TMP_DIR=$(mktemp -d /tmp/ide-setup.XXXXXX)
trap 'rm -rf "$TMP_DIR"' EXIT
trap 'log "[ERROR] Command failed at line ${LINENO} (exit code: $?)" >&2' ERR
log "Starting ${DOC_NAME} (log file: ${LOG_FILE})"
step_begin "Check the environment info JSON"
[ -f "$ENV_JSON" ] || fail "${ENV_JSON} was not found. UserData may still be running. Wait a few minutes and run this document again. (Check: cloud-init status)"
jq -e . "$ENV_JSON" > /dev/null || fail "${ENV_JSON} is not valid JSON."
env_get() { jq -r --arg k "$1" '(.[$k] // empty) | tostring' "$ENV_JSON"; }
[ "$(env_get schemaVersion)" = "$SUPPORTED_SCHEMA_VERSION" ] \
|| fail "schemaVersion mismatch (JSON: $(env_get schemaVersion), this document: ${SUPPORTED_SCHEMA_VERSION}). The PerPerson template and this document are out of sync."
for key in systemName subName yourId yourEmail region domainName timeZone \
codeserverFqdn kirocrewFqdn userPoolId userPoolClientId \
userPoolProviderUrl cognitoDomain workDir; do
[ -n "$(env_get "$key")" ] || fail "Key '${key}' is missing or empty in ${ENV_JSON}."
done
SYSTEM_NAME=$(env_get systemName); SUB_NAME=$(env_get subName)
YOUR_ID=$(env_get yourId); YOUR_EMAIL=$(env_get yourEmail)
REGION=$(env_get region); DOMAIN_NAME=$(env_get domainName)
TIME_ZONE=$(env_get timeZone)
CODESERVER_FQDN=$(env_get codeserverFqdn); KIROCREW_FQDN=$(env_get kirocrewFqdn)
USER_POOL_ID=$(env_get userPoolId); USER_POOL_CLIENT_ID=$(env_get userPoolClientId)
USER_POOL_PROVIDER_URL=$(env_get userPoolProviderUrl)
COGNITO_DOMAIN=$(env_get cognitoDomain); WORK_DIR=$(env_get workDir)
step_end "Check the environment info JSON (${SYSTEM_NAME}-${SUB_NAME} / ${YOUR_ID} / ${REGION})"
# ===== End of common block =====
# ----- Dependency check: Setup-NodePython must be completed (no change is made) -----
step_begin "Check that a pyenv Python is available for ec2-user"
read -r -d '' CHECK_SCRIPT << 'EOF' || true
set -eo pipefail # no "-u": "pyenv init" output is not nounset-safe
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
[ -x "$PYENV_ROOT/bin/pyenv" ] || { echo "pyenv is not installed."; exit 1; }
eval "$(pyenv init - bash)"
[ "$(pyenv version-name)" != "system" ] || { echo "No pyenv Python is selected."; exit 1; }
python --version
EOF
sudo -u ec2-user -H bash -c "$CHECK_SCRIPT" \
|| fail "A pyenv Python is not available for ec2-user. Run ${SYSTEM_NAME}-${SUB_NAME}-Setup-NodePython first. Nothing has been changed."
step_end "Check that a pyenv Python is available for ec2-user"
# ----- OS libraries for Playwright's Chromium (runs as root) -----
# "playwright install --with-deps" does not work on Amazon Linux 2023 (it assumes apt), so install the libraries here.
step_begin "Install the OS libraries for Playwright"
dnf install -y \
atk at-spi2-atk cups-libs libXcomposite libXdamage libXfixes \
libXrandr mesa-libgbm pango alsa-lib nss nspr at-spi2-core \
libdrm libxkbcommon libXScrnSaver
step_end "Install the OS libraries for Playwright"
# ----- Python modules (runs as ec2-user; versions are not pinned: the latest are installed) -----
step_begin "Install Python modules"
read -r -d '' MODULES_SCRIPT << 'EOF' || true
set -eo pipefail # no "-u": "pyenv init" output is not nounset-safe
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"
# If a module has no build for this Python version (e.g. a very new Python), pip fails and so does this document.
python -m pip install --upgrade \
cfn-lint \
ruff \
pdf_oxide \
pdfminer.six \
openpyxl \
xlsxwriter \
python-pptx \
python-docx \
pypandoc_binary \
git-remote-codecommit \
boto3 \
pandas \
numpy \
scipy \
scikit-learn \
nltk \
Markdown \
pymdown-extensions \
playwright
pyenv rehash
# Chromium for Playwright. Safe to run again: browsers that are already downloaded are skipped.
# Run it again after Playwright is upgraded, so that the matching Chromium is downloaded.
python -m playwright install chromium
cfn-lint --version
ruff --version
python -m playwright --version
python -c "import openpyxl, pptx, docx, pandas, numpy, boto3; print('modules OK')"
python -m pip list
EOF
sudo -u ec2-user -H bash -c "$MODULES_SCRIPT" ide-setup
step_end "Install Python modules"
# ----- Check that Chromium starts (fails the document if a shared library is missing) -----
step_begin "Check that Chromium starts"
read -r -d '' CHROMIUM_CHECK_SCRIPT << 'EOF' || true
set -eo pipefail # no "-u": "pyenv init" output is not nounset-safe
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"
python - << 'PYEOF'
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.set_content("<title>playwright-ok</title>")
assert page.title() == "playwright-ok"
print("Chromium " + browser.version + " started and rendered a page.")
browser.close()
PYEOF
EOF
sudo -u ec2-user -H bash -c "$CHROMIUM_CHECK_SCRIPT" ide-setup \
|| fail "Chromium did not start. Check the OS libraries and the Playwright installation above."
step_end "Check that Chromium starts"
log "[COMPLETE] ${DOC_NAME} finished successfully."
Tags:
- Key: Cost
Value: !Sub "${SystemName}-${SubName}"
DocumentSetupKiroCli:
Type: AWS::SSM::Document
Properties:
Name: !Sub "${SystemName}-${SubName}-Setup-KiroCli"
DocumentType: Command
DocumentFormat: YAML
TargetType: /AWS::EC2::Instance
UpdateMethod: NewVersion
Content:
schemaVersion: "2.2"
description: "Sets up Kiro CLI, uv, the MCP settings, and a provisional steering file (AGENTS.md)."
mainSteps:
- action: aws:runShellScript
name: SetupKiroCli
inputs:
timeoutSeconds: "1800"
runCommand:
- |
#!/bin/bash
# shellcheck disable=SC2034 # variables from the common block are not all used by every document
# ===== Common block (identical in all Setup-* documents except DOC_NAME) =====
set -eEuo pipefail # Fail as failure. Never use "set -x" (it could print secrets).
umask 022
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DOC_NAME="Setup-KiroCli"
LOG_DIR="/var/log/ide-setup"
CONFIG_DIR="/etc/ide-setup-config"
ENV_JSON="${CONFIG_DIR}/environment.json"
SUPPORTED_SCHEMA_VERSION="1"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
fail() { log "[ERROR] $*" >&2; exit 1; }
step_begin() { log "[START] $*"; }
step_end() { log "[DONE] $*"; }
[ "$(id -u)" -eq 0 ] || { echo "[ERROR] This document must run as root." >&2; exit 1; }
# Make HOME explicit (it may be unset under Run Command).
# Work for ec2-user uses "sudo -u ec2-user -H".
export HOME=/root
mkdir -p "$LOG_DIR"
LOG_FILE="${LOG_DIR}/${DOC_NAME}-$(date '+%Y%m%d-%H%M%S').log"
exec > >(tee -a "$LOG_FILE") 2>&1
TMP_DIR=$(mktemp -d /tmp/ide-setup.XXXXXX)
trap 'rm -rf "$TMP_DIR"' EXIT
trap 'log "[ERROR] Command failed at line ${LINENO} (exit code: $?)" >&2' ERR
log "Starting ${DOC_NAME} (log file: ${LOG_FILE})"
step_begin "Check the environment info JSON"
[ -f "$ENV_JSON" ] || fail "${ENV_JSON} was not found. UserData may still be running. Wait a few minutes and run this document again. (Check: cloud-init status)"
jq -e . "$ENV_JSON" > /dev/null || fail "${ENV_JSON} is not valid JSON."
env_get() { jq -r --arg k "$1" '(.[$k] // empty) | tostring' "$ENV_JSON"; }
[ "$(env_get schemaVersion)" = "$SUPPORTED_SCHEMA_VERSION" ] \
|| fail "schemaVersion mismatch (JSON: $(env_get schemaVersion), this document: ${SUPPORTED_SCHEMA_VERSION}). The PerPerson template and this document are out of sync."
for key in systemName subName yourId yourEmail region domainName timeZone \
codeserverFqdn kirocrewFqdn userPoolId userPoolClientId \
userPoolProviderUrl cognitoDomain workDir; do
[ -n "$(env_get "$key")" ] || fail "Key '${key}' is missing or empty in ${ENV_JSON}."
done
SYSTEM_NAME=$(env_get systemName); SUB_NAME=$(env_get subName)
YOUR_ID=$(env_get yourId); YOUR_EMAIL=$(env_get yourEmail)
REGION=$(env_get region); DOMAIN_NAME=$(env_get domainName)
TIME_ZONE=$(env_get timeZone)
CODESERVER_FQDN=$(env_get codeserverFqdn); KIROCREW_FQDN=$(env_get kirocrewFqdn)
USER_POOL_ID=$(env_get userPoolId); USER_POOL_CLIENT_ID=$(env_get userPoolClientId)
USER_POOL_PROVIDER_URL=$(env_get userPoolProviderUrl)
COGNITO_DOMAIN=$(env_get cognitoDomain); WORK_DIR=$(env_get workDir)
step_end "Check the environment info JSON (${SYSTEM_NAME}-${SUB_NAME} / ${YOUR_ID} / ${REGION})"
# ===== End of common block =====
EC2_HOME=/home/ec2-user
# ----- Kiro CLI (runs as ec2-user) -----
step_begin "Install Kiro CLI"
read -r -d '' KIRO_SCRIPT << 'EOF' || true
set -eo pipefail
export PATH="$HOME/.local/bin:$PATH"
if [ -x "$HOME/.local/bin/kiro-cli" ] && kiro-cli --version > /dev/null 2>&1; then
# The installer asks "replace it?" on /dev/tty when Kiro CLI exists, so never run it again
echo "Kiro CLI is already installed ($(kiro-cli --version)). Skipping."
else
# The installer needs 2 GiB in its work folder; /tmp is tmpfs (RAM), so use the root disk
curl -fsSL https://cli.kiro.dev/install | TMPDIR=/var/tmp bash
fi
kiro-cli --version
EOF
sudo -u ec2-user -H bash -c "$KIRO_SCRIPT" ide-setup
step_end "Install Kiro CLI"
# ----- uv (runs as ec2-user) -----
step_begin "Install uv"
read -r -d '' UV_SCRIPT << 'EOF' || true
set -eo pipefail
export PATH="$HOME/.local/bin:$PATH"
if [ -x "$HOME/.local/bin/uv" ] && [ -x "$HOME/.local/bin/uvx" ]; then
echo "uv is already installed ($(uv --version)). Skipping."
else
# ~/.local/bin is already on PATH through the default ~/.bashrc, so do not let the installer edit shell profiles
curl -LsSf https://astral.sh/uv/install.sh | UV_NO_MODIFY_PATH=1 sh
fi
uv --version
uvx --version
EOF
sudo -u ec2-user -H bash -c "$UV_SCRIPT" ide-setup
step_end "Install uv"
# ----- Kiro CLI settings (created only when missing: the developer edits them) -----
step_begin "Create the Kiro CLI settings"
make_dir() { # make_dir <path> <mode>: create only when missing, owned by ec2-user
[ -d "$1" ] || install -d -o ec2-user -g ec2-user -m "$2" "$1"
}
make_dir "${EC2_HOME}/.cache" 700
make_dir "${EC2_HOME}/.kiro" 700
make_dir "${EC2_HOME}/.kiro/settings" 700
make_dir "${EC2_HOME}/.kiro/steering" 755
if [ -e "${EC2_HOME}/.kiro/settings/mcp.json" ]; then
log "${EC2_HOME}/.kiro/settings/mcp.json already exists. Not changed."
else
cat > "${TMP_DIR}/mcp.json" << 'EOF'
{
"mcpServers": {
"awslabs.aws-documentation-mcp-server": {
"command": "uvx",
"args": ["awslabs.aws-documentation-mcp-server@latest"],
"env": {
"FASTMCP_LOG_LEVEL": "ERROR",
"AWS_DOCUMENTATION_PARTITION": "aws"
},
"disabled": false,
"autoApprove": []
}
}
}
EOF
install -m 600 -o ec2-user -g ec2-user "${TMP_DIR}/mcp.json" "${EC2_HOME}/.kiro/settings/mcp.json"
log "Created ${EC2_HOME}/.kiro/settings/mcp.json"
fi
if [ -e "${EC2_HOME}/.kiro/steering/AGENTS.md" ]; then
log "${EC2_HOME}/.kiro/steering/AGENTS.md already exists. Not changed."
else
cat > "${TMP_DIR}/AGENTS.md" << 'EOF'
# Steering Instructions
## Common rules
- 日本語で回答すること
## Change Confirmation Policy
変更作業は必ずユーザーが内容を理解したことを確認し、実行の指示を得てから作業してください。
いきなりファイルに変更をかけることは絶対にしないでください。
ユーザーは、指定がない限り対話型での作業を期待しています。
EOF
install -m 644 -o ec2-user -g ec2-user "${TMP_DIR}/AGENTS.md" "${EC2_HOME}/.kiro/steering/AGENTS.md"
log "Created ${EC2_HOME}/.kiro/steering/AGENTS.md (provisional)"
fi
step_end "Create the Kiro CLI settings"
log "[COMPLETE] ${DOC_NAME} finished successfully. Next: sign in manually with 'kiro-cli login' (not done by this document)."
Tags:
- Key: Cost
Value: !Sub "${SystemName}-${SubName}"
DocumentSetupKiroCrew:
Type: AWS::SSM::Document
Properties:
Name: !Sub "${SystemName}-${SubName}-Setup-KiroCrew"
DocumentType: Command
DocumentFormat: YAML
TargetType: /AWS::EC2::Instance
UpdateMethod: NewVersion
Content:
schemaVersion: "2.2"
description: "Sets up Kiro Crew with its own FQDN, certificate, oauth2-proxy, and nginx settings. Run Setup-CodeServer, Setup-NodePython, and Setup-KiroCli first."
mainSteps:
- action: aws:runShellScript
name: SetupKiroCrew
inputs:
timeoutSeconds: "1800"
runCommand:
- |
#!/bin/bash
# shellcheck disable=SC2034 # variables from the common block are not all used by every document
# ===== Common block (identical in all Setup-* documents except DOC_NAME) =====
set -eEuo pipefail # Fail as failure. Never use "set -x" (it could print secrets).
umask 022
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DOC_NAME="Setup-KiroCrew"
LOG_DIR="/var/log/ide-setup"
CONFIG_DIR="/etc/ide-setup-config"
ENV_JSON="${CONFIG_DIR}/environment.json"
SUPPORTED_SCHEMA_VERSION="1"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
fail() { log "[ERROR] $*" >&2; exit 1; }
step_begin() { log "[START] $*"; }
step_end() { log "[DONE] $*"; }
[ "$(id -u)" -eq 0 ] || { echo "[ERROR] This document must run as root." >&2; exit 1; }
# Make HOME explicit (it may be unset under Run Command).
# Work for ec2-user uses "sudo -u ec2-user -H".
export HOME=/root
mkdir -p "$LOG_DIR"
LOG_FILE="${LOG_DIR}/${DOC_NAME}-$(date '+%Y%m%d-%H%M%S').log"
exec > >(tee -a "$LOG_FILE") 2>&1
TMP_DIR=$(mktemp -d /tmp/ide-setup.XXXXXX)
trap 'rm -rf "$TMP_DIR"' EXIT
trap 'log "[ERROR] Command failed at line ${LINENO} (exit code: $?)" >&2' ERR
log "Starting ${DOC_NAME} (log file: ${LOG_FILE})"
step_begin "Check the environment info JSON"
[ -f "$ENV_JSON" ] || fail "${ENV_JSON} was not found. UserData may still be running. Wait a few minutes and run this document again. (Check: cloud-init status)"
jq -e . "$ENV_JSON" > /dev/null || fail "${ENV_JSON} is not valid JSON."
env_get() { jq -r --arg k "$1" '(.[$k] // empty) | tostring' "$ENV_JSON"; }
[ "$(env_get schemaVersion)" = "$SUPPORTED_SCHEMA_VERSION" ] \
|| fail "schemaVersion mismatch (JSON: $(env_get schemaVersion), this document: ${SUPPORTED_SCHEMA_VERSION}). The PerPerson template and this document are out of sync."
for key in systemName subName yourId yourEmail region domainName timeZone \
codeserverFqdn kirocrewFqdn userPoolId userPoolClientId \
userPoolProviderUrl cognitoDomain workDir; do
[ -n "$(env_get "$key")" ] || fail "Key '${key}' is missing or empty in ${ENV_JSON}."
done
SYSTEM_NAME=$(env_get systemName); SUB_NAME=$(env_get subName)
YOUR_ID=$(env_get yourId); YOUR_EMAIL=$(env_get yourEmail)
REGION=$(env_get region); DOMAIN_NAME=$(env_get domainName)
TIME_ZONE=$(env_get timeZone)
CODESERVER_FQDN=$(env_get codeserverFqdn); KIROCREW_FQDN=$(env_get kirocrewFqdn)
USER_POOL_ID=$(env_get userPoolId); USER_POOL_CLIENT_ID=$(env_get userPoolClientId)
USER_POOL_PROVIDER_URL=$(env_get userPoolProviderUrl)
COGNITO_DOMAIN=$(env_get cognitoDomain); WORK_DIR=$(env_get workDir)
step_end "Check the environment info JSON (${SYSTEM_NAME}-${SUB_NAME} / ${YOUR_ID} / ${REGION})"
# ===== End of common block =====
EC2_HOME=/home/ec2-user
# ----- Dependency check (no change is made) -----
step_begin "Check the dependencies"
# (1) Checks for ec2-user: Python, Node.js, Kiro CLI, uvx. No "-e" so that all problems are reported at once.
read -r -d '' CHECK_SCRIPT << 'EOF' || true
export PATH="$HOME/.local/bin:$PATH"
export PYENV_ROOT="$HOME/.pyenv"
export NVM_DIR="$HOME/.nvm"
ok() { echo "[OK] $*"; }
missing() { echo "[MISSING] $*"; }
warn() { echo "[WARN] $*"; }
# Python 3.12 or later, provided by pyenv
if [ -x "$PYENV_ROOT/bin/pyenv" ]; then
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"
if [ "$(pyenv version-name)" != "system" ] \
&& python -c 'import sys; sys.exit(0 if sys.version_info >= (3, 12) else 1)' 2> /dev/null; then
ok "Python $(python --version 2>&1 | cut -d' ' -f2) (pyenv)"
else
missing "Python 3.12 or later from pyenv. Run Setup-NodePython."
fi
else
missing "pyenv is not installed. Run Setup-NodePython."
fi
# Node.js 22.12 or later as the nvm default
if [ -s "$NVM_DIR/nvm.sh" ]; then
. "$NVM_DIR/nvm.sh"
NODE_DEFAULT=$(nvm version default 2> /dev/null || true) # e.g. v24.22.0, or N/A
NODE_NUM="${NODE_DEFAULT#v}"
if [[ "$NODE_NUM" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
&& [ "$(printf '%s\n' 22.12.0 "$NODE_NUM" | sort -V | head -1)" = "22.12.0" ]; then
ok "Node.js ${NODE_DEFAULT} (nvm default)"
else
missing "Node.js 22.12 or later as the nvm default (found: ${NODE_DEFAULT:-none}). Run Setup-NodePython."
fi
else
missing "nvm is not installed. Run Setup-NodePython."
fi
# Kiro CLI
if kiro-cli --version > /dev/null 2>&1; then
ok "Kiro CLI $(kiro-cli --version)"
else
missing "Kiro CLI cannot be run. Run Setup-KiroCli."
fi
# uvx (warning only: needed to start MCP servers)
if [ -x "$HOME/.local/bin/uvx" ]; then ok "uvx"; else warn "uvx was not found (MCP servers cannot start). Run Setup-KiroCli."; fi
EOF
CHECK_OUT=$(sudo -u ec2-user -H bash -c "$CHECK_SCRIPT" ide-setup 2>&1)
echo "$CHECK_OUT"
MISSING_COUNT=$(grep -c '^\[MISSING\]' <<< "$CHECK_OUT" || true)
# (2) Checks for root: Setup-CodeServer must be completed
miss() { log "[MISSING] $*"; MISSING_COUNT=$((MISSING_COUNT + 1)); }
for svc in nginx oauth2-proxy; do
if systemctl is-active --quiet "$svc"; then log "[OK] ${svc} is running"; else miss "${svc} is not running. Run Setup-CodeServer."; fi
done
[ -x /usr/local/bin/oauth2-proxy ] || miss "/usr/local/bin/oauth2-proxy was not found. Run Setup-CodeServer."
[ -s /etc/oauth2-proxy/allowed_emails.txt ] || miss "/etc/oauth2-proxy/allowed_emails.txt was not found. Run Setup-CodeServer."
[ -f /etc/nginx/conf.d/codeserver.conf ] || miss "/etc/nginx/conf.d/codeserver.conf was not found. Run Setup-CodeServer."
[ -s "/etc/letsencrypt/live/${CODESERVER_FQDN}/fullchain.pem" ] || miss "The certificate of ${CODESERVER_FQDN} was not found. Run Setup-CodeServer."
[ -f "${EC2_HOME}/.config/code-server/config.yaml" ] || miss "The code-server config was not found. Run Setup-CodeServer."
if [ -d "$WORK_DIR" ] && [ "$(stat -c %U "$WORK_DIR")" = "ec2-user" ]; then
log "[OK] ${WORK_DIR} exists and is owned by ec2-user"
else
miss "${WORK_DIR} does not exist or is not owned by ec2-user. Run Setup-CodeServer."
fi
# (3) Memory (warning only): the official minimum is 10GB
MEM_KIB=$(awk '/^MemTotal:/ { print $2 }' /proc/meminfo)
if [ "$MEM_KIB" -lt $((10 * 1024 * 1024)) ]; then
log "[WARN] Memory is $((MEM_KIB / 1024)) MiB. Kiro Crew officially needs at least 10GB (16GB recommended)."
else
log "[OK] Memory is $((MEM_KIB / 1024)) MiB"
fi
if [ "$MISSING_COUNT" -gt 0 ]; then
fail "${MISSING_COUNT} dependency check(s) failed. See the [MISSING] lines above. Nothing has been changed."
fi
step_end "Check the dependencies"
# ----- linger: keeps ec2-user's systemd and /run/user/<uid>/bus alive (needed by Kiro Crew) -----
step_begin "Enable linger for ec2-user"
loginctl enable-linger ec2-user
EC2_UID=$(id -u ec2-user)
for _ in $(seq 1 30); do # the user manager starts asynchronously
[ -S "/run/user/${EC2_UID}/bus" ] && break
sleep 1
done
[ -S "/run/user/${EC2_UID}/bus" ] \
|| fail "/run/user/${EC2_UID}/bus was not created after 'loginctl enable-linger ec2-user'. Kiro Crew cannot be installed as a service."
step_end "Enable linger for ec2-user"
# ----- Kiro Crew: install, configure, and register the service (runs as ec2-user) -----
step_begin "Install and configure Kiro Crew"
read -r -d '' CREW_SCRIPT << 'EOF' || true
set -eo pipefail
TIME_ZONE="$1"; KIROCREW_FQDN="$2"; WORK_DIR="$3"
# The user bus for "kirocrew service install" (the shell started by sudo has no login session)
XDG_RUNTIME_DIR="/run/user/$(id -u)"
export XDG_RUNTIME_DIR
export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
# "kirocrew service install" writes the PATH of this moment into the service,
# so put pyenv shims and the default nvm Node.js on PATH first
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - bash)"
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
nvm use default > /dev/null
export PATH="$HOME/.local/bin:$PATH"
echo "PATH used by the service: $PATH"
# Install with the pyenv Python (--system-python): no managed CPython is downloaded
if command -v kirocrew > /dev/null 2>&1 && kirocrew --version > /dev/null 2>&1; then
echo "Kiro Crew is already installed ($(kirocrew --version)). Skipping the installation."
else
export KIROCREW_INSTALL_PLAIN=1
curl -fsSL https://download.crew.kiro.dev/cli.sh | sh -s -- --system-python
hash -r
kirocrew --version
fi
# Initial setup (agent only; sign-in stays manual). Skipped when the config already exists.
if [ -f "$HOME/.kiro/crew/config.json" ]; then
echo "$HOME/.kiro/crew/config.json exists. Skipping 'kirocrew setup'."
else
kirocrew setup --agent-only < /dev/null
fi
# Settings (set to the same values every time)
kirocrew config set timezone "$TIME_ZONE"
kirocrew config set dashboard.url "https://${KIROCREW_FQDN}"
# workspaces.default.dir: "config set <key> <value>" rejects an absolute path, so pass the whole config with --file
umask 077
CONFIG_TMP=$(mktemp)
trap 'rm -f "$CONFIG_TMP"' EXIT
jq --arg d "$WORK_DIR" '.workspaces.default.dir = $d' "$HOME/.kiro/crew/config.json" > "$CONFIG_TMP"
kirocrew config set --file "$CONFIG_TMP"
echo "workspaces.default.dir = $(jq -r '.workspaces.default.dir' "$HOME/.kiro/crew/config.json")"
# Service: never re-run 'service install' when it exists (it restarts the gateway, which invalidates tokens and interrupts sessions)
if [ -f /etc/systemd/system/kirocrew.service ]; then
echo "kirocrew.service already exists. Not re-installed."
echo "To switch the Node.js used by the service, run 'kirocrew service install' again by hand."
else
kirocrew service install
fi
kirocrew service status | head -5 || true
EOF
sudo -u ec2-user -H bash -c "$CREW_SCRIPT" ide-setup "$TIME_ZONE" "$KIROCREW_FQDN" "$WORK_DIR"
step_end "Install and configure Kiro Crew"
# ----- Certificate for Kiro Crew -----
step_begin "Issue the certificate for ${KIROCREW_FQDN}"
LE_LIVE_KC="/etc/letsencrypt/live/${KIROCREW_FQDN}"
if [ -s "${LE_LIVE_KC}/fullchain.pem" ] && [ -s "${LE_LIVE_KC}/privkey.pem" ]; then
log "The certificate already exists. Not re-issued (use renew-cert-kirocrew.sh to renew)."
else
certbot certonly --dns-route53 -d "$KIROCREW_FQDN" --agree-tos -m "$YOUR_EMAIL" \
--non-interactive --preferred-challenges dns-01
fi
step_end "Issue the certificate for ${KIROCREW_FQDN}"
# ----- oauth2-proxy-kirocrew service (generated from the environment info and Cognito; nothing is copied) -----
step_begin "Configure the oauth2-proxy-kirocrew service"
OPK_UNIT=/etc/systemd/system/oauth2-proxy-kirocrew.service
# Client secret: always fetched from Cognito (never printed)
CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client \
--user-pool-id "$USER_POOL_ID" --client-id "$USER_POOL_CLIENT_ID" --region "$REGION" \
--query 'UserPoolClient.ClientSecret' --output text)
{ [ -n "$CLIENT_SECRET" ] && [ "$CLIENT_SECRET" != "None" ]; } \
|| fail "Could not get the client secret of the Cognito app client."
# Cookie secret (separate from the one of code-server): keep the existing one, otherwise generate
COOKIE_SECRET=""
if [ -f "$OPK_UNIT" ]; then
COOKIE_SECRET=$(sed -n 's/^Environment=COOKIE_SECRET=//p' "$OPK_UNIT" | head -1)
fi
if [ -z "$COOKIE_SECRET" ]; then
COOKIE_SECRET=$(openssl rand -base64 32 | tr '+/' '-_')
log "Generated a new cookie secret."
else
log "Kept the existing cookie secret."
fi
# shellcheck disable=SC2016 # the single-quoted $CLIENT_SECRET and $COOKIE_SECRET are expanded by systemd, not by the shell
OPK_ARGS=(
--provider=oidc
"--oidc-issuer-url=${USER_POOL_PROVIDER_URL}"
"--client-id=${USER_POOL_CLIENT_ID}"
'--client-secret=${CLIENT_SECRET}'
"--redirect-url=https://${KIROCREW_FQDN}/oauth2/callback"
"--login-url=https://${COGNITO_DOMAIN}/oauth2/authorize"
"--profile-url=https://${COGNITO_DOMAIN}/oauth2/userInfo"
"--redeem-url=https://${COGNITO_DOMAIN}/oauth2/token"
--scope=openid
--cookie-name=_oauth2_proxy_kirocrew
--cookie-secure=true
--cookie-expire=72h
--cookie-refresh=1h
'--cookie-secret=${COOKIE_SECRET}'
--authenticated-emails-file=/etc/oauth2-proxy/allowed_emails.txt
--http-address=127.0.0.1:4181
)
{
cat << 'EOF'
[Unit]
Description=oauth2-proxy for Kiro Crew
After=network.target
[Service]
Type=simple
EOF
printf 'Environment=CLIENT_SECRET=%s\n' "$CLIENT_SECRET"
printf 'Environment=COOKIE_SECRET=%s\n' "$COOKIE_SECRET"
printf 'ExecStart=/usr/local/bin/oauth2-proxy'; printf ' %s' "${OPK_ARGS[@]}"; printf '\n'
cat << 'EOF'
Restart=on-failure
User=ec2-user
Group=ec2-user
[Install]
WantedBy=multi-user.target
EOF
} > "${TMP_DIR}/oauth2-proxy-kirocrew.service"
OPK_UNIT_CHANGED=0
if ! cmp -s "${TMP_DIR}/oauth2-proxy-kirocrew.service" "$OPK_UNIT"; then
install -m 600 -o root -g root "${TMP_DIR}/oauth2-proxy-kirocrew.service" "$OPK_UNIT" # 600: it contains secrets
OPK_UNIT_CHANGED=1
log "Wrote ${OPK_UNIT} (contents are not shown because they contain secrets)."
else
log "${OPK_UNIT} is unchanged."
fi
systemctl daemon-reload
systemctl enable oauth2-proxy-kirocrew
# Restart only when the definition changed or the service is not running.
# A running instance keeps its old oauth2-proxy binary after Setup-CodeServer changed the version (restart it by hand).
if [ "$OPK_UNIT_CHANGED" -eq 1 ] || ! systemctl is-active --quiet oauth2-proxy-kirocrew; then
systemctl restart oauth2-proxy-kirocrew
fi
unset CLIENT_SECRET COOKIE_SECRET
OPK_PING=""
for _ in $(seq 1 30); do
OPK_PING=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:4181/ping || true)
[ "$OPK_PING" = "200" ] && break
sleep 1
done
[ "$OPK_PING" = "200" ] \
|| fail "oauth2-proxy-kirocrew /ping did not return 200 (last: ${OPK_PING:-none}). Check 'journalctl -u oauth2-proxy-kirocrew'."
step_end "Configure the oauth2-proxy-kirocrew service (/ping OK)"
# ----- nginx for Kiro Crew -----
step_begin "Configure nginx for Kiro Crew"
install -d -m 700 /etc/nginx/ssl
install -m 644 "${LE_LIVE_KC}/fullchain.pem" /etc/nginx/ssl/kirocrew-chain.crt
install -m 600 "${LE_LIVE_KC}/privkey.pem" /etc/nginx/ssl/kirocrew.key
cat > "${TMP_DIR}/kirocrew.conf.tpl" << 'EOF'
server {
listen 443 ssl;
server_name __KIROCREW_FQDN__;
large_client_header_buffers 4 16k;
ssl_certificate_key /etc/nginx/ssl/kirocrew.key;
ssl_certificate /etc/nginx/ssl/kirocrew-chain.crt;
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
proxy_pass http://127.0.0.1:5476;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
location /oauth2/ {
proxy_pass http://127.0.0.1:4181;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Auth-Request-Redirect $request_uri;
proxy_buffers 16 16k;
proxy_buffer_size 16k;
}
location = /oauth2/auth {
proxy_pass http://127.0.0.1:4181;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header Content-Length "";
proxy_pass_request_body off;
proxy_buffers 16 16k;
proxy_buffer_size 16k;
}
}
EOF
NGINX_CONF_KC=/etc/nginx/conf.d/kirocrew.conf
if [ -f "$NGINX_CONF_KC" ]; then cp -p "$NGINX_CONF_KC" "${TMP_DIR}/kirocrew.conf.bak"; fi
sed "s#__KIROCREW_FQDN__#${KIROCREW_FQDN}#g" "${TMP_DIR}/kirocrew.conf.tpl" > "$NGINX_CONF_KC"
if ! nginx -t; then # roll back so that a bad file does not break other documents' reload
if [ -f "${TMP_DIR}/kirocrew.conf.bak" ]; then cp -p "${TMP_DIR}/kirocrew.conf.bak" "$NGINX_CONF_KC"; else rm -f "$NGINX_CONF_KC"; fi
fail "nginx -t failed. The previous nginx configuration was restored."
fi
systemctl enable nginx
if systemctl is-active --quiet nginx; then systemctl reload nginx; else systemctl start nginx; fi
step_end "Configure nginx for Kiro Crew"
# ----- Certificate renewal script -----
step_begin "Create the certificate renewal script"
cat > "${TMP_DIR}/renew-cert-kirocrew.sh.tpl" << 'EOF'
#!/bin/bash
# Renews the certificate of __KIROCREW_FQDN__ (Kiro Crew).
# Usage: sudo /home/ec2-user/renew-cert-kirocrew.sh
set -euo pipefail
[ "$(id -u)" -eq 0 ] || { echo "Run this script with sudo." >&2; exit 1; }
FQDN="__KIROCREW_FQDN__"
LIVE="/etc/letsencrypt/live/${FQDN}"
certbot renew --cert-name "$FQDN"
if cmp -s "${LIVE}/fullchain.pem" /etc/nginx/ssl/kirocrew-chain.crt \
&& cmp -s "${LIVE}/privkey.pem" /etc/nginx/ssl/kirocrew.key; then
echo "The certificate is not due for renewal yet. Nothing was changed."
exit 0
fi
install -m 644 "${LIVE}/fullchain.pem" /etc/nginx/ssl/kirocrew-chain.crt
install -m 600 "${LIVE}/privkey.pem" /etc/nginx/ssl/kirocrew.key
nginx -t
systemctl reload nginx
echo "The certificate was renewed and nginx was reloaded."
EOF
sed "s#__KIROCREW_FQDN__#${KIROCREW_FQDN}#g" "${TMP_DIR}/renew-cert-kirocrew.sh.tpl" > /home/ec2-user/renew-cert-kirocrew.sh
chown ec2-user:ec2-user /home/ec2-user/renew-cert-kirocrew.sh
chmod 755 /home/ec2-user/renew-cert-kirocrew.sh
step_end "Create the certificate renewal script"
# ----- End-to-end check -----
step_begin "Check Kiro Crew end to end"
# (1) The gateway answers for the public host name (dashboard.url allows it); it may need a moment to start
GW_CODE=""
for _ in $(seq 1 60); do
GW_CODE=$(curl -s -o /dev/null -w '%{http_code}' -H "Host: ${KIROCREW_FQDN}" http://127.0.0.1:5476/ || true)
[ "$GW_CODE" = "200" ] && break
sleep 1
done
[ "$GW_CODE" = "200" ] \
|| fail "The Kiro Crew gateway did not return 200 on 127.0.0.1:5476 (last: ${GW_CODE:-none}). Check 'kirocrew service status' and 'kirocrew logs'."
# (2) nginx with the issued certificate shows the sign-in page (TLS is verified: the certificate must be valid for the FQDN).
# Retry: right after "systemctl reload nginx" the new configuration may not be served yet (seen in a test run: curl got no response).
NGX_CODE=""
for _ in $(seq 1 30); do
NGX_CODE=$(curl -sS -o /dev/null -w '%{http_code}' --resolve "${KIROCREW_FQDN}:443:127.0.0.1" "https://${KIROCREW_FQDN}/" 2> "${TMP_DIR}/nginx-curl.err" || true)
[ "$NGX_CODE" = "200" ] && break
sleep 1
done
if [ "$NGX_CODE" != "200" ]; then
fail "https://${KIROCREW_FQDN}/ did not return 200 through nginx (last: ${NGX_CODE:-none}; curl said: $(head -c 300 "${TMP_DIR}/nginx-curl.err")). Check 'nginx -t' and the certificate."
fi
step_end "Check Kiro Crew end to end"
log "[COMPLETE] ${DOC_NAME} finished successfully. Next: sign in with 'kiro-cli login', then run 'kirocrew token | grep ^https://' and open the URL."
Tags:
- Key: Cost
Value: !Sub "${SystemName}-${SubName}"
DocumentSetupLibreOffice:
Type: AWS::SSM::Document
Properties:
Name: !Sub "${SystemName}-${SubName}-Setup-LibreOffice"
DocumentType: Command
DocumentFormat: YAML
TargetType: /AWS::EC2::Instance
UpdateMethod: NewVersion
Content:
schemaVersion: "2.2"
description: "Installs the latest stable LibreOffice (official RPM) and links soffice."
mainSteps:
- action: aws:runShellScript
name: SetupLibreOffice
inputs:
timeoutSeconds: "1800"
runCommand:
- |
#!/bin/bash
# shellcheck disable=SC2034 # variables from the common block are not all used by every document
# ===== Common block (identical in all Setup-* documents except DOC_NAME) =====
set -eEuo pipefail # Fail as failure. Never use "set -x" (it could print secrets).
umask 022
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DOC_NAME="Setup-LibreOffice"
LOG_DIR="/var/log/ide-setup"
CONFIG_DIR="/etc/ide-setup-config"
ENV_JSON="${CONFIG_DIR}/environment.json"
SUPPORTED_SCHEMA_VERSION="1"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
fail() { log "[ERROR] $*" >&2; exit 1; }
step_begin() { log "[START] $*"; }
step_end() { log "[DONE] $*"; }
[ "$(id -u)" -eq 0 ] || { echo "[ERROR] This document must run as root." >&2; exit 1; }
# Make HOME explicit (it may be unset under Run Command).
# Work for ec2-user uses "sudo -u ec2-user -H".
export HOME=/root
mkdir -p "$LOG_DIR"
LOG_FILE="${LOG_DIR}/${DOC_NAME}-$(date '+%Y%m%d-%H%M%S').log"
exec > >(tee -a "$LOG_FILE") 2>&1
TMP_DIR=$(mktemp -d /tmp/ide-setup.XXXXXX)
trap 'rm -rf "$TMP_DIR"' EXIT
trap 'log "[ERROR] Command failed at line ${LINENO} (exit code: $?)" >&2' ERR
log "Starting ${DOC_NAME} (log file: ${LOG_FILE})"
step_begin "Check the environment info JSON"
[ -f "$ENV_JSON" ] || fail "${ENV_JSON} was not found. UserData may still be running. Wait a few minutes and run this document again. (Check: cloud-init status)"
jq -e . "$ENV_JSON" > /dev/null || fail "${ENV_JSON} is not valid JSON."
env_get() { jq -r --arg k "$1" '(.[$k] // empty) | tostring' "$ENV_JSON"; }
[ "$(env_get schemaVersion)" = "$SUPPORTED_SCHEMA_VERSION" ] \
|| fail "schemaVersion mismatch (JSON: $(env_get schemaVersion), this document: ${SUPPORTED_SCHEMA_VERSION}). The PerPerson template and this document are out of sync."
for key in systemName subName yourId yourEmail region domainName timeZone \
codeserverFqdn kirocrewFqdn userPoolId userPoolClientId \
userPoolProviderUrl cognitoDomain workDir; do
[ -n "$(env_get "$key")" ] || fail "Key '${key}' is missing or empty in ${ENV_JSON}."
done
SYSTEM_NAME=$(env_get systemName); SUB_NAME=$(env_get subName)
YOUR_ID=$(env_get yourId); YOUR_EMAIL=$(env_get yourEmail)
REGION=$(env_get region); DOMAIN_NAME=$(env_get domainName)
TIME_ZONE=$(env_get timeZone)
CODESERVER_FQDN=$(env_get codeserverFqdn); KIROCREW_FQDN=$(env_get kirocrewFqdn)
USER_POOL_ID=$(env_get userPoolId); USER_POOL_CLIENT_ID=$(env_get userPoolClientId)
USER_POOL_PROVIDER_URL=$(env_get userPoolProviderUrl)
COGNITO_DOMAIN=$(env_get cognitoDomain); WORK_DIR=$(env_get workDir)
step_end "Check the environment info JSON (${SYSTEM_NAME}-${SUB_NAME} / ${YOUR_ID} / ${REGION})"
# ===== End of common block =====
LO_BASE_URL="https://download.documentfoundation.org/libreoffice/stable"
LO_WORK=""
# Work folder is on the root disk (/var/tmp): /tmp is tmpfs (RAM) and the package is large
trap 'rm -rf "$TMP_DIR" "$LO_WORK"' EXIT
# ----- Find the latest stable version (no change is made) -----
step_begin "Find the latest stable LibreOffice"
LO_LISTING=$(curl -fsSL --retry 3 "${LO_BASE_URL}/")
LO_VERSIONS=$(grep -oE 'href="[0-9]+(\.[0-9]+)+/"' <<< "$LO_LISTING" | sed -E 's/^href="//; s#/"$##' | sort -V || true)
[ -n "$LO_VERSIONS" ] || fail "Could not read the LibreOffice version list. Nothing has been changed."
LO_VERSION=$(tail -1 <<< "$LO_VERSIONS")
LO_SERIES=$(cut -d. -f1,2 <<< "$LO_VERSION") # e.g. 26.8: used by RPM names and the /opt folder
LO_TARBALL="LibreOffice_${LO_VERSION}_Linux_x86-64_rpm.tar.gz"
LO_URL="${LO_BASE_URL}/${LO_VERSION}/rpm/x86_64/${LO_TARBALL}"
log "Available versions: $(tr '\n' ' ' <<< "$LO_VERSIONS")"
log "Selected (largest number): ${LO_VERSION}"
curl -fsSL --retry 3 -I -o /dev/null "$LO_URL" \
|| fail "The RPM package of LibreOffice ${LO_VERSION} was not found: ${LO_URL}. Nothing has been changed."
step_end "Find the latest stable LibreOffice (${LO_VERSION})"
# ----- Install LibreOffice (skipped when the same version is installed) -----
step_begin "Install LibreOffice ${LO_VERSION}"
LO_INSTALLED=""
if rpm -q --quiet "libreoffice${LO_SERIES}"; then
LO_INSTALLED=$(rpm -q --qf '%{VERSION}' "libreoffice${LO_SERIES}") # e.g. 26.8.1.1
fi
if [ -n "$LO_INSTALLED" ] && { [ "$LO_INSTALLED" = "$LO_VERSION" ] || [[ "$LO_INSTALLED" == "${LO_VERSION}."* ]]; }; then
log "LibreOffice ${LO_INSTALLED} is already installed. Skipping the installation."
else
LO_WORK=$(mktemp -d /var/tmp/ide-setup-lo.XXXXXX)
curl -fsSL --retry 3 -o "${LO_WORK}/${LO_TARBALL}" "$LO_URL"
curl -fsSL --retry 3 -o "${LO_WORK}/${LO_TARBALL}.sha256" "${LO_URL}.sha256"
(cd "$LO_WORK" && sha256sum -c "${LO_TARBALL}.sha256") # fails the document on mismatch
tar -xzf "${LO_WORK}/${LO_TARBALL}" -C "$LO_WORK"
# The folder name after extraction has the full version (e.g. 26.8.1.1), so use a wildcard
LO_RPMS=("$LO_WORK"/LibreOffice_*_Linux_x86-64_rpm/RPMS/*.rpm)
[ -e "${LO_RPMS[0]}" ] || fail "No RPM files were found after extracting ${LO_TARBALL}."
dnf install -y "${LO_RPMS[@]}"
log "Installed LibreOffice ${LO_VERSION} (previous: ${LO_INSTALLED:-none})"
fi
# Libraries needed to start in headless mode (without them libXinerama.so.1 etc. are not found)
dnf install -y libXinerama libXrender libXext libX11 libSM libICE cups-libs dbus-libs libxslt
step_end "Install LibreOffice ${LO_VERSION}"
# ----- soffice link (the RPM command name has the version, e.g. libreoffice26.8) -----
step_begin "Link /usr/local/bin/soffice"
LO_SOFFICE="/opt/libreoffice${LO_SERIES}/program/soffice"
[ -x "$LO_SOFFICE" ] || fail "${LO_SOFFICE} was not found after the installation."
ln -sfn "$LO_SOFFICE" /usr/local/bin/soffice
sudo -u ec2-user -H /usr/local/bin/soffice --version
step_end "Link /usr/local/bin/soffice -> ${LO_SOFFICE}"
# ----- Record the installed version -----
mkdir -p "$CONFIG_DIR"
rpm -q --qf '%{VERSION}\n' "libreoffice${LO_SERIES}" > "${CONFIG_DIR}/libreoffice.version"
log "[COMPLETE] ${DOC_NAME} finished successfully (LibreOffice $(cat "${CONFIG_DIR}/libreoffice.version"))."
Tags:
- Key: Cost
Value: !Sub "${SystemName}-${SubName}"
デプロイ手順
- このテンプレートでスタックを作成します。パラメータは、
SystemNameとSubNameを Cognito 編、EC2 インスタンス編と同じ値にします。ソフトウェアのバージョンは、必要に応じて変更します。 - スタックが完了したら、Systems Manager のコンソールの「ドキュメント」で、6つのドキュメントができていることを確認します。
ドキュメントの実行手順
AWS マネジメントコンソールから、Run Command で実行します。
インストールするソフトウェアのバージョンを指定できるドキュメントでは、既定値が入っています。変えたいときだけ書き換えます。
複数のドキュメントを同じ EC2 インスタンスに同時に実行しないでください。
ステータスが「成功」になれば完了です。出力の最後に、次の行が表示されます。
[COMPLETE] Setup-CodeServer finished successfully.
失敗したときは、ステータスが「失敗」になります。出力の [ERROR] の行にある原因を取り除いてから、同じドキュメントをもう一度実行してください。再実行しても安全なように作ってあります。出力の全体は、CloudWatch Logs の ide-setup ロググループで確認できます。
運用について
証明書の更新
Let’s Encrypt の証明書の有効期間は、現在は 90日ですが、将来は短くなっていく予定です。
このテンプレートは、証明書の自動更新を設定していません。代わりに、手動で更新するスクリプトを、SSM ドキュメントが ec2-user のホームフォルダに作成します。
| スクリプト | 作るドキュメント | 対象の証明書 |
|---|---|---|
/home/ec2-user/renew-cert-codeserver.sh |
Setup-CodeServer |
code-server の FQDN |
/home/ec2-user/renew-cert-kirocrew.sh |
Setup-KiroCrew |
Kiro Crew の FQDN |
code-server のターミナルから、sudo で実行します。証明書エラーで code-server にアクセスできなくなった場合は、Session Manager で OS にログインします。
sudo /home/ec2-user/renew-cert-codeserver.sh
sudo /home/ec2-user/renew-cert-kirocrew.sh
ソフトウェアのバージョンアップ
ドキュメントを再実行すると、バージョンアップできるものと、できないものがあります。ドキュメントでバージョンアップできないソフトウェアのバージョンアップ方法は以下の通りです。
| ソフトウェア | バージョンアップの方法 |
|---|---|
| Kiro CLI | ターミナルで kiro-cli update を実行します。 |
| Kiro Crew | ターミナルで kirocrew update を実行します。 |
| uv | ターミナルで uv self update を実行します。 |
| pyenv | ターミナルで pyenv update を実行します。 |
| nginx、certbot | ターミナルで sudo dnf upgrade --releasever=latest nginx certbot python3-certbot-dns-route53 を実行します。 |
| VS Code 拡張機能 | 自動更新を無効にしているため、code-server の拡張機能の画面から更新します。 |
まとめ
いかがでしたでしょうか。
EC2 インスタンスの中のソフトウェア導入を、SSM ドキュメントにまとめました。ここまで実行できれば、自分用の code-server / Kiro CLI / Kiro Crew 環境が EC2 環境で立ち上げられます。
SSM ドキュメントのコマンドは私が決めただけなので、適宜必要なものに変更すれば他の用途にも応用可能です。
本記事が皆様のお役に立てれば幸いです。

