1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Lambda(Node.js) で ElastiCache for Memcached を使用する

Posted at

elasticache.png

AWS CDK

VPCを作成し、おなじサブネットにElastiCacheとLambdaを配置します。ElastiCacheの設定エンドポイントをLambdaの環境変数に設定します。
cacheNodeTypeは下記のリンク先から適切なノードを選択してください。

VPC LambdaからElastiCacheにアクセスするために、セキュリティグループのインバウンドルールを作成する必要があります。

lib/stack/sample-stack.ts
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elasticache from 'aws-cdk-lib/aws-elasticache';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaNodeJs from 'aws-cdk-lib/aws-lambda-nodejs';

export class SampleStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        // VPC
        const vpc = this.createVpc();
        const securityGroup = this.createVpcSecurityGroup(vpc);

        // ElastiCache
        const memcache = this.createElasticache(vpc, securityGroup);
        const memcachedConfigEndpoint = `${memcache.attrConfigurationEndpointAddress}:${memcache.attrConfigurationEndpointPort}`;

        // VPC Lambda
        this.createFunc(vpc, memcachedConfigEndpoint);
    }

    /**
     * @description VPCを作成する
     * {@link Vpc | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html}
     */
    private createVpc(): ec2.IVpc {
        return new ec2.Vpc(this, 'Vpc', {
            ipAddresses: ec2.IpAddresses.cidr('192.168.0.0/16'),
            subnetConfiguration: [
                {
                    name: `vpc-public`,
                    cidrMask: 24,
                    subnetType: ec2.SubnetType.PUBLIC,
                },
                {
                    cidrMask: 24,
                    name: `vpc-private`,
                    subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
                },
            ],
        });
    }

    /**
     * {@link SecurityGroup | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.SecurityGroup.html}
     */
    private createVpcSecurityGroup(vpc: ec2.IVpc): ec2.SecurityGroup {
        return new ec2.SecurityGroup(this, 'VpcSecurityGroup', { vpc });
    }

    /**
     * @description ElastiCache for Memcachedを作成する
     * {@link Amazon ElastiCache for Memcached | https://docs.aws.amazon.com/ja_jp/AmazonElastiCache/latest/mem-ug/WhatIs.html}
     * {@link CfnSubnetGroup | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticache.CfnSubnetGroup.html}
     * {@link CfnCacheCluster | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticache.CfnCacheCluster.html}
     */
    private createElasticache(vpc: ec2.IVpc, securityGroup: ec2.SecurityGroup): elasticache.CfnCacheCluster {
        const subnetGroup = new elasticache.CfnSubnetGroup(this, 'SubnetGroup', {
            description: 'private subnet',
            subnetIds: vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_ISOLATED }).subnetIds,
        });

        const cluster = new elasticache.CfnCacheCluster(this, 'ElastiCache', {
            engine: 'memcached',
            cacheNodeType: 'cache.r6g.large',
            numCacheNodes: 1,
            cacheSubnetGroupName: subnetGroup.ref,
            vpcSecurityGroupIds: [securityGroup.securityGroupId], // vpcSecurityGroupIds,cacheSecurityGroupNamesのいずれかが必須
        });

        // インバウンドルールの追加
        const port = ec2.Port.tcp(cluster.port ?? 11211);
        securityGroup.addIngressRule(ec2.Peer.anyIpv4(), port);

        return cluster;
    }

    /**
     * @description Lambdaの作成
     * {@link NodejsFunction | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html}
     */
    private createFunc(vpc: ec2.IVpc, memcachedConfigEndpoint: string): lambdaNodeJs.NodejsFunction {
        return new lambdaNodeJs.NodejsFunction(this, 'SampleFunc', {
            entry: 'src/lambda/sample/index.ts',
            handler: 'handler',
            runtime: lambda.Runtime.NODEJS_18_X,
            timeout: cdk.Duration.minutes(5),
            environment: {
                MEMCACHED_CONFIG_ENDPOINT: memcachedConfigEndpoint,
            },
            vpc,
            vpcSubnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_ISOLATED }),
        });
    }
}

Lambda

ここでは、memcachedのクライアントに「Memcache Plus」というライブラリを使用します。
このライブラリはElastiCacheの設定エンドポイントからエンドポイントを取得するautodiscoverという機能が備わっています。

% npm install memcache-plus aws-lambda
% npm install --save-dev @types/aws-lambda esbuild@0
src/lambda/sample/index.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import MemcachePlus = require('memcache-plus');

export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
    const configEndpoint: string = process.env.MEMCACHED_CONFIG_ENDPOINT ?? '';

    const client = new MemcachePlus({
        hosts: [configEndpoint],
        autodiscover: true,
    });
    const ttl = 86400 * 30; // キャッシュ期間。単位秒。

    const key = 'key';
    const input = Math.floor(Math.random() * 1000);

    // データをキャッシュする。keyは250バイトまで。
    await client.set(key, input, ttl);

    // キャッシュしたデータを取得する。存在しない場合はnullを返却する。
    const output = await client.get(key);

    if (input === output) {
        console.debug(`memcachedから取得した値: ${output}`);
        return {
            statusCode: 200,
            body: output,
        };
    } else {
        console.error(`期待値: ${input}, 実際: ${output}`);
        return {
            statusCode: 500,
            body: `期待値: ${input}, 実際: ${output}`
        };
    }
}
types/memcache-plus.d.ts
declare module 'memcache-plus' {
    interface MemcacheOptions {
        autodiscover?: boolean;
        backoffLimit?: number;
        bufferBeforeError?: number;
        disabled?: boolean;
        hosts?: string | string[];
        maxValueSize?: number;
        queue?: boolean;
        netTimeout?: number;
        reconnect?: boolean;
    }

    class MemcacheClient {
        constructor(param: string | string[] | MemcacheOptions);
        get(key: string): Promise<any>;
        set(key: string, value: any, lifetime?: number): Promise<any>;
        // 必要に応じて他のメソッドの型定義を追加
    }

    export = MemcacheClient;
}

テスト

test/sample.test.ts
test/sample.test.ts
import * as cdk from 'aws-cdk-lib';
import { Match, Template } from 'aws-cdk-lib/assertions';
import { SampleStack } from '../lib/sample-stack';

test('SecurityGroup', () => {
    const app = new cdk.App();
    const stack = new SampleStack(app, 'SampleStack');
    const template = Template.fromStack(stack);

    // Security Group
    template.hasResourceProperties('AWS::EC2::SecurityGroup', {
        VpcId: Match.anyValue(),
        SecurityGroupIngress: [{
            "CidrIp": "0.0.0.0/0",
            "Description": "from 0.0.0.0/0:11211",
            "FromPort": 11211,
            "IpProtocol": "tcp",
            "ToPort": 11211
        }]
    });
});

test('ElastiCache', () => {
    const app = new cdk.App();
    const stack = new SampleStack(app, 'SampleStack');
    const template = Template.fromStack(stack);

    // Subnet Group
    template.resourceCountIs('AWS::ElastiCache::SubnetGroup', 1);
    template.hasResourceProperties('AWS::ElastiCache::SubnetGroup', {
        SubnetIds: Match.anyValue(),
        Description: 'private subnet',
    });

    // Cache Cluster
    template.resourceCountIs('AWS::ElastiCache::CacheCluster', 1);
    template.hasResourceProperties('AWS::ElastiCache::CacheCluster', {
        Engine: 'memcached',
        CacheNodeType: 'cache.r6g.large',
        NumCacheNodes: 1,
        CacheSubnetGroupName: Match.anyValue(),
        VpcSecurityGroupIds: Match.anyValue(),
    });
});

test('Lambda', () => {
    const app = new cdk.App();
    const stack = new SampleStack(app, 'SampleStack');
    const template = Template.fromStack(stack);

    // Lambda
    template.resourceCountIs('AWS::Lambda::Function', 1);
    template.hasResourceProperties('AWS::Lambda::Function', {
        Runtime: 'nodejs18.x',
        Timeout: 300,
        VpcConfig: Match.anyValue(),
    });
});

エラー

Error: spawnSync docker ENOENT

esbuildをインストールすることで解消します。

$ npm install --save-dev esbuild@0

error TS2688: Cannot find type definition file for 'babel__generator'.

node_modulesを削除してnpm installすると解消すると解消することがあります。

Autodiscovery failed. Error: connect ETIMEDOUT 192.168.3.236:11211

セキュリティグループにインバウンドルールが追加されていない場合にこのエラーが起こることがあります。
TCPのインバウンド・アウトバウンド双方にトラフィックの許可が必要です。アウトバウンドルールはデフォルトで全て許可されるので、インバウンドルールを設定します。
手動で設定する場合は下記のように設定します。

スクリーンショット 2023-08-11 13.03.13.png

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?