CloudFormation 提供了基礎設施即程式碼(IaC)的便利性,但有時需要與其原生不支援的服務互動,或在 AWS 外部建立資源。此時,自定義資源(Custom Resources)便能派上用場。透過結合 Lambda 函式,自定義資源可以擴充套件 CloudFormation 的能力,滿足更彈性的資源組態需求。對於已有一定規模且維護既有 CR 的團隊來說,理解其運作方式至關重要。本文將探討如何使用自定義資源與外部系統互動,並以建立 RDS 資料函式庫為例,示範其應用。此方法能有效橋接 CloudFormation 與外部服務或資源,讓開發者能以一致的 IaC 方式管理所有基礎設施。
使用自定義資源在AWS以外建立資源
CloudFormation通常是第一個支援新AWS服務的基礎設施即程式碼(IaC)工具。然而,有時我們需要與不支援CloudFormation的服務進行通訊,甚至需要在AWS以外建立資源。目前,CloudFormation已經增加了對擴充套件的支援。其中,私有登入檔允許開發人員構建自己的資源型別,我們將在第8章中探討。在此之前,開發人員會使用自定義資源(CR)——一種使用AWS Lambda作為資源提供者的CloudFormation資源型別。如果您在全新的環境中工作,建議採用私有登入檔。然而,一些成熟的公司和團隊可能仍在使用需要維護的CR。在本章中,我們將使用CR來建立外部資源,這是一種在AWS以外建立自定義資源的傳統方法。
理解自定義資源(CR)
當我們在CloudFormation範本中宣告資源時,會提供一個Type屬性。該屬性聲明瞭要建立的服務和資源,這意味著CloudFormation知道該去哪裡、做什麼以及呼叫哪些API。AWS是主要的名稱空間,而後面的部分是另一個名稱空間,用於宣告服務。如果是EC2,CloudFormation將向EC2的API傳送呼叫。如果是RDS,則向RDS的API傳送呼叫。最後一部分是我們要建立的實際資源。自定義資源是指不屬於CloudFormation原生支援的資源,可以是外部提供者、內部或自託管系統,甚至是不支援CloudFormation的服務。
自定義資源的宣告
自定義資源可以以長格式(AWS::CloudFormation::CustomResource)或短格式(Custom::MyResourceName)宣告。在堆積疊操作期間,CloudFormation會向我們在資源屬性中定義的端點傳送特定的有效負載。例如:
Cr:
Type: Custom::MyResource
Properties:
ServiceToken: "..."
Key1: Value1
Key2:
- list_element1
- list_element2
CloudFormation將生成以下有效負載:
{
"RequestType": "Create/Update/Delete",
"ResponseURL": "http://...",
"StackId": "...",
"RequestId": "uniqueId for this request",
"ResourceType": "Custom::MyResource",
"LogicalResourceId": "Cr",
"ResourceProperties": {
"Key1": "Value1",
"Key2": [
"list_element1",
"list_element2"
]
}
}
該有效負載包含資源宣告中的欄位、堆積疊ID、請求型別(建立資源、更新資源或刪除資源)以及Lambda函式必須傳送回應物件的URL。
Lambda函式的內部工作原理
Lambda函式是由事件觸發的程式碼。執行時,它接收事件和上下文物件,並執行內部程式碼來處理這些物件。上下文物件只是Lambda函式執行的後設資料,可以用於自我維護和優雅關閉,而事件物件包含我們要關注的有效負載。
編寫和管理自定義資源
在本章中,我們將重點介紹使用AWS Lambda開發CR。Lambda函式的程式碼如下所示:
import boto3
import json
def lambda_handler(event, context):
# 處理事件
if event['RequestType'] == 'Create':
# 建立資源
pass
elif event['RequestType'] == 'Update':
# 更新資源
pass
elif event['RequestType'] == 'Delete':
# 刪除資源
pass
# 傳送回應
send_response(event, context)
def send_response(event, context):
# 建構回應物件
response_body = {
'Status': 'SUCCESS',
'Reason': 'See the details in CloudWatch Log Stream: ' + context.log_stream_name,
'PhysicalResourceId': context.log_stream_name,
'StackId': event['StackId'],
'RequestId': event['RequestId'],
'LogicalResourceId': event['LogicalResourceId'],
'Data': {}
}
# 傳送回應
response_url = event['ResponseURL']
json_response = json.dumps(response_body)
boto3.client('s3').put_object(Body=json_response, Bucket='your-bucket', Key='response.json')
# 或者使用HTTPS PUT請求
# requests.put(response_url, headers={'content-type': ''}, data=json_response)
內容解密:
此Lambda函式處理CR的有效負載,根據請求型別(建立、更新或刪除)執行相應的操作。然後,它向CloudFormation傳送回應,報告操作的結果。回應包含狀態(成功或失敗)、原因、物理資源ID、堆積疊ID、請求ID和邏輯資源ID等資訊。
處理自定義資源的更新、刪除和故障
在處理CR時,我們需要考慮更新、刪除和故障處理。更新CR時,我們需要確定是否需要更新底層資源。刪除CR時,我們需要確保刪除相關資源。故障處理則需要考慮如何處理錯誤和異常。
自定義資源的生命週期
graph LR
A[建立CR] --> B[呼叫Lambda函式]
B --> C[處理建立請求]
C --> D[傳送回應]
D --> E[更新CR]
E --> F[呼叫Lambda函式]
F --> G[處理更新請求]
G --> H[傳送回應]
H --> I[刪除CR]
I --> J[呼叫Lambda函式]
J --> K[處理刪除請求]
K --> L[傳送回應]
圖表翻譯: 此圖表顯示了自定義資源的生命週期,包括建立、更新和刪除CR的過程。Lambda函式在每個階段被呼叫,以處理相應的請求並傳送回應。
使用自定義資源在 AWS 以外建立資源
自定義資源的運作原理
在處理自定義資源(Custom Resources, CRs)時,我們需要解析堆積疊(stack)的資訊,執行我們的邏輯,並回應給 CloudFormation。回應中應包含以下欄位:
- 狀態(Status):成功(SUCCESS)或失敗(FAILED)
- 實體資源 ID(Physical Resource ID):由於是自定義資源,我們需要自行決定資源 ID
- 堆積疊 ID(Stack ID):與 CR 請求中的相同
- 請求 ID(Request ID):與 CR 請求中的相同
- 邏輯資源 ID(Logical Resource ID):與 CR 請求中的相同
- 資料(Data):可選,用於內建函式
Fn::GetAtt
在執行完資源的建立或更新後,自定義資源的 Lambda 函式必須向原始請求中的 ResponseURL 發送回應。該回應必須採用 JSON 格式。
成功回應範例:
{
"Status": "SUCCESS",
"RequestId": "...",
"LogicalResourceId": "...",
"StackId": "...",
"PhysicalResourceId": "MyResourceUniqueId1234",
"Data": {
"Attr1": "foo",
"Attr2": "bar"
}
}
失敗回應範例:
{
"Status": "FAILED",
"Reason": "This failed because of reasons",
"RequestId": "...",
"LogicalResourceId": "...",
"StackId": "...",
"PhysicalResourceId": "MyResourceUniqueId1234"
}
失敗原因(Reason)將顯示在 CloudFormation 控制檯的事件(Events)部分,因此提供詳細的失敗原因至關重要。
Lambda 函式的內部運作
Lambda 支援的自定義資源流程如下:
- 使用者執行堆積疊操作,例如建立、更新或刪除堆積疊。
- CloudFormation 產生包含堆積疊和資源元資料的負載(payload)。
- CloudFormation 呼叫 Lambda 函式並傳遞負載。
- Lambda 函式執行資源組態邏輯並回應 CloudFormation。
Lambda 函式範例程式碼:
import cfnresponse
def handler(event, context):
input_props = event['ResourceProperties']
# 執行自定義邏輯
out = generate_id(event['ResourceType'])
cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physicalResourceId=out)
使用 cfn-response 模組
cfn-response 是一個包裝器,用於生成回應物件並將其傳送回 CloudFormation。使用此模組可簡化回應 CloudFormation 的過程。
自定義資源實作範例:建立 RDS 資料函式庫
由於 CloudFormation 不直接支援在 RDS 例項中建立資料函式庫,我們需要開發自定義資源來實作此功能。
自定義資源定義(YAML):
CustomDb:
Type: Custom::DB
Properties:
ServiceToken: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:CustomDbFunction'
DBName: mydatabase
DBUser: myuser
DBPassword: mypassword
RDSEndpoint: my-rds-instance.cdxvysqgdxqq.us-west-2.rds.amazonaws.com
RDSUser: admin
RDSPassword: adminpassword
Lambda 函式實作(Python):
import cfnresponse
import pymysql
def handler(event, context):
physicalResourceId = ""
try:
input_props = event['ResourceProperties']
required_props = ['DBName', 'RDSEndpoint', 'RDSUser', 'RDSPassword']
missing_props = [prop for prop in required_props if prop not in input_props]
if missing_props:
raise CustomResourceException(f"Required properties are missing: {missing_props}")
# 連線 RDS 例項
conn = pymysql.connect(
host=input_props['RDSEndpoint'],
user=input_props['RDSUser'],
password=input_props['RDSPassword']
)
# 建立資料函式庫和使用者
with conn.cursor() as cur:
cur.execute(f"CREATE DATABASE {input_props['DBName']}")
cur.execute(f"CREATE USER '{input_props['DBUser']}'@'%' IDENTIFIED BY '{input_props['DBPassword']}'")
cur.execute(f"GRANT ALL PRIVILEGES ON {input_props['DBName']}.* TO '{input_props['DBUser']}'@'%'")
conn.commit()
physicalResourceId = f"{input_props['DBName']}-{input_props['DBUser']}"
cfnresponse.send(event, context, cfnresponse.SUCCESS, physicalResourceId=physicalResourceId)
except Exception as e:
cfnresponse.send(event, context, cfnresponse.FAILED, reason=str(e))
程式碼解密:
- 檢查必要屬性:首先檢查
ResourceProperties中是否包含所有必要的屬性,如DBName、RDSEndpoint、RDSUser和RDSPassword。若缺少任何屬性,則回傳失敗回應並拋出自定義異常。 - 連線 RDS 例項:使用
pymysql函式庫連線指定的 RDS 例項。 - 執行資料函式庫操作:在連線成功後,執行 SQL 指令以建立資料函式庫、使用者,並授與相應的許可權。
- 提交變更並回應 CloudFormation:提交資料函式庫變更,並向 CloudFormation 傳送成功或失敗的回應。