初心易得,始終難守
C'est la vie.© 2002 - 2026
  • 我是誰-Who Am I
  • 我在哪-Where Am I
  • 我是什麼-What Am I
  • 年鑑-YearBook
    • 二零零六年终总结
    • 一吻定情—二零零八年年终总结
    • 突如其来的明天—二零零九年年终总结
    • 人生大起大落得太快——二零一零年年终总结
    • 贰零①①年年终总结-女朋友已经成家了
    • 贰零壹贰年年终总结-奔波的肿瘤
    • 贰零壹叁年年终总结
    • 雪字怎么写-贰零壹肆年年终总结
    • 每个不曾表白的今天,都是对青春的亏欠-贰零壹伍年年终总结
    • 按部就班的IT 人生-貳零貳肆年年終總結
    • 真正的閱讀-貳零貳伍年年終總結
  • 連結
RSS
5 月 20 日, 2026 年

AI 生成了到2050年的二十四節氣給iPhone 使用

Ken 隨筆 0 Comments

因為iPhone 自帶的農曆不知道從什麼時候又變了,節氣消失了。

然後呢,台灣人和香港人都沒有人要做這個東西,中國人很愛做,但用起來都不盡人意,只好讓AI 做了一個。

以下是AI 完成 –

1. 研究節氣計算方法

  • 搜索了多個來源(Wikipedia、香港天文台 HKO、GitHub 項目)
  • 確認 24 節氣本質是太陽黃經每 15° 的時刻(從春分 0° 到大寒 300°,共 360°)
  • 找到 HKO 2025 年的精確節氣日期作為驗證基準

2. 選擇計算工具

  • 本機沒有天文計算庫,安裝了 PyEphem(pip3 install --break-system-packages ephem)
  • PyEphem 使用 VSOP87 理論計算太陽位置,用二分法搜索太陽到達目標黃經的時刻

3. 解決精度問題

  • 第一版用 ephem.Ecliptic(sun) 不帶 epoch → 因歲差導致座標偏移,結果錯誤
  • 第二版改用 ephem.Ecliptic(sun, epoch=date) → 使用「黃道of date」座標系,概念正確
  • 但與 HKO 對比發現系統性偏差:所有節氣一致地早了約 5.93 小時(PyEphem 的 Delta T 或內部模型與 HKO 使用的英國皇家航海年鑑數據有差異)
  • 加入 +5.93h 常數修正後,2025 年 24 個節氣全部與 HKO 完全匹配(24/24)

4. 生成 ICS 文件

  • 計算 2026–2050 年共 600 個節氣事件
  • 輸出標準 iCalendar 格式(RFC 5545),包含:
  • 全天事件(VALUE=DATE)
  • 透明事件(TRANSP:TRANSPARENT,不佔行程)
  • 訂閱刷新間隔 30 天(REFRESH-INTERVAL:P30D)
  • 日曆名稱「二十四節氣」

5. 最終產出

  • /Users/Ken/solar_terms_2026_2050.ics — 放到 web server 供 iPhone 訂閱,我已經放到了 https://bbken.org/js/solar_terms_2026_2050.ics,歡迎取用。
  • /Users/Ken/generate_solar_terms.py — 可重新生成或調整年份範圍。
4 月 29 日, 2026 年

硬體供應鏈危機

Ken 隨筆 0 Comments

隨著AI 的野蠻擴張,這個世界的硬體供應鏈似乎已然出現危機,因為AI 對於HDD 的需求過大,三大HDD 廠商的產能已經完全被訂購一空,8Tb HDD 的價格,居然比我幾年前買的時候,價格上漲了一倍。

也就是說,如果現在我的NAS HDD壞掉一顆,我將要付出巨大的成本。

這對於終端來說是危機,但對於三大HDD 廠商,何嘗不是機遇呢?所以有些人啊,非常狹隘,總是從自己的角度考慮問題。

雲端服務商也開始漲價,但是,這對於核心客戶而言,不管漲價或是不漲價他們都是要用的,自建datacenter 又不是一朝一夕,對於散客而言,具備專業知識的,則會選擇中小型雲端服務商,不具備專業知識的,還是會選擇三大,文檔可能會更完備,必要的時候還可以找一下客服,但大多數問題其實可以問AI,根本不需要去找人。

前幾天在路上看到一台好乾淨的檳榔車,無論是輪轂,還是腳踏板,車身,後照鏡,都很乾淨,可以趕得上日本了。

肚臍眼左側還是會時不時的隱隱作痛,臺大醫院的肝膽腸胃科總是掛不到號,也不知道什麼什麼時候才能做得上腸鏡和胃鏡。

4 月 27 日, 2026 年

例行的清明掃墓,度了一個劫

Ken 隨筆 0 Comments

倒不是因為觸了霉頭,我這個人不信邪,我說過,就算是超自然的力量奪走我的生命,我到死也不會承認他的存在。

上山的前一天開始發燒,不知道是吃錯了什麼東西,還是收拾家裡陳舊的物品時,帶起來陳舊的細菌或是病毒,特別是我媽留下放了十年的那一堆酵素,每次回家我都要把他們倒掉然後扔掉瓶子,因為實在是太多了。也許是傾倒的時候吸入了某些細菌或病毒。


發燒的第一天我自己並沒有任何感覺,但是Apple Watch開始告警,夜間的心率升高,靜息呼吸頻率也變高,我沒注意。

看來這個東西是很準確的,Apple Watch 是個好夥伴。

清明上山的時候除了肚子有點痛,頭有點痛,腳發軟,覺得睏,打電話的時候居然跟Miley 一樣不自覺的蹲在了地上,沒有其他的感覺,其實應該已經是燒到38度以上了。

燒到第三天,叫小姑送我去醫院,然後到了社區診所,醫生摸了下脈搏,開了退燒藥,然後用生薑蓋在肚皮上,艾灸了10分鐘。

當天晚上開始心率狂跳,一直出汗,折騰整夜。(應該是身體的白血球在劇烈的進攻細菌)

總之呢,燒了四天之後,去了醫院急診,開始轉入感染科住院,血液檢查提示有感染,開始IV 廣譜抗生素,

血檢報告提示:

尿檢未見異常

糞便檢測未見異常

PCR 病毒檢測未見異常

武漢肺炎 病毒檢測未見異常

沒有嘔吐,沒有腹瀉,沒有咳嗽,除了肚臍左側有一點點痛和發燒,沒有其他任何症狀。

IV 兩天針對好氧菌和厭氧菌的不同抗生素,體溫不但沒有下降,反而上升了一點點,從37.6 到了38,護士已經拿來了退燒藥,說告訴我讓我吃就吃。

CT 做完,沒有看出什麼問題,醫生試圖以不明原因發燒忽悠過去,於是我要求做增強CT,然後增強CT 做出來,還是沒看出來什麼問題。

我想,看得出來問題的話,我大概就出不了院了。

要求強制出院,準備趕回台灣,最多是在機場入境被攔下來然後救護車送去醫院,總比在這小山村的醫院什麼都查不出來好。

他媽的,辦完出院就退燒了。

小姑和小姑丈一直在照料我,因為大概率可能是吃了他們家的東西出的問題,但根本原因還是我脆弱的腸胃。

計劃是,妹妹幫我叫了一台車直接送我去重慶的酒店。

Miley 當晚飛到重慶等我,第二天和我一起飛回去。

但是他媽的,辦完出院就退燒了。

雖然精神還是沒有很好,畢竟燒了六天,但是感覺問題不大,小又一定要來找我,於是在酒店見面。

小又終於是又離婚了,怎麼說呢,小又一直沒什麼自我獨立意識,從小家裡富裕,不缺錢,但是因為媽媽的原因,又對婚姻和男人有著傳統意義上的要求,大概是因為她爸爸在男女關係上為所欲為,所以她反而沒有為所欲為,而是走了一條非常傳統的道路,真是讓人意外,本來以為她可以像她爸爸一樣,成為交際花的,在她開那間酒吧的時候,這是我對她的期待,算是落空了。

小王也離婚了,葳君和我都不意外,畢竟她幾乎每次見面都會說她要離婚,但付諸實施後,她似乎並不希望更多人知道這個消息,看起來她只告訴了少數幾個人,好在兩個兒子應該有很多事情需要她忙,並不會讓她過於空閒而至於最後要去看身心科。

勇君現在是ByteDance 重慶的市場總監,每天有很多巴結他的人,以前的同學也有很多巴結他,畢竟隨便放點流量過去,就可以把那隻豬吹上風口。

至於從軍隊退役的野人,聽說每次都騙勇君說兩個人吃飯,然後等勇君去了就會發現一群地方領導等著給他敬酒……

抵達桃園機場的時候,並沒有被溫度探測儀檢測到體溫過高,因為已經沒有發燒了,只好回家,第二天去了臺大醫院,剛好還能掛上一個號。

開了抽血單,第三天從健保app 上看了結果,所有的指標都已經恢復正常。

台灣的血檢數值和中國還不一樣,但對於不是醫生的我來說,不過也是看個熱鬧罷了。

4 月 25 日, 2026 年

如何使用中央氣象署的api 獲取當前天氣和預報

Ken 隨筆 0 Comments

我之前一直通過n8n 使用OpenWeatherMap 的api 來獲取天氣和預報並通過pushover 發送到手機,但是,由於台灣是海島型氣候,天氣多變,預報往往不太準確,而比較權威的the weather channel 又不提供api,中央氣象署的api 其實我很早就註冊了,之前沒有AI 幫忙,他們的文檔又非常讓人困擾,所以沒有使用。

今天讓AI 看了一下他們的文檔,終於替換掉了OpenWeatherMap 的api 。

中央氣象署開放資料平臺之資料擷取API – https://opendata.cwa.gov.tw/dist/opendata-swagger.html#/

除了實時天氣之外,還有預報信息,由於台灣很小,所以可以具體到某一個區域,例如板橋區。

但問題在於,板橋區的實時天氣和預報是兩個不同的api,而UV index 則不是在所有的氣象監測站都有,比如板橋區就沒有,新北只有一個。

所以,為了生成這麼一條信息,需要調取三個api。

當然,首先您需要去註冊一個中央氣象署會員 – https://pweb.cwa.gov.tw/emember/register/authorization

註冊成功後,在會員資料部分,從“API授權碼” 中申請一個授權碼,您將會在後面的每一個api call 中使用他。

我記得我註冊的時候他還是中央氣象局,升級署了。

天氣預報api,板橋區監測站

https://opendata.cwa.gov.tw/api/v1/rest/datastore/F-D0047-069?Authorization=YOUR-CWB-API-KEY&LocationName=板橋區

實時天氣api,距離我最近的監測站

https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0001-001?Authorization=YOUR-CWB-API-KEY&StationId=C0AJ80

UV index api, 只有比較大的站有。

https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0003-001?Authorization=YOUR-CWB-API-KEY&StationId=466881

之所以使用三個api 是因為實時天氣我選了距離我最近的一個,如果您不需要那麼準確,可以只用新北區的監測站就可以。

n8n 的 workflow 中,因為merge block 只支持兩個input,所以只能使用串行執行,如果只用兩個api 的話就可以merge ,稍快,但對於每個小時發送一次的天氣來說,這點時間差異其實沒有必要,串行就可以了。

workflow 大概這個樣子。

其中呢,code block 是把這三個api 的結果梳理一下,送給pushover。

const obs = $('CWB Realtime Banqiao District').first().json.records.Station[0];
const we = obs.WeatherElement;
const fc = $('CWB Forecast Banqiao District').first().json.records.Locations[0].Location[0];
const uvStation = $('CWA UV Index').first().json.records.Station[0].WeatherElement;

const el = {};
for (const e of fc.WeatherElement) el[e.ElementName] = e.Time;

const at = el['體感溫度'];
const pop = el['3小時降雨機率'];
const wx = el['天氣現象'];
const rh = el['相對濕度'];
const ci = el['舒適度指數'];

const days = {};
for (const t of at) {
  const d = t.DataTime.slice(0, 10);
  if (!days[d]) days[d] = {};
  const h = t.DataTime.slice(11, 13);
  if (h === '06') days[d].morn = t.ElementValue[0].ApparentTemperature;
  if (h === '12') days[d].day = t.ElementValue[0].ApparentTemperature;
  if (h === '21') days[d].night = t.ElementValue[0].ApparentTemperature;
}
for (const t of wx) {
  const d = t.StartTime.slice(0, 10);
  if (!days[d]) days[d] = {};
  if (t.StartTime.slice(11, 13) === '12') days[d].wx = t.ElementValue[0].Weather;
}
for (const t of pop) {
  const d = t.StartTime.slice(0, 10);
  if (!days[d]) days[d] = {};
  if (t.StartTime.slice(11, 13) === '12') days[d].pop = t.ElementValue[0].ProbabilityOfPrecipitation;
}
for (const t of rh) {
  const d = t.DataTime.slice(0, 10);
  if (!days[d]) days[d] = {};
  if (t.DataTime.slice(11, 13) === '12') days[d].rh = t.ElementValue[0].RelativeHumidity;
}
for (const t of ci) {
  const d = t.DataTime.slice(0, 10);
  if (!days[d]) days[d] = {};
  if (t.DataTime.slice(11, 13) === '12') days[d].comfort = t.ElementValue[0].ComfortIndexDescription;
}

const dayTemp = {};
for (const t of at) {
  const d = t.DataTime.slice(0, 10);
  const v = parseFloat(t.ElementValue[0].ApparentTemperature);
  if (!dayTemp[d]) dayTemp[d] = { min: v, max: v };
  if (v < dayTemp[d].min) dayTemp[d].min = v;
  if (v > dayTemp[d].max) dayTemp[d].max = v;
}

const sorted = Object.keys(days).sort();

return [{json: {
  station: obs.StationName,
  weather: we.Weather !== '-99' ? we.Weather : uvStation.Weather,
  temp: we.AirTemperature,
  humidity: we.RelativeHumidity,
  wind: we.WindSpeed,
  windDir: el['風向'][0].ElementValue[0].WindDirection,
  rain: we.Now.Precipitation,
  pressure: we.AirPressure,
  high: we.DailyExtreme.DailyHigh.TemperatureInfo.AirTemperature,
  low: we.DailyExtreme.DailyLow.TemperatureInfo.AirTemperature,
  pop: pop[0].ElementValue[0].ProbabilityOfPrecipitation,
  comfort: ci[0].ElementValue[0].ComfortIndexDescription,
  feelsLike: at[0].ElementValue[0].ApparentTemperature,
  uv: uvStation.UVIndex,
  d0_morn: days[sorted[0]]?.morn || '-',
  d0_night: days[sorted[0]]?.night || '-',
  d1_wx: days[sorted[1]]?.wx || '-',
  d1_comfort: days[sorted[1]]?.comfort || '-',
  d1_min: dayTemp[sorted[1]]?.min || '-',
  d1_max: dayTemp[sorted[1]]?.max || '-',
  d1_rh: days[sorted[1]]?.rh || '-',
  d1_pop: days[sorted[1]]?.pop || '-',
  d2_wx: days[sorted[2]]?.wx || '-',
  d2_comfort: days[sorted[2]]?.comfort || '-',
  d2_min: dayTemp[sorted[2]]?.min || '-',
  d2_max: dayTemp[sorted[2]]?.max || '-',
  d2_rh: days[sorted[2]]?.rh || '-',
  d2_pop: days[sorted[2]]?.pop || '-',
}}];

在pushover 的block 中將這些output 整理成人類看得懂的樣子。

3 月 5 日, 2026 年

如何使用 Lambda 來完成WordPress Spot instance 的滾動更新

Ken Tech 0 Comments

AI 說使用Terraform 不是適用於這個場景的,使用Lambda 更合適,好吧,您也可以使用一台EC2 或者本地部署他,這樣Lambda 的錢也不用付。

畢竟我們的目的是省錢。

添加一個Lambda function ,名字叫RotateSpotInstance,修改Timeout 為 15min,因為這個過程可能需要比較長的時間,特別是在製作AMI 的部分。

為自動生成的IAM role
RotateSpotInstance-role-mhe3v2sg 添加下面的權限,為什麼是這些?您可以看一下下面需要完成的幾步工作 –

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeSpotFleetInstances",
        "ec2:CreateImage",
        "ec2:DescribeImages",
        "ec2:CreateLaunchTemplateVersion",
        "ec2:ModifyLaunchTemplate",
        "ec2:ModifySpotFleetRequest",
        "ec2:TerminateInstances",
        "ec2:CreateTags",
        "ec2:DescribeInstanceStatus"
      ],
      "Resource": "*"
    }
  ]
}

Lambda 的Configuration 配置按照實際情況,例如:

LAUNCH_TEMPLATE_ID = lt-0716c882cb57a921d
SPOT_FLEET_ID = sfr-c7ccc145-d71d-4268-8b67-089161e02af6

這個function 通過這樣幾步來完成這項工作 –

# 1. Get current instance – 從當前的spot fleet 中獲取運行中的instance id。
# 2. Create AMI – 從當前運行中的instance 創建一個AMI。
# 3. Wait for AMI – 檢測並且等待這個AMI 創建完成。
# 4. Update Launch Template – 將創建完成的AMI id 更新到模板。
# 5. Increase capacity – 修改spot fleet request 的Total target capacity 為2,這樣會自動起一台新的先。
# 6. Wait for new instance to be healthy – 從instance status checks 判斷,等待新啟動的instance 通過健康檢查。
# 7. Terminate old instance – 當新啟動的instance 通過健康檢查後,終止舊的instance。
# 8. Restore capacity – 立刻修改spot fleet request 的Total target capacity 為1,這樣就不會再起更多的instance。

import boto3
import os
import time
import urllib.request
import urllib.error
from datetime import datetime


def lambda_handler(event, context):
    SPOT_FLEET_ID = os.environ['SPOT_FLEET_ID']
    LAUNCH_TEMPLATE_ID = os.environ['LAUNCH_TEMPLATE_ID']

    ec2 = boto3.client('ec2')  # 自動使用 Lambda 所在的 region

    try:
        print(f"Starting rotation for Spot Fleet: {SPOT_FLEET_ID}")

        # 1. 取得目前的 instance
        response = ec2.describe_spot_fleet_instances(
            SpotFleetRequestId=SPOT_FLEET_ID
        )

        if not response['ActiveInstances']:
            return {'statusCode': 400, 'error': 'No active instances'}

        instance_id = response['ActiveInstances'][0]['InstanceId']
        print(f"Current instance: {instance_id}")

        # 2. 建立 AMI
        ami_name = f"wordpress-{datetime.now().strftime('%Y%m%d-%H%M')}"
        ami_response = ec2.create_image(
            InstanceId=instance_id,
            Name=ami_name,
            NoReboot=True,
            TagSpecifications=[{
                'ResourceType': 'image',
                'Tags': [{'Key': 'auto-delete', 'Value': 'no'}]
            }]
        )
        ami_id = ami_response['ImageId']
        print(f"Creating AMI: {ami_id}")

        # 3. 等 AMI 可用
        print("Waiting for AMI to be available...")
        waiter = ec2.get_waiter('image_available')
        waiter.wait(
            ImageIds=[ami_id],
            WaiterConfig={'Delay': 30, 'MaxAttempts': 40}
        )
        print(f"AMI {ami_id} is available")

        # 4. 更新 Launch Template —— 重點修正區段
        # 4a. 建立新版本,拿到實際版本號
        new_version_response = ec2.create_launch_template_version(
            LaunchTemplateId=LAUNCH_TEMPLATE_ID,
            SourceVersion='$Latest',
            LaunchTemplateData={'ImageId': ami_id}
        )
        new_version_number = str(
            new_version_response['LaunchTemplateVersion']['VersionNumber']
        )
        print(f"Created launch template version {new_version_number} with AMI {ami_id}")

        # 4b. 用實際版本號設定 default(不是 '$Latest')
        ec2.modify_launch_template(
            LaunchTemplateId=LAUNCH_TEMPLATE_ID,
            DefaultVersion=new_version_number
        )

        # 4c. 驗證 default version 真的更新了
        lt_desc = ec2.describe_launch_templates(
            LaunchTemplateIds=[LAUNCH_TEMPLATE_ID]
        )
        actual_default = str(lt_desc['LaunchTemplates'][0]['DefaultVersionNumber'])
        if actual_default != new_version_number:
            raise Exception(
                f"Default version mismatch: expected {new_version_number}, "
                f"got {actual_default}"
            )
        print(f"Verified launch template default version = {actual_default}")

        # 4d. 稍等 Spot Fleet 看到新的 default
        print("Waiting 60s for Spot Fleet to pick up new launch template version...")
        time.sleep(60)

        # 5. 增加 capacity —— 會用新的 AMI 啟動
        ec2.modify_spot_fleet_request(
            SpotFleetRequestId=SPOT_FLEET_ID,
            TargetCapacity=2
        )
        print("Increased capacity to 2")

        # 6. 等新 instance 變健康,並驗證它是用新 AMI 啟動
        new_instance_id = wait_for_new_instance(
            ec2, SPOT_FLEET_ID, instance_id, expected_ami_id=ami_id
        )
        print(f"New instance {new_instance_id} is healthy and using new AMI")

        # 7. 終止舊 instance
        ec2.terminate_instances(InstanceIds=[instance_id])
        print(f"Terminated old instance: {instance_id}")

        # 8. 恢復 capacity
        ec2.modify_spot_fleet_request(
            SpotFleetRequestId=SPOT_FLEET_ID,
            TargetCapacity=1
        )
        print("Restored capacity to 1")

        return {
            'statusCode': 200,
            'ami_id': ami_id,
            'launch_template_version': new_version_number,
            'old_instance': instance_id,
            'new_instance': new_instance_id
        }

    except Exception as e:
        print(f"Error: {str(e)}")
        try:
            ec2.modify_spot_fleet_request(
                SpotFleetRequestId=SPOT_FLEET_ID,
                TargetCapacity=1
            )
            print("Rolled back capacity to 1")
        except Exception as rollback_error:
            print(f"Rollback failed: {rollback_error}")
        return {'statusCode': 500, 'error': str(e)}


def check_http_port(ec2, instance_id, port=80):
    """透過 HTTP HEAD 檢查 port 是否可連線"""
    try:
        response = ec2.describe_instances(InstanceIds=[instance_id])
        instance = response['Reservations'][0]['Instances'][0]
        public_ip = instance.get('PublicIpAddress') or instance.get('PrivateIpAddress')

        if not public_ip:
            return False

        try:
            req = urllib.request.Request(f'http://{public_ip}:{port}', method='HEAD')
            urllib.request.urlopen(req, timeout=5)
            print(f"HTTP port {port} is responding on {public_ip}")
            return True
        except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError):
            return False

    except Exception as e:
        print(f"Error checking HTTP port: {e}")
        return False


def get_instance_ami(ec2, instance_id):
    """取得 instance 實際使用的 AMI ID"""
    try:
        resp = ec2.describe_instances(InstanceIds=[instance_id])
        return resp['Reservations'][0]['Instances'][0].get('ImageId')
    except Exception as e:
        print(f"Error getting AMI for {instance_id}: {e}")
        return None


def wait_for_new_instance(ec2, spot_fleet_id, old_instance_id,
                          expected_ami_id=None, max_wait=600):
    """
    等新 instance 出現、變 running、HTTP 通。
    若提供 expected_ami_id,會驗證新 instance 確實使用該 AMI,
    避免 Spot Fleet 用到舊 template 卻被誤判為成功。
    """
    print("Waiting for new instance to be healthy...")

    for i in range(max_wait // 10):
        response = ec2.describe_spot_fleet_instances(
            SpotFleetRequestId=spot_fleet_id
        )
        instances = response['ActiveInstances']

        if len(instances) < 2:
            print(f"Waiting for new instance... ({i*10}s)")
            time.sleep(10)
            continue

        new_instances = [inst for inst in instances
                         if inst['InstanceId'] != old_instance_id]

        if not new_instances:
            print(f"No new instance found yet... ({i*10}s)")
            time.sleep(10)
            continue

        new_instance_id = new_instances[0]['InstanceId']

        # 驗證新 instance 使用的是預期的 AMI
        if expected_ami_id:
            actual_ami = get_instance_ami(ec2, new_instance_id)
            if actual_ami != expected_ami_id:
                raise Exception(
                    f"New instance {new_instance_id} is using AMI {actual_ami}, "
                    f"expected {expected_ami_id}. "
                    f"Launch template default version may not have been picked up."
                )
            print(f"Confirmed new instance {new_instance_id} uses AMI {actual_ami}")

        try:
            status_response = ec2.describe_instance_status(
                InstanceIds=[new_instance_id],
                IncludeAllInstances=True
            )

            if not status_response['InstanceStatuses']:
                print(f"Instance {new_instance_id} status not available yet...")
                time.sleep(10)
                continue

            status = status_response['InstanceStatuses'][0]
            instance_state = status['InstanceState']['Name']

            print(f"Instance: {instance_state}")

            # running 後檢查 HTTP port 80
            if instance_state == 'running':
                if check_http_port(ec2, new_instance_id, port=80):
                    print(f"Instance {new_instance_id} is fully healthy!")
                    return new_instance_id
                else:
                    print("Instance running but HTTP port 80 not ready yet...")

        except Exception as e:
            print(f"Error checking status: {e}")

        time.sleep(10)

    raise Exception(f"New instance didn't become healthy within {max_wait}s")

Deploy 到Lambda,Publish!

trigger it –

aws lambda invoke \
  --function-name RotateSpotInstance \
  --region ap-northeast-3 \
  --invocation-type Event \
  response.json

View logs in real-time –

aws logs tail /aws/lambda/RotateSpotInstance \
  --follow \
  --region ap-northeast-3


2026-03-05T06:33:57.808000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 INIT_START Runtime Version: python:3.14.v35 Runtime Version ARN: arn:aws:lambda:ap-northeast-3::runtime:35b4fe1ff6a2b42e1513619f35af63e09acce626823e1d0e547d6393c854bc71
2026-03-05T06:33:58.102000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 START RequestId: 7bf945e2-dcbf-4345-a7fb-8e811842cf69 Version: $LATEST
2026-03-05T06:34:00.771000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Starting rotation for Spot Fleet: sfr-c7ccc145-d71d-4268-8b67-089161e02af6
2026-03-05T06:34:01.142000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Current instance: i-0e187b0c5195efaa6
2026-03-05T06:34:01.538000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Creating AMI: ami-07590290f72731092
2026-03-05T06:34:01.538000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Waiting for AMI to be available...
2026-03-05T06:34:59.256000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b INIT_START Runtime Version: python:3.14.v35 Runtime Version ARN: arn:aws:lambda:ap-northeast-3::runtime:35b4fe1ff6a2b42e1513619f35af63e09acce626823e1d0e547d6393c854bc71
2026-03-05T06:34:59.567000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b START RequestId: 3e94eeea-257e-4203-b1af-9cd12adab8d4 Version: $LATEST
2026-03-05T06:35:02.268000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Starting rotation for Spot Fleet: sfr-c7ccc145-d71d-4268-8b67-089161e02af6
2026-03-05T06:35:02.664000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Current instance: i-0e187b0c5195efaa6
2026-03-05T06:35:03.024000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Creating AMI: ami-0c75c75b4f29b190e
2026-03-05T06:35:03.024000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Waiting for AMI to be available...
2026-03-05T06:36:01.324000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f INIT_START Runtime Version: python:3.14.v35 Runtime Version ARN: arn:aws:lambda:ap-northeast-3::runtime:35b4fe1ff6a2b42e1513619f35af63e09acce626823e1d0e547d6393c854bc71
2026-03-05T06:36:01.627000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f START RequestId: d9621da3-bd40-4aed-a028-1ac35f2ed22c Version: $LATEST
2026-03-05T06:36:01.980000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 AMI ami-07590290f72731092 is available
2026-03-05T06:36:02.354000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Updated Launch Template to use ami-07590290f72731092
2026-03-05T06:36:02.542000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Increased capacity to 2
2026-03-05T06:36:02.542000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Waiting for new instance to be healthy...
2026-03-05T06:36:02.652000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Waiting for new instance... (0s)
2026-03-05T06:36:04.407000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f Starting rotation for Spot Fleet: sfr-c7ccc145-d71d-4268-8b67-089161e02af6
2026-03-05T06:36:04.783000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f Current instance: i-0e187b0c5195efaa6
2026-03-05T06:36:05.128000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f Creating AMI: ami-035bf50417603be87
2026-03-05T06:36:05.128000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f Waiting for AMI to be available...
2026-03-05T06:36:12.748000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Waiting for new instance... (10s)
2026-03-05T06:36:22.920000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance: running, System: initializing, Check: initializing
2026-03-05T06:36:33.082000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance: running, System: initializing, Check: initializing
2026-03-05T06:36:43.263000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance: running, System: initializing, Check: initializing
2026-03-05T06:36:53.430000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:03.489000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b AMI ami-0c75c75b4f29b190e is available
2026-03-05T06:37:03.598000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:03.889000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Updated Launch Template to use ami-0c75c75b4f29b190e
2026-03-05T06:37:04.054000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Increased capacity to 2
2026-03-05T06:37:04.054000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Waiting for new instance to be healthy...
2026-03-05T06:37:04.212000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:13.770000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:14.379000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:23.922000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:24.542000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:34.083000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:34.704000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:44.269000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:44.869000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:54.661000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance: running, System: initializing, Check: initializing
2026-03-05T06:37:55.047000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Instance: running, System: initializing, Check: initializing
2026-03-05T06:38:04.816000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance: running, System: ok, Check: ok
2026-03-05T06:38:04.816000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Instance i-0638e625f7b42d66a is fully healthy!
2026-03-05T06:38:04.816000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 New instance i-0638e625f7b42d66a is healthy
2026-03-05T06:38:05.113000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Terminated old instance: i-0e187b0c5195efaa6
2026-03-05T06:38:05.229000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Instance: running, System: ok, Check: ok
2026-03-05T06:38:05.229000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Instance i-0638e625f7b42d66a is fully healthy!
2026-03-05T06:38:05.229000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b New instance i-0638e625f7b42d66a is healthy
2026-03-05T06:38:05.309000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 Restored capacity to 1
2026-03-05T06:38:05.333000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 END RequestId: 7bf945e2-dcbf-4345-a7fb-8e811842cf69
2026-03-05T06:38:05.333000+00:00 2026/03/05/[$LATEST]4e889110fba78fe4e51e9fb26315e4d6 REPORT RequestId: 7bf945e2-dcbf-4345-a7fb-8e811842cf69  Duration: 247229.90 ms  Billed Duration: 247521 ms  Memory Size: 128 MB Max Memory Used: 98 MB  Init Duration: 290.80 ms
2026-03-05T06:38:05.496000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Terminated old instance: i-0e187b0c5195efaa6
2026-03-05T06:38:05.580000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f AMI ami-035bf50417603be87 is available
2026-03-05T06:38:05.630000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b Restored capacity to 1
2026-03-05T06:38:05.647000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b END RequestId: 3e94eeea-257e-4203-b1af-9cd12adab8d4
2026-03-05T06:38:05.647000+00:00 2026/03/05/[$LATEST]4907e5924ad374a64d41ffd64da3c19b REPORT RequestId: 3e94eeea-257e-4203-b1af-9cd12adab8d4  Duration: 186080.32 ms  Billed Duration: 186387 ms  Memory Size: 128 MB Max Memory Used: 97 MB  Init Duration: 306.42 ms
2026-03-05T06:38:05.944000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f Updated Launch Template to use ami-035bf50417603be87
2026-03-05T06:38:06.115000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f Error: An error occurred (FleetNotInModifiableState) when calling the ModifySpotFleetRequest operation: Fleet Request: sfr-c7ccc145-d71d-4268-8b67-089161e02af6 is not in a modifiable state.
2026-03-05T06:38:06.555000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f Rolled back capacity to 1
2026-03-05T06:38:06.588000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f END RequestId: d9621da3-bd40-4aed-a028-1ac35f2ed22c
2026-03-05T06:38:06.588000+00:00 2026/03/05/[$LATEST]5fd07f95f7fb4ff087fdefaab8e80e9f REPORT RequestId: d9621da3-bd40-4aed-a028-1ac35f2ed22c  Duration: 124960.33 ms  Billed Duration: 125260 ms  Memory Size: 128 MB Max Memory Used: 97 MB  Init Duration: 299.41 ms

有併發數量的問題,限制Lambda 只能1個進程跑 –

aws lambda put-function-concurrency \
–function-name RotateSpotInstance \
–reserved-concurrent-executions 1 \
–region ap-northeast-3
{
“ReservedConcurrentExecutions”: 1
}

確實清爽多了,也不會有服務中斷的問題。

當然了,為了服務的持續性,您需要使用Dynamic DNS 來將新的instance ip report 到DNS server 並且設置script 在 boot 的時候自動執行。

例如ddclient 更新到Cloudflare –

INFO:    [cloudflare][private.bbken.org]> getting Cloudflare Zone ID
INFO:    [cloudflare][private.bbken.org]> Zone ID is 0933028cb8e70c5cb4f0c736be6fee37
INFO:    [cloudflare][private.bbken.org]> setting IPv4 address to 10.4.41.150
SUCCESS: [cloudflare][private.bbken.org]> IPv4 address set to 10.4.41.150
INFO:    [cloudflare][kix.bbken.org]> getting Cloudflare Zone ID
INFO:    [cloudflare][kix.bbken.org]> Zone ID is 0933028cb8e70c5cb4f0c736be6fee37
INFO:    [cloudflare][kix.bbken.org]> setting IPv4 address to 172.15.168.113
SUCCESS: [cloudflare][kix.bbken.org]> IPv4 address set to 172.15.168.113
INFO:    [cloudflare][kix.bbken.org]> setting IPv6 address to 2406:da16:a8d:2bc6:5a03:80fb:3a46:796
SUCCESS: [cloudflare][kix.bbken.org]> IPv6 address set to 2406:da16:a8d:2bc6:5a03:80fb:3a46:796
1 2 3 4›»

過 客

  1. R2 on 卷進了美商5 月 15 日, 2024 年

    终于回来了,好。

  2. Ken on Mommy最後的樣子11 月 6 日, 2023 年

    也沒有很久吧,最近終於閒下來

  3. R2 on Mommy最後的樣子10 月 26 日, 2023 年

    好久不见

  4. Ken on 天朝Loli控组曲(带歌词,修正版)10 月 12 日, 2023 年

    哈哈哈,祝福你,好人一生平安

  5. liu on 天朝Loli控组曲(带歌词,修正版)10 月 12 日, 2023 年

    hello,我在找天朝lolicon组曲时发现了你的博客,感谢你十四年前做出的贡献,祝一切安好

May 2026
S M T W T F S
 12
3456789
10111213141516
17181920212223
24252627282930
31  
« Apr    

Spam Blocked

103,815 spam blocked by Akismet

↑

© 初心易得,始終難守 2026
Powered by WordPress • Themify WordPress Themes