Lambda (golang)でOpenSearchのドメインを定期的に名前解決し、ALBのターゲットを更新(プライベートサブネットのみ)
初めに
生産技術部で製品の検査工程を担当しているエンジニアです。工場のデータを見える化するために、工場で使わなくなったサーバを利用して、オンプレミス環境にThe Elastic Stack(Elasticsearch、Kibana、Logstash、Filebeat、Metricbeat)でサービスを構築しました。
しかし、本業である生産技術業務の傍らで、生産量増加に伴うサーバ負荷対応や故障対応といったサーバの管理を行う事は難しいため、AWSのマネージドサービスの利用を検討しています。AWSを利用する目的は管理面の課題解消だけでなく、AWSで提供される多様なサービスとの連携や、パブリッククラウドを利用する事による他事業部との連携を視野に入れて検討しています。
AWSで検討している構成
会社のネットワークからAWS Direct Connect(専用線接続サービス)を利用し、接続しています。セキュリティの要件から、会社からのアクセスに限定し、インターネットゲートウェイは配置せず、プライベートサブネットのみを利用します。
社内に配置したFilebeatからFargateで立ち上げたLogstashにデータを転送し、LogstashからAmazon OpenSearch Service(以下、OpenSearch)に接続します。ALBをOpenSearchに繋ぎ、会社からOpenSearch Dashboardを閲覧出来る様にします。
OpenSearchとALB接続における課題
プライベートサブネットのみでネットワークを構成するため、OpenSearchのエンドポイントはVPCに向けています。この設定では、OpenSearchのENI(Elastic Network Interface)がVPC内に配置され、VPC内のサービスからOpenSearchにアクセスする事が出来ます。OpenSearchのエンドポイントは、以下の様なドメインエンドポイントとしてサービスを公開します。
https://vpc-domain-name
現在(2022/3/27)時点で、ALBはドメインエンドポイントをターゲットにする事が出来ないため、OpenSearchのENIに割り当てられたプライベートIPアドレスをターゲットとする必要があります。しかし、OpenSearchのIPアドレスを利用する場合に、このような制約があります。
IP アドレスは変更する可能性があるため、ドメインのエンドポイントを定期的に解決して常に正しいデータノードにアクセスできるようにすることが重要です。DNS 解決間隔を 1 分間に設定することが推奨されます。
AWS Lambdaを利用した対策と方針
Lambdaを利用し、1分毎にドメインエンドポイントの名前解決を行い、ALBのターゲットグループの設定と異なる場合は、値を更新する仕組みを実装します。
1分毎の定期実行には、Amazon EventBridge(CloudWatch Events)を利用し、Lambdaに利用する言語は、自分が一番使いやすそうなgolangとします。ライブラリは以下の2つを利用します。
-
https://github.com/aws/aws-lambda-go
Lambdaを操作するために使用 -
https://github.com/aws/aws-sdk-go-v2
ALBを操作するために使用
Lambdaをプライベートサブネットのみで利用するためにAWS PrivateLinkを設定します。
Lambda Functionの実装
- 環境変数に設定したOpenSearchのドメインエンドポイント、AWS::ElasticLoadBalancingV2::LoadBalancerのAmazon Resource Name (以下、ARN)、AWS::ElasticLoadBalancingV2::TargetGroupのARNを取得
- OpenSearchのドメインエンドポイントから名前解決し、IPアドレスを取得
- aws-lambda-go、aws-sdk-go-v2のライブラリを使用するための初期設定
- LoadBalancerのARNからLoadBalancerを取得
- TargetGroupのARNからTargetGroupを取得
- TargetGroupにOpensearchのIPアドレスが登録されていなければ、新規にTargetを登録
- ALBのヘルスチェック結果を元に、不要になったTargetの登録を解除
func HandleLambdaEvent() {
// 1. 環境変数取得
domainEndpoint := os.Getenv("DomainEndpoint")
albId := os.Getenv("AlbId")
albTargetGroupId := os.Getenv("AlbTargetGroupId")
fmt.Printf("GET ENV DomainEndpoint: %s AlbId: %s AlbTargetGroupId: %s\n", domainEndpoint, albId, albTargetGroupId)
// 2. 名前解決
opensearchIpAddr := ResolveIpAddress(domainEndpoint)
fmt.Printf("GET OpensearchIpAddressddress: %s\n", opensearchIpAddr)
// 3. 初期設定
svc := Init()
// 4. 指定したLoadbalancerを取得
lb := GetSpecifiedLoadbalancer(svc, albId)
fmt.Printf("GET LoadbalancerName: %s LoadbalancerArn: %s\n", *lb.LoadBalancerName, *lb.LoadBalancerArn)
// 5. 指定したLoadbalancerのTargetGroupを取得
tg := GetSpecifiedTargetGroup(svc, lb, albTargetGroupId)
fmt.Printf("GET TargetGroupName: %s TargetGroupArn: %s\n", *tg.TargetGroupName, *tg.TargetGroupArn)
// 6. TargetGroupにTargetの登録
if !HasTarget(svc, tg, opensearchIpAddr) {
const httpsPort = 443
RegisterSpecifiedTarget(svc, tg, opensearchIpAddr, httpsPort)
fmt.Println("REGISTER")
}
// 7. UnheltyなTargetの登録解除
DeregisterUnheltyTargets(svc, tg)
}
Cloudformationは、下記のパラメータを設定しています。S3Bucketを指定して実行ファイルを取得します。ドメインエンドポイント、ALBのARN、TargetGroupのARNは環境変数として設定します。IAMロールのポリシーには、VPC内でLambdaを実行するためのAWSLambdaVPCAccessExecutionRole
、S3にアクセスする為のAmazonS3ReadOnlyAccess
、LoadBalancerに関連するアクセスポリシーを追加しています。
Function:
Type: AWS::Lambda::Function
Properties:
Handler: populate-alb-tg-with-opensearch
Role: !GetAtt LambdaRole.Arn
Code:
S3Bucket: lambda-XXXXXXXXXXXXXXXXXXX-artifact
S3Key: populate-alb-tg-with-opensearch.zip
Runtime: go1.x
Timeout: 5
TracingConfig:
Mode: Active
VpcConfig:
SecurityGroupIds:
- Ref: SecurityGroupId
SubnetIds: !Split [",", !Ref SubnetIds]
Environment:
Variables:
DomainEndpoint: !Ref DomainEndpoint
AlbId: !Ref AlbId
AlbTargetGroupId: !Ref AlbTargetGroupId
LambdaRole:
Properties:
AssumeRolePolicyDocument:
Statement:
- Action:
- sts:AssumeRole
Condition: {}
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Version: 2012-10-17
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
- arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
Policies:
- PolicyName: FIoTElasticLoadBalancingAccessPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "elasticloadbalancing:DescribeLoadBalancers"
- "elasticloadbalancing:DescribeTargetHealth"
- "elasticloadbalancing:DescribeTargetGroups"
- "elasticloadbalancing:RegisterTargets"
- "elasticloadbalancing:DeregisterTargets"
Resource: "*"
Tags:
- Key: com.trim.f-iot
Value: lambda
Type: AWS::IAM::Role
EventBridgeの設定
参考実装:https://github.com/TomoyukiSugiyama/ElasticStack/blob/main/provisioning/build/cfn/lambda.yaml
イベントのルールは、cron形式で記述し、1分毎に実行するよう指定します。AWS::Lambda::PermissionにEventBridgeからの関数呼び出しを許可します。
EventRule:
Type: AWS::Events::Rule
Properties:
Description: pupulate atb target with opensearch
Name: populate-alb-tg-with-opensearch-rule
ScheduleExpression: cron(* * * * ? *)
State: ENABLED
Targets:
- Arn: !GetAtt Function.Arn
Id: lambda
EventPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref Function
Principal: events.amazonaws.com
SourceAccount: !Ref AWS::AccountId
SourceArn: !GetAtt EventRule.Arn
PrivateLinkの設定
参考実装:https://github.com/TomoyukiSugiyama/ElasticStack/blob/main/provisioning/build/cfn/vpc-endpoint.yaml
以下のPrivateLinkを設定します。
- LambdaからS3にアクセスするためのエンドポイント(Gateway型のエンドポイントを設定していますが、インターフェース型のエンドポイントも設定可能です。)
- LambdaのLogを取得するためのエンドポイント
- LambdaからALBにアクセスするためのエンドポイント
S3Endpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
VpcEndpointType: Gateway
VpcId: !Ref VpcId
RouteTableIds:
- !Ref PrivateRouteTableId
LogsEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Sub "com.amazonaws.${AWS::Region}.logs"
VpcEndpointType: Interface
PrivateDnsEnabled: true
VpcId: !Ref VpcId
SubnetIds: !Split [",", !Ref SubnetIds]
SecurityGroupIds:
- !Ref SecurityGroupId
ElasticLoadBalancingEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
ServiceName: !Sub "com.amazonaws.${AWS::Region}.elasticloadbalancing"
VpcEndpointType: Interface
PrivateDnsEnabled: true
VpcId: !Ref VpcId
SubnetIds: !Split [",", !Ref SubnetIds]
SecurityGroupIds:
- !Ref SecurityGroupId
動作確認
OpenSearchのサブネットを編集し、IPアドレスを強制的に変更します。
新しいルートが追加されると、Lambdaで検知してALBのTargetを追加します。
OpenSearchが古いルートを削除すると、古いIPアドレス宛のヘルスチェックは、Unhealtyとなります。
LambdaでUnhealtyなTargetを検知して、Targetの登録を解除します。
TargetGroupのメトリクスを確認すると、想定通りの動きになっていることがわかります。
ログで確認すると、REGISTER
、DEREGISTER
が一回ずつ実行されていることがわかります。
START RequestId: f100a28d-367f-4d89-ad45-ebf6b36867ad Version: $LATEST
GET OpensearchIpAddressddress: 172.31.35.214
GET LoadbalancerName: f-iot-alb LoadbalancerArn: arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:loadbalancer/app/f-iot-alb/a7e46ad189fb1a72
GET TargetGroupName: f-iot-alb-tg TargetGroupArn: arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/f-iot-alb-tg/4ab4dba084c2bb5c
REGISTER
END RequestId: f100a28d-367f-4d89-ad45-ebf6b36867ad
:
:
START RequestId: f55b9976-fd87-4e26-acfa-e75ae4ed4d76 Version: $LATEST
GET OpensearchIpAddressddress: 172.31.35.214
GET LoadbalancerName: f-iot-alb LoadbalancerArn: arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:loadbalancer/app/f-iot-alb/a7e46ad189fb1a72
GET TargetGroupName: f-iot-alb-tg TargetGroupArn: arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/f-iot-alb-tg/4ab4dba084c2bb5c
DEREGISTER UnhealtyTarget.Id: 172.31.19.18
END RequestId: f55b9976-fd87-4e26-acfa-e75ae4ed4d76
最後に
AWSは始めたばかりの初心者で、ハードウェア系の職場のため、一人で試行錯誤しながらAWSを使っている状況です。Lambdaも初めて利用しましたが、なんとか想定通りに動かすことができました。不適切な部分がありましたら、コメントでご指摘お願いします。
インフラの構築自体が素人のため、いろんなところで苦戦しています。今後は、初めて使うAWSサービス(AWS DirectConnect、Fargate)で苦労したことなどを発信していければと思います。今後ともよろしくお願いします。
ご参考
Discussion