Lambda環境でできない処理をECSで実行する

(2019-10-28)

以前cdkbotというツールを出した。これはGitHubのPRからCDKのデプロイなどを実行できるツールでlambda上で動いていた。

PR上でCDKのレビューやデプロイを行うツールcdkbotを作った - sambaiz-net

npmやgitといった外部コマンドを実行するため、layerにバイナリを詰めて上げていた。

Lambda上でnpm installできるLayerを作った - sambaiz-net

CDKにはローカルのDockerfileをbuildしてECRに上げてくれる ecs.ContainerImage.fromAsset()という関数があって、これに対応させるためlayerにdockerを追加してみたのだがrootが取れず動かない。 rootなしで動くudockerでdind(docker in docker)のイメージを動かしたりもしてみたがbuildはできなかった。

この他にもCloudFrontなどリソースによっては作成に時間がかかり、Lambdaのタイムアウト上限に到達する問題もあったので、lambda+API Gatewayでwebhookのリクエストだけ受け取り、ECS(Fargate)でTaskを立ち上げて処理を行うことにした。

templateにECSまわりのものを追加したところ、Serverless Application Repository非対応ということで上げられなくなってしまった。

AWS SAMでLambdaの関数をデプロイしServerless Application Repositoryに公開する - sambaiz-net

Taskへのパラメータの受け渡し

runTaskcontainerOverridesでコマンドの引数としてlambdaに来たリクエストをそのまま渡そうとしたところ、8192文字文字の上限に当たってしまったので SQSで受け渡すようにした。

sess := session.New()
sqsSvc := sqs.New(sess)
if _, err := sqsSvc.SendMessage(&sqs.SendMessageInput{
    MessageBody:    aws.String(string(payload)),
    QueueUrl:       aws.String(os.Getenv("OPERATION_QUEUE_URL")),
    MessageGroupId: aws.String("group"),
}); err != nil {
    fmt.Println(err.Error())
    return response{
        StatusCode: http.StatusInternalServerError,
    }, err
}

同時実行数を制限する

アプリケーションの特性上、同時実行されないようにしたい。 そこで普段はTask0のServiceを作成し、実行するときは要求Taskを1にして、Queueが空になるまで実行させ、最後に0に戻すようにした。Taskがない場合は立ち上がりにやや時間がかかるが、元々Lambdaで動いていたこともあって常に料金が発生する常駐リソースをなるべく使いたくなかった。将来常駐オプションを追加するかもしれない。

sess := session.New()
sqsSvc := sqs.New(sess)
for {
    res, err := sqsSvc.ReceiveMessage(&sqs.ReceiveMessageInput{
        QueueUrl:            aws.String(os.Getenv("OPERATION_QUEUE_URL")),
        MaxNumberOfMessages: aws.Int64(1),
    })
    if err != nil {
        logger.Error("receive message error", zap.Error(err))
        break
    }

    if len(res.Messages) == 0 {
        break
    }
    ...
}

ecsSvc := ecs.New(sess)
if _, err := ecsSvc.UpdateService(&ecs.UpdateServiceInput{
    Cluster:      aws.String(os.Getenv("TASK_ECS_CLUSTER_ARN")),
    DesiredCount: aws.Int64(0),
    Service:      aws.String(os.Getenv("OPERATION_SERVICE_ARN")),
}); err != nil {
    logger.Error("shutdown task error", zap.Error(err))
}