從 AWS S3 移轉到 Cloudflare Pages
我將這個部落格從 AWS S3 移轉到 Cloudflare Pages。過去是用 S3 做靜態網站托管,透過 CloudFront 發佈,但為了簡化管理及提升效能,決定採用 Cloudflare Pages。
我將這個部落格從 AWS S3 移轉到 Cloudflare Pages。過去是用 S3 做靜態網站托管,透過 CloudFront 發佈,但為了簡化管理及提升效能,決定採用 Cloudflare Pages。
Amazon S3 Files 是一項可以將 S3 桶直接掛載到 EC2 等計算資源的服務,並且以 NFS 檔案系統的形式運行。資料保持在 S3 中,可以使用一般的檔案操作(ls、cp、cat 等)進行讀寫。
S3 Files 是基於 Amazon EFS 建立的共享檔案系統,讓使用者能以檔案系統的形式存取 S3 桶中的數據。
主要特點如下:
| 項目 | 內容 |
|---|---|
| 協定 | NFS 4.1 / 4.2 |
| 支援的計算資源 | EC2、Lambda、ECS、EKS |
| 同時連接數量 | 最多 25,000 個計算資源 |
| 讀取吞吐量 | 最大 TB/秒 |
| IOPS | 超過 1,000 萬/桶 |
| 加密 | TLS(傳輸中)+ AWS KMS(儲存時) |
| 檔案系統功能 | POSIX 權限、檔案鎖定、讀取後寫入一致性 |
S3 Files 將被訪問的資料自動載入高效能儲存系統,並以低延遲的方式提供服務。
高效能儲存中的資料,如果在一定時間內(預設 30 天,可設置為 1 - 365 天)未被訪問,將自動刪除。
AmazonS3FilesClientFullAccess 管理策略S3 Files 需要兩個 IAM 角色。
使用管理控制台時會自動創建,因此不需要手動創建
此角色用於讓 S3 Files 存取桶。
# 創建角色
aws iam create-role \
--role-name S3Files-FileSystem-Role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "s3files.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}'
# 附加 S3 Files 客戶端策略
aws iam attach-role-policy \
--role-name S3Files-FileSystem-Role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FilesClientFullAccess
在創建檔案系統時用 --role-arn 指定此角色。
未附加 IAM 角色會導致掛載失敗
在 CloudShell 中創建以下角色。
# 創建角色
aws iam create-role \
--role-name EC2-S3Files-Role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}'
# 附加 S3 Files 客戶端策略
aws iam attach-role-policy \
--role-name EC2-S3Files-Role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FilesClientFullAccess
# 創建並附加實例檔案配置
aws iam create-instance-profile \
--instance-profile-name EC2-S3Files-Profile
aws iam add-role-to-instance-profile \
--instance-profile-name EC2-S3Files-Profile \
--role-name EC2-S3Files-Role
將此角色附加到實例上。
在 S3 控制台中創建通用桶,也可以使用現有的桶。
需要特別注意的是,必須啟用桶的版本控制設定

從控制台創建後,所有可用區會自動創建掛載目標和存取點。

記錄下輸出的檔案系統 ID(例如:fs-0123456789abcdef0)。
在終端中執行以下命令。
# 創建掛載點
sudo mkdir /mnt/s3files
# 挂載
sudo mount -t s3files fs-0123456789abcdef0:/ /mnt/s3files
如果無法掛載,請執行以下命令再試一次:
sudo dnf install -y amazon-efs-utils # Amazon Linux, RHEL
# sudo apt install -y amazon-efs-utils (Ubuntu, Debian)
如果在執行 dnf 命令時通訊出現問題,請設置 S3 端點(網關),並指定與實例相同的可用區。
但要注意,S3 端點的路由表應與實例所在的子網相同。
確認掛載情況:
df -h /mnt/s3files
應顯示類似以下內容:
Filesystem Size Used Avail Use% Mounted on
<s3files-dns> 8.0E 129M 8.0E 1% /mnt/s3files
cd /mnt/s3files
# 創建檔案
sudo sh -c 'echo "Hello, s3 Files!" > test.txt'
# 讀取檔案
cat test.txt
# 創建目錄
sudo mkdir test-directory
ls -la
# 複製檔案
sudo cp test.txt test-directory/
cd test-directory/
# 確認檔案列表
ls -la
寫入的檔案會在約 1 分鐘內與 S3 桶同步。可以在 S3 控制台確認對象已創建。
aws s3 ls s3://<bucket-name>/
為了在重啟後保持掛載,需要將以下內容添加到 /etc/fstab。
# 添加到 /etc/fstab
fs-0123456789abcdef0:/ /mnt/s3files s3files _netdev,nofail 0 0
_netdev 是一個選項,確保在網路連接後再進行掛載,這是必需的。加入 nofail 可以防止掛載失敗時導致實例無法啟動。
S3 Files 的收費如下:
為按量計費的無需配置模式,根據 AWS 的說法,與傳統 S3 和檔案系統間數據複製相比可減少最多 90% 的成本。
ls、cat、cp 等正常的檔案操作/etc/fstab 設定自動掛載,即使重啟後也能維持在使用 AWS SAM 建立的檔案儲存 API 的使用者認證中,我使用了 Amazon Cognito。最近我新增了透過密碼金鑰 (WebAuthn) 登入的功能,因此總結一下設定內容。
要在 Cognito 中使用密碼金鑰,必須具備以下所有條件。
| 要件 | 本次的架構 |
|---|---|
| UserPool 層級 | ESSENTIALS 以上 |
| 管理式登入 | v2 (新的登入 UI) |
| 自訂網域 | login.example.com (用於 Relying Party ID) |
Cognito 的密碼金鑰必須從管理式登入 v2 UI 中註冊及使用。在 LITE 層級 (免費) 中無法使用 WebAuthn,因此需要 ESSENTIALS 層級。
第一次登入時使用密碼,然後從帳戶設定中註冊密碼金鑰。
註冊後,可以透過「使用密碼金鑰登入」按鈕直接認證。
為了新增密碼金鑰,我在 template.yaml (SAM 模板) 上進行了僅 6 行的變更。
UserPool:
Type: AWS::Cognito::UserPool
Properties:
# ...
Policies:
PasswordPolicy:
MinimumLength: 8
# ...
MfaConfiguration: "OFF"
UserPool:
Type: AWS::Cognito::UserPool
Properties:
# ...
Policies:
PasswordPolicy:
MinimumLength: 8
# ...
SignInPolicy:
AllowedFirstAuthFactors:
- PASSWORD
- WEB_AUTHN # ← 新增密碼金鑰
MfaConfiguration: "OFF"
WebAuthnRelyingPartyID: login.example.com # ← 指定 RP ID
WebAuthnUserVerification: required # ← 強制要求生物認證
SignInPolicy.AllowedFirstAuthFactors初步認證步驟中可用的認證方式列表。只有 PASSWORD 時僅能使用密碼,若加上 WEB_AUTHN 則能選擇密碼金鑰。
WebAuthnRelyingPartyIDWebAuthn 的 Relying Party ID (RP ID)。密碼金鑰將綁定到此網域生成和儲存,因此必須與 實際提供登入頁面的網域一致。
本次直接使用自訂網域 login.example.com。若使用 Cognito 的預設網域 (xxx.auth.ap-northeast-1.amazoncognito.com),則需指定該網域。
WebAuthnUserVerification使用密碼金鑰時的用戶確認等級。
| 值 | 說明 |
|---|---|
required | 強制要求生物認證或 PIN 等的身份確認 |
preferred | 儘可能要求身份確認,但可不要求 |
discouraged | 省略身份確認 (不使用生物認證等) |
為了提高安全性,選擇了 required。
在管理式登入 v2 的界面中,密碼金鑰設定後,登入畫面將自動新增「使用密碼金鑰登入」按鈕。第一次註冊時需先使用密碼登入,然後可在帳戶設定中新增密碼金鑰。
sam build
sam deploy --no-confirm-changeset
由於 samconfig.toml 中已定義堆疊名稱、區域及參數,因此每次部署時不需要指定選項。
啟用 Cognito 密碼金鑰的重點總結如下:
SignInPolicy.AllowedFirstAuthFactors 中增加 WEB_AUTHNWebAuthnUserVerification: required 強制要求生物認證僅透過 6 行的變更,即可使用密碼金鑰登入。此時仍保留密碼,逐步向密碼金鑰過渡,這正是 Cognito 的便利之處。
Claude 若要透過 API 使用,除了直接使用 Anthropic API 外,也能經由 AWS Bedrock、Google Vertex AI、Microsoft Azure (Azure AI Foundry) 使用。基本價格各路徑幾乎相同,但在批次處理與與雲端生態系統整合面會有差異。
單位:USD / 1M 代幣 (MTok)。資料截至 2026 年 3 月。
| 模型 | 項目 | Anthropic API | Bedrock | Vertex AI | Azure |
|---|---|---|---|---|---|
| Claude Opus 4.6 | 輸入 | $5.00 | $5.00 | $5.00 | $5.00 |
| 輸出 | $25.00 | $25.00 | $25.00 | $25.00 | |
| Claude Sonnet 4.6 | 輸入 | $3.00 | $3.00 | $3.00 | $3.00 |
| 輸出 | $15.00 | $15.00 | $15.00 | $15.00 | |
| Claude Haiku 4.5 | 輸入 | $1.00 | $1.00 | $1.00 | $1.00 |
| 輸出 | $5.00 | $5.00 | $5.00 | $5.00 | |
| Claude Sonnet 4.5 | 輸入 | $3.00 | $3.00 | $3.00 | $3.00 |
| 輸出 | $15.00 | $15.00 | $15.00 | $15.00 |
基本價格在各路徑相同。
不過在 Vertex AI 若不是使用全球端點而指定區域端點(regional endpoint),標準價格會額外加收 10%。Bedrock 有 Long Context 變體(另有 SKU),但價格相同。Anthropic API 則已將 Long Context 整合在一般模型中。
Prompt 快取(Prompt Caching)的費用在各路徑也相同。
| 模型 | 快取類型 | Anthropic API | Bedrock | Vertex AI | Azure |
|---|---|---|---|---|---|
| Claude Opus 4.6 | 5 分快取寫入 | $6.25 | $6.25 | $6.25 | $6.25 |
| 1 小時快取寫入 | $10.00 | $10.00 | $10.00 | $10.00 | |
| 快取讀取 | $0.50 | $0.50 | $0.50 | $0.50 | |
| Claude Sonnet 4.6 | 5 分快取寫入 | $3.75 | $3.75 | $3.75 | $3.75 |
| 1 小時快取寫入 | $6.00 | $6.00 | $6.00 | $6.00 | |
| 快取讀取 | $0.30 | $0.30 | $0.30 | $0.30 | |
| Claude Haiku 4.5 | 5 分快取寫入 | $1.25 | $1.25 | $1.25 | $1.25 |
| 1 小時快取寫入 | $2.00 | $2.00 | $2.00 | $2.00 | |
| 快取讀取 | $0.10 | $0.10 | $0.10 | $0.10 |
快取寫入依 TTL 分為 5 分(短期)和 1 小時(長期)兩種。長期快取的寫入成本較高,但如果系統提示(system prompt)很長且會被重複參考,透過節省讀取費用通常是划得來的。
Bedrock、Vertex AI、Anthropic API 三者的非同步批次 API 可享按需價格的 50% 折扣。Azure 目前未明示。
| 模型 | 批次輸入 | 批次輸出 |
|---|---|---|
| Claude Opus 4.6 | $2.50 | $12.50 |
| Claude Sonnet 4.6 | $1.50 | $7.50 |
| Claude Haiku 4.5 | $0.50 | $2.50 |
| Claude Sonnet 4.5 | $1.50 | $7.50 |
若要大量處理資料(例如日誌分析、向量嵌入生成等)並頻繁使用批次處理,無論走哪個路徑都能把成本砍半。
| 比較重點 | Anthropic API | Bedrock | Vertex AI | Azure |
|---|---|---|---|---|
| 基本價格 | 相同 | 相同 | 相同 | 相同 |
| 區域加價 | — | — | +10%(區域端點) | — |
| 批次處理(50% OFF) | ○ | ○ | ○ | 未明示 |
| 東京區域 | — | ○ | ○ | — |
| IAM / 審計日誌整合 | — | AWS | Google Cloud | Azure |
| VPC / PrivateLink | — | ○ | ○ | ○ |
| 計費整合 | Anthropic 直接 | AWS | Google Cloud | Azure |
| 新功能推出速度 | 最快 | 會延遲 | 會延遲 | 會延遲 |
新功能(例如 Extended Thinking)通常會先在 Anthropic API 上推出,向 Vertex AI、Bedrock、Azure 的推展有時會晚幾週。
想要在這個部落格加入留言功能,因此在 AWS 無伺服器架構下自建了一個 API。介紹其設計與實作。
請求以以下流程處理。
瀏覽器 (www.hikari-dev.com)
↓ HTTPS
API Gateway
├── GET /comment?postId=... → 取得留言
├── POST /comment → 發表留言
└── PATCH /comment/{id} → 管理(切換隱藏)
↓
Lambda (Node.js 20 / arm64)
↓
DynamoDB(儲存留言)
+ SES v2(通知管理者)
程式碼以 TypeScript 撰寫,並以 SAM(Serverless Application Model)管理基礎設施即程式碼(IaC)。Lambda 採用 arm64(Graviton2)以稍微節省成本。
資料表名稱為 blog-comments,分區鍵為 postId,排序鍵為 commentId。
| 鍵 | 型別 | 說明 |
|---|---|---|
postId | String | 文章的識別值(例: /blog/2026/03/20/hime) |
commentId | String | ULID(可時序排序的 ID) |
因為在排序鍵使用 ULID,使用 QueryCommand 取得的留言會自動按發表順序排列。這也是為什麼沒有使用 UUID 的原因。
在將留言寫入 DynamoDB 之前,會與 keywords.json 中定義的關鍵字比對。
若命中關鍵字,會以 isHidden: true 自動隱藏,並附加 isFlagged: "1"。若未命中則立即公開。
isFlagged 作為稀疏 GSI(Sparse GSI)的鍵使用。未命中的留言不會帶入此屬性,因此不會在 GSI 中增加多餘的分區,對成本與效能都有利。只要在 DynamoDB Document Client 設定 removeUndefinedValues: true 就能達成。
每次有留言發表時,會用 SES v2 發信通知自己。郵件內容包含發言者名稱、內容、評分、IP 位址以及是否被標記(flag)等資訊。
郵件發送採非同步進行,即使發送失敗也會忽略錯誤(不影響使用者流程)。這是為了避免影響留言發表的回應速度。
雖然會在 DynamoDB 中儲存 IP 位址與 User-Agent,但不會把它們包含在 GET 端點的回應中。我們在型別定義層就進行分離。
| 層級 | 對策 |
|---|---|
| 網路 | 使用 AWS WAF 設定每個 IP 每 5 分鐘 100 次的速率限制 |
| CORS | 僅允許 https://www.hikari-dev.com |
| 管理 API | API Gateway 的 API Key 認證(X-Api-Key 標頭) |
| 垃圾留言 | 關鍵字過濾自動隱藏 |
管理端點(PATCH /comment/{id})只要在 SAM 模板設定 ApiKeyRequired: true 即可啟用 API 金鑰認證。無需自行實作 Lambda Authorizer,較為簡單。
採用無伺服器架構後不需管理伺服器,DynamoDB 的按需付費讓流量低的個人部落格也能把成本降到最低。
程式碼以 SAM + TypeScript + esbuild 打包,僅需執行 sam build && sam deploy 即可部署。
自分専用のファイル共有システムが欲しいと思い、AWS のサーバーレスサービスだけでファイルストレージサービスを作りました。
この記事では、設計で意識したポイントと、実際のアーキテクチャを紹介します。
制作した Web システムは、Web ブラウザからファイルのアップロード・ダウンロード・フォルダ管理ができるクラウドストレージサービスです。
以下、構成図です。

認証の大部分は Cognito で行い、 ファイル転送は S3 の Presigned URL を Lambda で発行してクライアントと S3 が直接やり取りする仕組みです。
| レイヤー | 技術 |
|---|---|
| バックエンド | C# (.NET 8) / AWS Lambda |
| 認証 | Amazon Cognito + Managed Login v2 |
| API | API Gateway (REST) + Cognito Authorizer |
| ストレージ | Amazon S3 |
Cognito の OAuth 2.0 エンドポイントと Managed Login を活用し、認証機能を実現しました。
最終的に認証系の Lambda は TokenFunction 1 つだけになりました。
機能的にもセキュリティ的にも減らせるコードは減らすのが吉です。
AWS のサービスがやってくれることを自前で書く必要はありません。
ファイルのアップロード・ダウンロードで Lambda を経由すると、いくつかの問題が生じます:
Presigned URL なら、Lambda は URL を発行するだけで、実際のファイル転送はブラウザと S3 が直接行います。
Lambda の実行時間は数十ミリ秒で済み、ファイルサイズの制約も S3 の上限までとなります。
アップロードの流れ:
1. ブラウザ → Lambda: 「file.pdf をアップロードしたい!アップロード先の URL を送れ~」
2. Lambda → ブラウザ: 「アップロード先の Presigned URL だよ。ここに PUT してね~」
3. ブラウザ → S3: 「S3 に PUT するよ~」
4. ブラウザ → Lambda: 「アップロード完了したよ~」
S3 にはフォルダごとダウンロードする機能がありません。
複数ファイルの一括ダウンロードは、Lambda 上で ZIP を生成して一時的に S3 に置き、その Presigned URL を返す方式にしました。
一時 ZIP ファイルは S3 のライフサイクルルールで 1 日後に自動削除されるので、ゴミが溜まることはありません。
| 対策 | 実装 |
|---|---|
| ブルートフォース防止 | Cognito 標準のロック機能 (5 回失敗: 15 分ロック) |
| API 保護 | Cognito Authorizer による JWT 検証 |
| CORS | AllowedOrigin を特定のドメインに限定 |
| 一時ファイル管理 | S3 ライフサイクルで不要なファイルを 1 日で自動削除 |
サーバーレス構成なので、利用がなければコストはほぼゼロです。
個人利用なら月額数十円〜数百円程度に収まります。
インフラ全体を 1 つの template.yaml (AWS SAM) で定義しています。
Cognito User Pool、API Gateway、Lambda 3 関数、S3 バケット、CloudWatch アラーム、SNS — すべてのリソースを 600 行程度の YAML で定義しています。
PS C:\> aws ec2-instance-connect ssh --instance-id i-0aa38de21acf2aa1c --region ap-south-1
Bad permissions. Try removing permissions for user: \\OWNER RIGHTS (S-1-3-4) on file C:/Users/hikari/AppData/Local/Temp/tmpm9m1bf7j/private-key.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions for 'C:\\Users\\hikari\\AppData\\Local\\Temp\\tmpm9m1bf7j\\private-key' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "C:\\Users\\hikari\\AppData\\Local\\Temp\\tmpm9m1bf7j\\private-key": bad permissions
ec2-user@192.168.0.4: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
截至 2025/06/11 的驗證。
PS C:\> wsl -- aws ec2-instance-connect ssh --instance-id i-0aa38de21acf2aa1c --region ap-south-1
, #_
~\_ ####_ Amazon Linux 2023
~~ \_#####\
~~ \###|
~~ \#/ ___ https://aws.amazon.com/linux/amazon-linux-2023
~~ V~' '->
~~~ /
~~._. _/
_/ _/
_/m/'
Last login: Tue Jun 10 22:50:33 2025 from 192.168.0.183
[ec2-user@ip-192-168-0-4 ~]$
為什麼?
降級後就能連線。
希望能修正。
參考: https://github.com/aws/aws-cli/issues/9114
msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2-2.17.35.msi
EC2 Instance Connect 是用來簡化對 AWS EC2 執行個體的 SSH 連線的服務。
過去的 SSH 連線方式需要事先在執行個體放置公開金鑰,但使用 EC2 Instance Connect 可以將臨時的 SSH 公開金鑰傳送到執行個體以建立連線。(不過,除部分 AMI 外,需要安裝 Instance Connect 的套件)
連線到執行個體的方法有好幾種。

直接從網際網路連線的方式需要經由 Internet Gateway 或 NAT Gateway,且需要公有 IP 位址,因此若限定在私有網路則無法使用。
可以直接使用 ssh 指令,因此是最簡單的方式。
ssh <使用者名稱>@<公有 IP 位址>
使用 AWS CLI 並透過 EC2 Instance Connect 端點連線時,不需要公有 IP 位址。
另外,還可以省下那部分的費用(每月數百日圓)。
使用 AWS CLI 可用類似下面的指令連線,但需事先匯入金鑰對並為執行個體設定金鑰對。
例如可以使用以下指令:
aws ec2-instance-connect ssh --private-key-file .ssh/id_ed25519 --os-user <使用者名稱> --instance-id <執行個體 ID> --connection-type eice
※ 不過,要事先取得存取金鑰並用 aws configure 設定好。
如果不想讓執行個體連到網際網路,且想使用非官方 AMI,採用此連線方式最合適。
若是 Amazon Linux 或 Ubuntu,只要建立 Instance Connect 端點,就可以從管理主控台連到執行個體。
不過,除部分 AMI 外,仍需安裝 Instance Connect 的套件。
詳情請見:https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/ec2-instance-connect-set-up.html
需為 Session Manager 建立兩個端點,並將允許透過 Session Manager 連線的 IAM 角色附加到執行個體。
此外,除部分 AMI 外,需安裝 Session Manager 的套件。
使用序列埠主控台可以直接連到執行個體。請注意,若未設定密碼就無法登入。
預設會允許所有流量,因此若使用預設設定通常不需要特別調整。
以下列出最基本需要的設定。
入站規則需允許 SSH(22)。
因為執行個體的 SSH 伺服器預設使用 22 埠,故需允許該埠。
出站規則需允許自訂 TCP(1024-65535)。
1024-65535 是 SSH 連線時客戶端會使用的埠範圍。
入站規則需允許 SSH(22)。
此設定為必要。
安全性群組是有狀態(stateful)的,所以通常不需要設定出站規則。
因為為有狀態,通常不需要設定入站規則。
需允許 SSH(22)。
因為要對執行個體的 22 埠進行通訊,所以必須允許此埠。
從官方頁面取得 AMI。
https://rockylinux.org/ja-JP/download
選擇要設定給實例的架構 (ARM (aarch64)),並選擇 Cloud Images 裡的 AWS AMI。

以版本號過濾,找到符合條件的映像。

AMI ID 無法直接複製,因此點擊 Deploy 按鈕,然後從 AWS 主控台複製。
用 AMI ID 搜尋會出現如下

用擁有者過濾會比較好。
擁有者 = 792107900819

ssh-keygen -t ed25519 指令產生公鑰,將 .ssh/id_ed25519.pub 匯入成 Key pair比起 NAT Gateway,使用公開 IP 比較便宜,所以建立 Elastic IP。
架構圖大概長這樣。


建立 EC2 Instance Connect 端點後,可以從 AWS CLI 登入。
因此我用以下條件建立。
在 PC 上打開終端機,執行以下指令。
aws ec2-instance-connect ssh --private-key-file .ssh/id_ed25519 --os-user rocky --instance-id i-*****************
Rocky Linux 的 AMI 映像沒有包含 Instance Connect 套件,無法從管理控制台連線。因此需要安裝套件。
參考 https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/ec2-instance-connect-set-up.html 下載套件。
curl https://amazon-ec2-instance-connect-us-west-2.s3.us-west-2.amazonaws.com/latest/linux_arm64/ec2-instance-connect.rhel8.rpm -o /tmp/ec2-instance-connect.rpm
curl https://amazon-ec2-instance-connect-us-west-2.s3.us-west-2.amazonaws.com/latest/linux_amd64/ec2-instance-connect-selinux.noarch.rpm -o /tmp/ec2-instance-connect-selinux.rpm
sudo dnf install -y /tmp/ec2-instance-connect.rpm /tmp/ec2-instance-connect-selinux.rpm
安裝完成後,就可以從 AWS 管理控制台存取。

我做了 CDK 範例,放上來供參考。
請記得更改 keyName(Key pair)的名稱。
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
export interface RockyLinuxStackProps extends cdk.StackProps {
}
export class RockyLinuxStack extends cdk.Stack {
public constructor(scope: cdk.App, id: string, props: RockyLinuxStackProps = {}) {
super(scope, id, props);
// Resources
const ec2dhcpOptions = new ec2.CfnDHCPOptions(this, 'EC2DHCPOptions', {
domainName: 'ap-south-1.compute.internal',
domainNameServers: [
'AmazonProvidedDNS',
],
],
});
ec2dhcpOptions.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2InternetGateway = new ec2.CfnInternetGateway(this, 'EC2InternetGateway', {
{
value: 'igw',
key: 'Name',
},
],
});
ec2InternetGateway.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2vpc = new ec2.CfnVPC(this, 'EC2VPC', {
cidrBlock: '10.0.0.0/16',
enableDnsSupport: true,
instanceTenancy: 'default',
enableDnsHostnames: true,
{
value: 'vpc',
key: 'Name',
},
],
});
ec2vpc.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2VPCGatewayAttachment = new ec2.CfnVPCGatewayAttachment(this, 'EC2VPCGatewayAttachment', {
vpcId: ec2vpc.ref,
internetGatewayId: ec2InternetGateway.ref,
});
ec2VPCGatewayAttachment.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2NetworkAcl = new ec2.CfnNetworkAcl(this, 'EC2NetworkAcl', {
vpcId: ec2vpc.ref,
],
});
ec2NetworkAcl.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2RouteTable = new ec2.CfnRouteTable(this, 'EC2RouteTable', {
vpcId: ec2vpc.ref,
});
ec2RouteTable.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2SecurityGroup = new ec2.CfnSecurityGroup(this, 'EC2SecurityGroup', {
groupDescription: 'launch-wizard-1 created 2025-04-27T00:11:58.641Z',
groupName: 'launch-wizard-1',
vpcId: ec2vpc.ref,
securityGroupIngress: [
{
cidrIp: '0.0.0.0/0',
ipProtocol: 'tcp',
fromPort: 22,
toPort: 22,
},
{
cidrIp: '0.0.0.0/0',
ipProtocol: 'icmp',
fromPort: 8,
toPort: -1,
},
],
securityGroupEgress: [
{
cidrIp: '0.0.0.0/0',
ipProtocol: '-1',
fromPort: -1,
toPort: -1,
},
],
});
ec2SecurityGroup.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2Subnet = new ec2.CfnSubnet(this, 'EC2Subnet', {
vpcId: ec2vpc.ref,
mapPublicIpOnLaunch: false,
enableDns64: false,
availabilityZoneId: 'aps1-az1',
privateDnsNameOptionsOnLaunch: {
EnableResourceNameDnsARecord: false,
HostnameType: 'ip-name',
EnableResourceNameDnsAAAARecord: false,
},
cidrBlock: '10.0.0.0/20',
ipv6Native: false,
{
value: 'subnet-public1-ap-south-1a',
key: 'Name',
},
],
});
ec2Subnet.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2InstanceConnectEndpoint = new ec2.CfnInstanceConnectEndpoint(this, 'EC2InstanceConnectEndpoint', {
preserveClientIp: false,
securityGroupIds: [
ec2SecurityGroup.attrGroupId,
],
subnetId: ec2Subnet.attrSubnetId,
});
ec2InstanceConnectEndpoint.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2vpcdhcpOptionsAssociation = new ec2.CfnVPCDHCPOptionsAssociation(this, 'EC2VPCDHCPOptionsAssociation', {
vpcId: ec2vpc.ref,
dhcpOptionsId: ec2dhcpOptions.ref,
});
ec2vpcdhcpOptionsAssociation.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2RouteHg = new ec2.CfnRoute(this, 'EC2RouteHG', {
routeTableId: ec2RouteTable.ref,
destinationCidrBlock: '0.0.0.0/0',
gatewayId: ec2InternetGateway.ref,
});
ec2RouteHg.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2SubnetNetworkAclAssociation = new ec2.CfnSubnetNetworkAclAssociation(this, 'EC2SubnetNetworkAclAssociation', {
networkAclId: ec2NetworkAcl.ref,
subnetId: ec2Subnet.ref,
});
ec2SubnetNetworkAclAssociation.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2SubnetRouteTableAssociation = new ec2.CfnSubnetRouteTableAssociation(this, 'EC2SubnetRouteTableAssociation', {
routeTableId: ec2RouteTable.ref,
subnetId: ec2Subnet.ref,
});
ec2SubnetRouteTableAssociation.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2Instance = new ec2.CfnInstance(this, 'EC2Instance', {
tenancy: 'default',
instanceInitiatedShutdownBehavior: 'stop',
cpuOptions: {
threadsPerCore: 1,
coreCount: 2,
},
blockDeviceMappings: [
{
ebs: {
volumeType: 'gp3',
iops: 3000,
volumeSize: 10,
encrypted: false,
deleteOnTermination: true,
},
deviceName: '/dev/sda1',
},
],
availabilityZone: 'ap-south-1a',
privateDnsNameOptions: {
enableResourceNameDnsARecord: false,
hostnameType: 'ip-name',
enableResourceNameDnsAaaaRecord: false,
},
ebsOptimized: true,
disableApiTermination: false,
keyName: 'hikari',
sourceDestCheck: true,
placementGroupName: '',
networkInterfaces: [
{
privateIpAddresses: [
{
privateIpAddress: '10.0.3.59',
primary: true,
},
],
secondaryPrivateIpAddressCount: 0,
deviceIndex: '0',
groupSet: [
ec2SecurityGroup.ref,
],
ipv6Addresses: [
],
subnetId: ec2Subnet.ref,
associatePublicIpAddress: true,
deleteOnTermination: true,
},
],
imageId: 'ami-0415efd8380284dc4',
instanceType: 't4g.medium',
monitoring: false,
],
creditSpecification: {
cpuCredits: 'unlimited',
},
});
ec2Instance.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2ElasticIp = new ec2.CfnEIP(this, 'EC2ElasticIp', {
domain: 'vpc',
{
key: 'Name',
value: 'elastic-ip',
},
],
});
ec2ElasticIp.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
const ec2EipAssociation = new ec2.CfnEIPAssociation(this, 'EC2EipAssociation', {
eip: ec2ElasticIp.ref,
instanceId: ec2Instance.ref,
});
ec2EipAssociation.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
}
}