Open6
AWS Step Functionsで何かしたい、Discordのgatewayイベントで何かする仕組みを作ってみる
ピン留めされたアイテム
import {
Stack,
StackProps,
aws_iam as iam,
aws_stepfunctions as sfn,
aws_stepfunctions_tasks as tasks,
aws_apigatewayv2 as apigw,
ArnFormat,
CfnOutput,
} from "aws-cdk-lib";
import { Construct } from "constructs";
export class StepStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const httpApi_inner = new apigw.CfnApi(this, "innerapi", {
name: "discord_webhook_stf",
protocolType: "HTTP",
});
//stage
new apigw.CfnStage(this, "innerstage", {
apiId: httpApi_inner.ref,
stageName: "$default",
autoDeploy: true,
});
const integration_inner = new apigw.CfnIntegration(
this,
"sfnIntegrationinner",
{
apiId: httpApi_inner.ref,
integrationType: "HTTP_PROXY",
integrationMethod: "ANY",
integrationUri: "<<ここにDiscord Webhook URL>>",
payloadFormatVersion: "1.0",
requestParameters: {
"overwrite:header.User-Agent":
"DiscordBot (app://my-bot, 0.0.1)",
"overwrite:header.Content-Type": "application/json",
},
}
);
new apigw.CfnRoute(this, "routeinner", {
apiId: httpApi_inner.ref,
routeKey: "ANY /",
authorizationType: "AWS_IAM",
target: `integrations/${integration_inner.ref}`,
});
const stateMachine = new sfn.StateMachine(this, "MyStateMachine", {
definition: new MyCallApiGatewayHttpApiEndpoint(this, "InvokeAPIGW", {
apiId: httpApi_inner.ref,
apiStack: this,
authType: tasks.AuthType.IAM_ROLE,
method: tasks.HttpMethod.POST,
requestBody: sfn.TaskInput.fromJsonPathAt("$"),
}).next(new sfn.Succeed(this, "SUCCEED")),
});
const invokeStepFunctionRole = new iam.Role(this, "MyAPIRole", {
assumedBy: new iam.ServicePrincipal("apigateway.amazonaws.com"),
});
stateMachine.grantStartExecution(invokeStepFunctionRole);
const httpApi = new apigw.CfnApi(this, "MyCfnApi", {
name: "step-MyApi",
protocolType: "HTTP",
});
const integration = new apigw.CfnIntegration(this, "sfnIntegration", {
apiId: httpApi.ref,
credentialsArn: invokeStepFunctionRole.roleArn,
integrationType: "AWS_PROXY",
integrationSubtype: "StepFunctions-StartExecution",
payloadFormatVersion: "1.0",
requestParameters: {
StateMachineArn: stateMachine.stateMachineArn,
Input: "$request.body",
},
});
new apigw.CfnRoute(this, "route", {
apiId: httpApi.ref,
routeKey: "$default",
authorizationType: "AWS_IAM",
target: `integrations/${integration.ref}`,
});
new apigw.CfnStage(this, "DefaultStage", {
apiId: httpApi.ref,
stageName: "$default",
autoDeploy: true,
});
const policy = new iam.PolicyStatement({
actions: ["execute-api:Invoke"],
resources: [
this.formatArn({
service: "execute-api",
resource: httpApi.ref,
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
resourceName: "$default/POST/",
}),
],
});
const user = iam.User.fromUserName(this, "api-user", "<<IAMユーザー名>>");
user.addToPrincipalPolicy(policy);
new CfnOutput(this, "endpoint", {
value: httpApi.attrApiEndpoint,
});
}
}
作るもの
Discordのボイスチャンネルへユーザーが接続・切断したらテキストチャンネルに投稿する
- ボットはVoiceStateUpdateイベントを受信する
- ボットはAPI Gatewayへイベントを転送する
- API Gatewayはワークフローの実行を開始する
- ワークフローはAPI Gatewayを実行する
- API GatewayはDiscordのWebhookでメッセージを投稿する
関連するもの
- Amazon API Gateway
- HTTP API
- IAM認証
- IAMユーザーの署名付きリクエストのみを受け付ける
- 統合
- Step FunctionsのExecutionWorkflowを呼びだす
- AWS Step Functions
- ワークフロー
- API Gateway統合
- タスクとしてAPI Gatewayを実行できる
- 指定できるMethodは1つだけ
- Discordボット
- Rust
- Serenity
- VoiceStateUpdateのイベントハンドラー
- RustでAmazon API GatewayのIAM認証付きHTTP APIを呼ぶ
- Serenity
- Rust
- Discord Webhook
- Amazon API GatewayのHTTP API HTTP_ProxyでDiscordにメッセージを投稿する
- 構成
- AWS CDK
- AWS上のリソースの構成をコードにする
- 構成図を作る
- Githubリポジトリにする
- Github Actionsでデプロイできるようにする
- AWS CDK
- 監視
- 課金対象
- Amazon API Gateway
- AWS Step Functions
- 課金対象
RustでAmazon API GatewayのIAM認証付きHTTP APIを呼ぶ
とりあえず
- rustls
- tokio
- reqwest
execute-api
できるユーザーのアクセスキー
アクセスキーはキーIDとシークレットキーの組み
credentials
署名を作る
Signature Version 4 (SigV4)
use aws_types::{Credentials, SigningService, region::SigningRegion};
use aws_smithy_http::body::SdkBody;
use aws_sig_auth::signer::{OperationSigningConfig, RequestConfig, SigV4Signer};
use http::Request;
use std::time::SystemTime;
use std::env;
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let payload = "{\"IsHelloWorldExample\": true}";
let mut request = Request::builder()
.uri("https://w0kphty5fk.execute-api.us-east-1.amazonaws.com/")
.method("POST")
.header("Content-Type", "application/json")
.body(SdkBody::from(payload))
.unwrap();
let key_id = env::var("key_id").expect("key_id");
let secret = env::var("secret_key").expect("secret_key");
let creds = Credentials::from_keys(key_id, secret, None);
let now = SystemTime::now();
let signer = SigV4Signer::new();
let request_config = RequestConfig {
request_ts: now,
region: &SigningRegion::from_static("us-east-1"),
service: &SigningService::from_static("execute-api"),
payload_override: None,
};
signer.sign(
&OperationSigningConfig::default_config(),
&request_config,
&creds,
&mut request
).unwrap();
let (parts, _) = request.into_parts();
let request = Request::from_parts(parts, payload);
let request: reqwest::Request = request.try_into().unwrap();
let client = reqwest::Client::new();
let res = client.execute(request).await?;
eprintln!("Response: {:?} {}", res.version(), res.status());
eprintln!("Headers: {:#?}\n", res.headers());
let body = res.text().await?;
println!("{}", body);
Ok(())
}
Amazon API GatewayのIAM認証付きHTTP APIを呼ぶためのHTTPリクエスト
どういうリクエストを作るのかPythonでやってみた。
Dateヘッダーでは面倒なので、x-amz-date
を使った
参考:
#以下のコードは、
#https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-signed-request-examples.html
#においてapache2.0ライセンスで配布される
#"Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved"
#なコードをapi gateway呼び出し用に改変したものです
method = 'POST'
endpoint = 'https://w0kphty5fk.execute-api.us-east-1.amazonaws.com/'
host = 'w0kphty5fk.execute-api.us-east-1.amazonaws.com'
region = 'us-east-1'
service = 'execute-api'
content_type = 'application/json'
payload = '{'
payload += '"IsHelloWorldExample": true'
payload += '}'
def sign(key, msg):
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
def getSignatureKey(key, date_stamp, regionName, serviceName):
kDate = sign(('AWS4' + key).encode('utf-8'), date_stamp)
kRegion = sign(kDate, regionName)
kService = sign(kRegion, serviceName)
kSigning = sign(kService, 'aws4_request')
return kSigning
access_key_id = 'AAAAAAAAAAAAAAAAAAAA'
secret_key = 'secret_key'
# Create a date for headers and the credential string
t = datetime.datetime.utcnow()
amz_date = t.strftime('%Y%m%dT%H%M%SZ')
date_stamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope
# ************* TASK 1: CREATE A CANONICAL REQUEST *************
canonical_uri = '/'
canonical_querystring = ''
canonical_headers = f'content-type:{content_type}\nhost:{host}\nx-amz-date:{amz_date}\n'
signed_headers = 'content-type;host;x-amz-date'
payload_hash = hashlib.sha256(payload.encode('utf-8')).hexdigest()
canonical_request = method + '\n'
canonical_request += canonical_uri + '\n'
canonical_request += canonical_querystring + '\n'
canonical_request += canonical_headers + '\n'
canonical_request += signed_headers + '\n'
canonical_request += payload_hash
hashed_canonical_request = hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
# ************* TASK 2: CREATE THE STRING TO SIGN*************
ALGORITHM = 'AWS4-HMAC-SHA256'
credential_scope = f'{date_stamp}/{region}/{service}/aws4_request'
string_to_sign = f'{ALGORITHM}\n{amz_date}\n{credential_scope}\n{hashed_canonical_request}'
signing_key = getSignatureKey(secret_key, date_stamp, region, service)
signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest()
authorization_header = ALGORITHM + ' '
authorization_header += 'Credential=' + access_key_id + '/' + credential_scope + ', '
authorization_header += 'SignedHeaders=' + signed_headers + ', '
authorization_header += 'Signature=' + signature
headers = {'Content-Type':content_type,
'X-Amz-Date':amz_date,
'Authorization':authorization_header}
# ************* SEND THE REQUEST *************
print('\nBEGIN REQUEST++++++++++++++++++++++++++++++++++++')
print('Request URL = ' + endpoint)
r = requests.post(endpoint, data=payload, headers=headers)
#print(f'{endpoint}\n[{payload}]\n{headers}')
print('\nRESPONSE++++++++++++++++++++++++++++++++++++')
print('Response code: %d\n' % r.status_code)
print(r.text)
API Gatewayの quick create
- API作成のときにTargetを指定すれば、
$default
のルートと$default
のステージが生成される。 - 統合タイプ HTTP ProxyとLambda Proxyにのみ対応している。
when calling the CreateApi operation: Target only supports HTTP Proxy or Lambda Proxy
WebのマネジメントコンソールでHTTP統合を選択したときに作られるルートのRouteKeyの既定値はANY /
ANY /
と$default
- integrationUriが
https://.../path/xyz
のときGET /
すると- $defaultだと、
GET .../xyz/
のリクエストになる -
ANY /
だと、GET .../xyz
のリクエストになる
- $defaultだと、
- パススルーするなら
ANY /{proxy+}
か
CDK aws_stepfunctions_tasks.CallApiGatewayHttpApiEndpointが無効なPolicyを生成する
aws-cdk-lib@2.34.2
Step FunctionsのAPI Gateway統合で、IAM認証付きのHTTP APIを呼ぶとき
- TaskのAuthTypeは
IAM_ROLE
- apiPath、stageを指定せずにデフォルト値を使用する
認証タイプをIAM_ROLEにするならば、apiPathとstageを設定しないとexecute-api:Invoke
ポリシーのリソース指定でundefinedという文字列になってアクセス許可にならい
- 問題のあるPolicy.Resource
arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${apiId}/undefined/POSTunefined
- 期待する値 apiPath: "/", stageName: "$default"
arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${apiId}/$default/POST/
- これだと、Taskがapiを呼べない、not foundになってしまう
- issueあった (aws-stepfunctions-tasks): CallApiGatewayHttpApiEndpoint produces an incorrect IAM policy for dynamic api paths · Issue #17464 · aws/aws-cdk
とりあえずの対応をした
- ちゃんと対応するにはパラメーターマッピングとかちゃんと考えないとならない
- 今回は、apiPathは
/
、stageNameは$default
で動作すればいい
class MyCallApiGatewayHttpApiEndpoint extends tasks.CallApiGatewayHttpApiEndpoint {
protected readonly taskPolicies?: iam.PolicyStatement[] | undefined;
protected readonly arnForExecuteApi: string;
constructor(scope:Construct, id: string, props: CallApiGatewayHttpApiEndpointProps) {
super(scope, id, props);
const {apiStack, apiId, stageName, method, apiPath} = props;
this.arnForExecuteApi = apiStack.formatArn({
service: 'execute-api',
resource: apiId,
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
resourceName: `${stageName??"$default"}/${method}${apiPath??"/"}`
});
this.taskPolicies = super.createPolicyStatements();
}
}
CallApiGatewayHttpApiEndpointのコンストラクタでtaskPolicies
が設定される。継承したコンストラクタでarnForExecuteApi
を設定しなおしてcreatePolicyStatements
を再実行する。
参考