LetsEncrypt, CDK 및 AWS Lambda를 사용하여 ECS Fargate 컨테이너에 직접 액세스(나쁜 생각일 수 있음)

소량의 코드만 있으면 실행 중인 Fargate 작업을 단일 하위 도메인의 인터넷에 직접 노출할 수 있다.예: ${taskid}.eu-west-2.browser.reflow.io.
이것은 거의 항상 나쁜 생각이다.그러나 만약 당신이 정말 이렇게 해야 한다면, 이 지침은 도움이 될 것이다.

너는 왜 이렇게 하지 말았어야 했니


AWS는 컨테이너화된 작업 부하를 조작하기 위해 다양한 전투 테스트를 거친 모델을 제공한다.이러한 모델은 더욱 쉽게 배치되고 파괴될 수 없으며 절대 다수의 고객 용례에 적용된다.예를 들면 다음과 같습니다.

  • Application Load Balanced Fargate Service: ECS 클러스터에서 실행되는 Fargate 서비스는 어플리케이션 로드 밸런서에 의해 제공됩니다.이점:
  • 은밀하게 운행 상황 검사를 지원하고 불건전한 실례를 다시 만들기 전에 유량을 불건전한 실례에서 옮긴다.
  • CodePipeline을 통해 배치할 때 위탁 관리 서비스는 점차적으로 데이터를 새로운 설정의 서비스로 옮기기 전에 이러한 서비스의 안정성을 감시한다.
  • 트래픽은 데이터 센터 복구 기능을 제공하기 위해 다양한 가용성 영역에 자동으로 분산됩니다.

  • Queue Processing Fargate Service: SQS 대기열의 작업을 처리하기 위해 자동으로 확장되는Fargate 서비스입니다.이점:
  • 장애 발생 시 자동으로 작업을 재시도할 수 있습니다.
  • 비동기식 워크로드 기반
  • 상향/하향 확장
  • 장기 업무 처리 가능.
  • 우리 왜 이러는지


    reflow에서 웹 브라우저를 실행하여 서버의 끝에서 끝까지의 테스트를 기록하고 실행합니다.테스트를 기록하기 위해서, 순식간에 서버를 가진 웹소켓을 만들어서 저장할 수 있어야 합니다.
    Fargate가 있는 ECS는 서버를 실행하는 위험과 조작 비용을 없애지만 Kubernetes가 도입하는 복잡성이 없기 때문에 좋은 선택이다.
    우리의 첫 번째 디자인은 Application Load Balanced Fargate Service 모델을 사용했지만 다음과 같은 문제에 부딪혔다.
  • 저희는 저희 서버에서 신뢰를 받지 못하는 고객 코드를 실행하기를 희망합니다. 이것은 고객 작업 부하를 서로 분리해야 할 뿐만 아니라 제로 권한 서버도 필요합니다.불행하게도 ALB 프런트엔드의 ECS 서비스를 사용하여 고객 수준의 물리적 격리를 수행하는 것은 매우 중요합니다.
  • 우리는 여러 구역의 체계 구조를 원하지만 각 구역에 하나의 실례와 하나의NAT 스위치의 따뜻한 비용을 유지하는 것은 자체적으로 시작한 초창기 회사에 매우 중요하다.사용하지 않는 경우, 그룹을 0으로 축소할 수 있는 방법이 없는 것 같습니다.
  • 우리는 같은 팀의 여러 고객이 브라우저 실례를 공유하여 공동 녹화 테스트를 할 수 있도록 하는 기능을 가지고 있다.그러나 이것은 우리의 클라우드 실례에서 작용하지 않는다. 왜냐하면 우리는 같은 팀의 여러 사용자가 같은 서버에 접근할 것이라고 보장할 수 없기 때문이다.
  • 다음과 같은 모드를 사용할 수 있습니다.
  • 사용하지 않을 때 0으로 축소된 클러스터.이것은 우리가 여러 지역으로 가기 위해 따뜻한 실례비를 지불할 필요가 없다는 것을 의미한다.
  • 모든 고객 워크로드는 물리적으로 분리되어 있으며 같은 팀의 여러 사용자가 순식간에 서버 상태를 공유할 수 있습니다.
  • 예상되는 고객 ID를 서버 프로세스의 환경 변수에 베이킹하여 인증 간소화
  • 각 가용 영역에서 NAT 게이트웨이를 실행할 필요가 없습니다.
  • 주요 부정적 영향:
  • DNS 전파 지연은 고객이 처음으로 기록 실례를 사용할 때 서버가 DNS를 사용할 수 있도록 약 1분을 기다려야 한다는 것을 의미한다.
  • 감시가 필요한 운동 부품이 더 많다.
  • 논리 구성 요소

  • 관련 도메인의 LetsEncrypt SSL 와일드카드 인증서.예: *.eu-west-2.browser.reflow.io
  • 위 인증서의 AWS Lambda 작업을 자동으로 업데이트하고 문제가 발생할 경우 알려 줍니다.매달 한 차례 운행할 계획이다.
  • 클러스터에 등록된 각 작업에 대해 DNS 레코드를 자동으로 생성 및 제거하는 AWS Lambda 작업
  • ECS 클러스터 및 작업 구성은 필요에 따라 서비스를 실행하고 공용 IP 주소를 자동으로 등록합니다.
  • 작업 호스트 이름을 등록하여 클라이언트에 보내는 추가 애플리케이션 로직AppSync/DynamoDB를 사용하여 이 정보를 웹 클라이언트로 전송합니다.서버가 안내할 때 클라이언트가 읽을 수 있는dynamodb 기록에 저장됩니다.마지막으로 서버의 환경 변수에 TeamId를 베이킹합니다. 이것은cognito 사용자 정의 속성으로 클라이언트 JWT에서 모든 웹에 서명을 요청해야 합니다.
  • 구성 요소[1],[2]와[3]는 완전히 통용되기 때문에 나는 여기서 그것들을 묘사할 것이다.구성된 후에는 대상 ECS 클러스터에서 작성된 모든 작업이 DNS를 통해 공개됩니다.[4][5]는 특정 영역이므로 다른 사람에게 유용하거나 관련될 수 없지만 문제가 있으면 언제든지 연락하십시오.

    LetsEncrypt SSL 인증서


    CDK


    우리는 CDK를 사용하여 우리의 모든 인프라를 관리한다.다음은 LetsEncrypt Lambda 함수를 관리하는 데 사용되는 구성입니다.
    다음과 같은 이점을 제공합니다.
  • 저희 증서를 보관하기 위한 S3통
  • 인증서 갱신 시 SNS 주제 알림
  • 모든 컨텐트를 실제로 업데이트하는 데 사용되는 Lambda 함수우리는 CDK 이외에 코드를 미리 컴파일할 수 있는 포장기ReamplifyLambdaFunction가 하나 있지만 이것도 포장기NodeJSFunction일 수 있다.
  • 언급된 내용은 다음과 같습니다.
  • 임무 실례의 DNS 기록의 위탁 관리 구역과 임무에 접두사를 추가하는 관련 구역domain을 놓을 것입니다.
  • 매개 변수workspace(예: dev/prod.이것은 AWS 계정에서 이 구조의 여러 실례를 제공할 수 있도록 합니다.
  • 갱신 알림을 보낼 이메일 주소입니다.
  • 모든 컨텐츠를 저장하는 경우region/account
  • import { Construct } from 'constructs';
    import { Duration, RemovalPolicy, StackProps, Tags } from 'aws-cdk-lib';
    import { BlockPublicAccess, Bucket, BucketEncryption, ObjectOwnership } from 'aws-cdk-lib/aws-s3';
    import { Topic } from 'aws-cdk-lib/aws-sns';
    import { EmailSubscription } from 'aws-cdk-lib/aws-sns-subscriptions';
    import { ReamplifyLambdaFunction } from './reamplifyLambdaFunction';
    import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
    import { IHostedZone } from 'aws-cdk-lib/aws-route53';
    import { Rule, Schedule } from 'aws-cdk-lib/aws-events';
    import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';
    
    interface CertbotProps {
      adminNotificationEmail: string;
      hostedZone: IHostedZone;
      domain: string;
      workspace: string;
      env: {
        region: string;
        account: string;
      };
    }
    
    export class Certbot extends Construct {
      public readonly certBucket: Bucket;
      constructor(scope: Construct, id: string, props: StackProps & CertbotProps) {
        super(scope, id);
        Tags.of(this).add('construct', 'Certbot');
        const certBucket = new Bucket(this, 'bucket', {
          bucketName: `certs.${props.env.region}.${props.workspace}.reflow.io`,
          objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED,
          removalPolicy: RemovalPolicy.DESTROY,
          autoDeleteObjects: true,
          versioned: true,
          lifecycleRules: [
            {
              enabled: true,
              abortIncompleteMultipartUploadAfter: Duration.days(1),
            },
          ],
          encryption: BucketEncryption.S3_MANAGED,
          enforceSSL: true,
          blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
        });
        this.certBucket = certBucket;
    
        const topic = new Topic(this, 'CertAdminTopic');
        topic.addSubscription(new EmailSubscription(props.adminNotificationEmail));
    
        const fn = new ReamplifyLambdaFunction(this, 'LambdaFn', {
          workspace: props.workspace,
          lambdaConfig: 'deploy/browserCerts.ts',
          timeout: Duration.minutes(15),
          environment: {
            NOTIFY_EMAIL: props.adminNotificationEmail,
            CERTIFICATES: JSON.stringify([
              {
                domains: [`*.${props.domain}`],
                zoneId: props.hostedZone.hostedZoneId,
                certStorageBucketName: certBucket.bucketName,
                certStoragePrefix: 'browser/',
                successSnsTopicArn: topic.topicArn,
                failureSnsTopicArn: topic.topicArn,
              },
            ]),
          },
        });
    
        fn.addToRolePolicy(
          new PolicyStatement({
            actions: ['route53:ListHostedZones'],
            resources: ['*'],
          })
        );
        fn.addToRolePolicy(
          new PolicyStatement({
            actions: ['route53:GetChange', 'route53:ChangeResourceRecordSets'],
            resources: ['arn:aws:route53:::change/*'].concat(props.hostedZone.hostedZoneArn),
          })
        );
        fn.addToRolePolicy(
          new PolicyStatement({
            actions: ['ssm:GetParameter', 'ssm:PutParameter'],
            resources: ['*'],
          })
        );
        certBucket.grantWrite(fn);
        topic.grantPublish(fn);
    
        new Rule(this, 'trigger', {
          schedule: Schedule.cron({ minute: '32', hour: '17', day: '3', month: '*', year: '*' }),
          targets: [new LambdaFunction(fn)],
        });
      }
    }
    

    AWS Lambda 함수


    종속성:
  • acme-client : 4.2.3
  • 이것은 논리적으로 분산되어 있는 모든 번거로운 작업을 수행하기 위해 acme-client에 의존합니다.
  • LetsEncrypt에서 새 계정을 만들 때마다 하나의 계정이 아닌 하나의 계정만 관리하도록 SSM 매개 변수를 유지하고 새 환경을 시작할 때 사전 의존 항목 없이 계정을 실행할 수 있도록 합니다.
  • DNS 레코드를 사용하여 Lets Encrypt 도전에 응답함으로써 지정된 도메인을 보유하고 있음을 증명합니다.
  • 결과 인증서를 S3에 저장합니다.
  • 관리자 인증서가 발급되었음을 알립니다(장애가 발생하면 관리자 인증서가 발급되지 않았음을 알립니다).
  • import AWS from 'aws-sdk';
    import acme from 'acme-client';
    
    const route53 = new AWS.Route53();
    const s3 = new AWS.S3();
    const sns = new AWS.SNS();
    
    export function assertEnv(key: string): string {
      if (process.env[key] !== undefined) {
        console.log('env', key, 'resolved by process.env as', process.env[key]!);
        return process.env[key]!;
      }
      throw new Error(`expected environment variable ${key}`);
    }
    
    export const assertEnvOrSSM = async (key: string, shouldThrow = true): Promise<string> => {
      const workspace = assertEnv('workspace');
    
      if (process.env[key] !== undefined) {
        console.log('env', key, 'resolved by process.env as', process.env[key]!);
        return Promise.resolve(process.env[key]!);
      } else {
        const SSMLocation = `/${workspace}/${key}`;
        console.log('env', key, 'resolving via SSM at', SSMLocation);
    
        const SSM = new AWS.SSM();
        try {
          const ssmResponse = await SSM.getParameter({
            Name: SSMLocation,
          }).promise();
          if (!ssmResponse.Parameter || !ssmResponse.Parameter.Value) {
            throw new Error(`env ${key} missing`);
          }
          console.log('env', key, 'resolved by SSM as', ssmResponse.Parameter.Value);
          process.env[key] = ssmResponse.Parameter.Value;
          return ssmResponse.Parameter.Value;
        } catch (e) {
          console.error(`SSM.getParameter({Name: ${SSMLocation}}):`, e);
          if (shouldThrow) {
            throw e;
          }
          return '';
        }
      }
    };
    
    export const writeSSM = async (key: string, value: string): Promise<void> => {
      const workspace = assertEnv('workspace');
    
      const SSMLocation = `/${workspace}/${key}`;
      console.log('env', key, 'writing to SSM at', SSMLocation, 'value', value);
    
      const SSM = new AWS.SSM();
      await SSM.putParameter({
        Name: SSMLocation,
        Value: value,
        Overwrite: true,
        DataType: 'text',
        Tier: 'Standard',
        Type: 'String',
      }).promise();
    };
    
    async function getOrCreateAccountPrivateKey() {
      let accountKey = await assertEnvOrSSM('LETSENCRYPT_ACCOUNT_KEY', false);
      if (accountKey) {
        return accountKey;
      }
      console.log('Generating Account Key');
      accountKey = (await acme.forge.createPrivateKey()).toString();
      await writeSSM('LETSENCRYPT_ACCOUNT_KEY', accountKey);
      return accountKey;
    }
    
    export const handler = async function (event) {
      const maintainerEmail = assertEnv('NOTIFY_EMAIL');
      const accountURL = await assertEnvOrSSM('LETSENCRYPT_ACCOUNT_URL', false);
      const certificates = JSON.parse(assertEnv('CERTIFICATES'));
      const accountPrivateKey = await getOrCreateAccountPrivateKey();
    
      acme.setLogger(console.log);
      const client = new acme.Client({
        directoryUrl: acme.directory.letsencrypt.production,
        accountKey: accountPrivateKey,
        accountUrl: accountURL ? accountURL : undefined,
      });
    
      const certificateRuns = certificates.map(async (certificate) => {
        const { domains, zoneId, certStorageBucketName, certStoragePrefix, successSnsTopicArn, failureSnsTopicArn } =
          certificate;
    
        try {
          const [certificateKey, certificateCsr] = await acme.forge.createCsr({
            commonName: domains[0],
            altNames: domains.slice(1),
          });
    
          const certificate = await client.auto({
            csr: certificateCsr,
            email: maintainerEmail,
            termsOfServiceAgreed: true,
            challengeCreateFn: async (authz, challenge, keyAuthorization) => {
              console.log(authz, challenge, keyAuthorization);
              const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
    
              if (challenge.type !== 'dns-01') {
                throw new Error('Only DNS-01 challenges are supported');
              }
              const changeReq = {
                ChangeBatch: {
                  Changes: [
                    {
                      Action: 'UPSERT',
                      ResourceRecordSet: {
                        Name: dnsRecord,
                        ResourceRecords: [
                          {
                            Value: '"' + keyAuthorization + '"',
                          },
                        ],
                        TTL: 60,
                        Type: 'TXT',
                      },
                    },
                  ],
                },
                HostedZoneId: zoneId,
              };
              console.log('Sending create request', JSON.stringify(changeReq));
              const response = await route53.changeResourceRecordSets(changeReq).promise();
              const changeId = response.ChangeInfo.Id;
              console.log(`Create request sent for ${dnsRecord} (Change id ${changeId}); waiting for it to complete`);
              const waitRequest = route53.waitFor('resourceRecordSetsChanged', { Id: changeId });
              const waitResponse = await waitRequest.promise();
              console.log(
                `Create request complete for ${dnsRecord}: (Change id ${waitResponse.ChangeInfo.Id}) ${waitResponse.ChangeInfo.Status}`
              );
            },
            challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
              const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
    
              const deleteReq = {
                ChangeBatch: {
                  Changes: [
                    {
                      Action: 'DELETE',
                      ResourceRecordSet: {
                        Name: dnsRecord,
                        ResourceRecords: [
                          {
                            Value: '"' + keyAuthorization + '"',
                          },
                        ],
                        TTL: 60,
                        Type: 'TXT',
                      },
                    },
                  ],
                },
                HostedZoneId: zoneId,
              };
              console.log('Sending delete request', JSON.stringify(deleteReq));
              const response = await route53.changeResourceRecordSets(deleteReq).promise();
              const changeId = response.ChangeInfo.Id;
              console.log(`Delete request sent for ${dnsRecord} (Change id ${changeId}); waiting for it to complete`);
              const waitRequest = route53.waitFor('resourceRecordSetsChanged', { Id: changeId });
              const waitResponse = await waitRequest.promise();
              console.log(
                `Delete request complete for ${dnsRecord}: (Change id ${waitResponse.ChangeInfo.Id}) ${waitResponse.ChangeInfo.Status}`
              );
            },
            challengePriority: ['dns-01'],
          });
    
          // Write private key & certificate to S3
          const certKeyWritingPromise = s3
            .putObject({
              Body: certificateKey.toString(),
              Bucket: certStorageBucketName,
              Key: certStoragePrefix + 'key.pem',
              ServerSideEncryption: 'AES256',
            })
            .promise();
          const certChainWritingPromise = s3
            .putObject({
              Body: certificate,
              Bucket: certStorageBucketName,
              Key: certStoragePrefix + 'cert.pem',
            })
            .promise();
    
          await Promise.all([certKeyWritingPromise, certChainWritingPromise]);
          console.log('Completed with certificate for ', domains);
    
          // after client.auto, an account should be available
          if (!accountURL) {
            await writeSSM('LETSENCRYPT_ACCOUNT_URL', client.getAccountUrl());
          }
    
          if (successSnsTopicArn) {
            await sns
              .publish({
                TopicArn: successSnsTopicArn,
                Message: `Certificate for ${JSON.stringify(domains)} issued`,
                Subject: 'Certificate Issue Success',
              })
              .promise();
          }
        } catch (err) {
          console.log('Error ', err);
          if (failureSnsTopicArn) {
            await sns
              .publish({
                TopicArn: failureSnsTopicArn,
                Message: `Certificate for ${JSON.stringify(domains)} issue failure\n${err}`,
                Subject: 'Certificate Issue Failure',
              })
              .promise();
          }
          throw err;
        }
      });
    
      await Promise.all(certificateRuns);
    };
    

    자동 DNS 로깅


    CDK


    이것은 다음과 같습니다.
  • AclusterArn 클러스터
  • 의 작업 상태 변경에 대한 ECS EventStream 이벤트 수집
  • serviceDiscoveryTLD(우리의 예에서는browser.${props.env.region}.reflow.io)를 DNS 기록
  • 의 접미사로 한다.
  • route 53은 기록을 만드는 구역을 불러옵니다
  • import { Rule } from 'aws-cdk-lib/aws-events';
    import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets';
    import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
    
    // ...
    
    const eventRule = new Rule(this, 'ECSChangeRule', {
       eventPattern: {
          source: ['aws.ecs'],
          detailType: ['ECS Task State Change'],
          detail: {
             clusterArn: [cluster.clusterArn],
          },
       },
    });
    
    const ecsChangeFn = new ReamplifyLambdaFunction(this, 'ECSStreamLambda', {
      ...props,
      lambdaConfig: 'stream/ecsChangeStream.ts',
      unreservedConcurrency: true,
      memorySize: 128,
      environment: {
        DOMAIN_PREFIX: props.serviceDiscoveryTLD,
        HOSTED_ZONE_ID: props.hostedZone.hostedZoneId,
      },
    });
    
    eventRule.addTarget(new LambdaFunction(ecsChangeFn));
    
    ecsChangeFn.addToRolePolicy(
            new PolicyStatement({
               actions: ['route53:GetChange', 'route53:ChangeResourceRecordSets', 'route53:ListResourceRecordSets'],
               resources: ['arn:aws:route53:::change/*'].concat(props.hostedZone.hostedZoneArn),
            })
    );
    ecsChangeFn.addToRolePolicy(
            new PolicyStatement({
               actions: ['ec2:DescribeNetworkInterfaces'],
               resources: ['*'],
            })
    );
    
    

    AWS Lambda


    이 기능은 다음과 같습니다.
  • 이벤트가 DNS 레코드
  • 에 영향을 미치는지 확인합니다.
  • 작업이 현재RUNNING 및 필요한 경우RUNNING:
  • 작업의 공용 IP를 찾습니다.
  • 작업 공용 IP에 A 레코드 삽입
  • 기타:
  • 작업과 관련된 ${taskId}.${DOMAIN_PREFIX} 기록을 삭제합니다.
  • import type { EventBridgeHandler } from 'aws-lambda';
    import AWS from 'aws-sdk';
    import { Task } from 'aws-sdk/clients/ecs';
    
    export function assertEnv(key: string): string {
      if (process.env[key] !== undefined) {
        console.log('env', key, 'resolved by process.env as', process.env[key]!);
        return process.env[key]!;
      }
      throw new Error(`expected environment variable ${key}`);
    }
    
    const ec2 = new AWS.EC2();
    const route53 = new AWS.Route53();
    const DOMAIN_PREFIX = assertEnv('DOMAIN_PREFIX');
    const HOSTED_ZONE_ID = assertEnv('HOSTED_ZONE_ID');
    
    export const handler: EventBridgeHandler<string, Task, unknown> = async (event) => {
      console.log('event', JSON.stringify(event));
      const task = event.detail;
      const clusterArn = task.clusterArn;
      const lastStatus = task.lastStatus;
      const desiredStatus = task.desiredStatus;
    
      if (!clusterArn) {
        return;
      }
    
      if (!lastStatus) {
        return;
      }
    
      if (!desiredStatus) {
        return;
      }
    
      const taskArn = task.taskArn;
      if (!taskArn) {
        return;
      }
      const taskId = taskArn.split('/').pop();
      if (!taskId) {
        return;
      }
    
      const clusterName = clusterArn.split(':cluster/')[1];
      if (!clusterName) {
        return;
      }
      const containerDomain = `${taskId}.${DOMAIN_PREFIX}`;
    
      if (lastStatus === 'RUNNING' && desiredStatus === 'RUNNING') {
        const eniId = getEniId(task);
        if (!eniId) {
          return;
        }
    
        const taskPublicIp = await fetchEniPublicIp(eniId);
        if (!taskPublicIp) {
          return;
        }
    
        const recordSet = createRecordSet(containerDomain, taskPublicIp);
    
        await updateDnsRecord(clusterName, HOSTED_ZONE_ID, recordSet);
    
        console.log(`DNS record update finished for ${taskId} (${taskPublicIp})`);
      } else {
        const recordSet = await route53
          .listResourceRecordSets({
            HostedZoneId: HOSTED_ZONE_ID,
            StartRecordName: containerDomain,
            StartRecordType: 'A',
          })
          .promise();
        console.log('listRecordSets', JSON.stringify(recordSet));
        const found = recordSet.ResourceRecordSets.find((record) => record.Name === containerDomain + '.');
        if (found && found.ResourceRecords?.[0].Value) {
          await route53
            .changeResourceRecordSets({
              HostedZoneId: HOSTED_ZONE_ID,
              ChangeBatch: {
                Changes: [
                  {
                    Action: 'DELETE',
                    ResourceRecordSet: {
                      Name: containerDomain,
                      Type: 'A',
                      ResourceRecords: [
                        {
                          Value: found.ResourceRecords[0].Value,
                        },
                      ],
                      TTL: found.TTL,
                    },
                  },
                ],
              },
            })
            .promise();
        }
      }
    };
    
    function getEniId(task): string | undefined {
      const eniAttachment = task.attachments.find(function (attachment) {
        return attachment.type === 'eni';
      });
      if (!eniAttachment) {
        return undefined;
      }
      const networkInterfaceIdDetail = eniAttachment.details.find((detail) => detail.name === 'networkInterfaceId');
      if (!networkInterfaceIdDetail) {
        return undefined;
      }
      return networkInterfaceIdDetail.value;
    }
    
    async function fetchEniPublicIp(eniId): Promise<string | undefined> {
      const data = await ec2
        .describeNetworkInterfaces({
          NetworkInterfaceIds: [eniId],
        })
        .promise();
      console.log(data);
    
      return data.NetworkInterfaces?.[0].PrivateIpAddresses?.[0].Association?.PublicIp;
    }
    
    function createRecordSet(domain, publicIp) {
      return {
        Action: 'UPSERT',
        ResourceRecordSet: {
          Name: domain,
          Type: 'A',
          TTL: 60,
          ResourceRecords: [
            {
              Value: publicIp,
            },
          ],
        },
      };
    }
    
    async function updateDnsRecord(clusterName, hostedZoneId, changeRecordSet) {
      let param = {
        ChangeBatch: {
          Comment: `Auto generated Record for ECS Fargate cluster ${clusterName}`,
          Changes: [changeRecordSet],
        },
        HostedZoneId: hostedZoneId,
      };
      await route53.changeResourceRecordSets(param).promise();
    }
    

    프로덕션에서 실행


    이것은 이미 생산에 들어간 지 두 달이 되었다. 비록 그것은 완벽하지는 않지만, 우리에게는 효과가 매우 좋다.
    우리가 걱정할 필요가 없는 일:
  • DNS 레코드가 누적될 수 있으므로 일부 오류로 인해 삭제할 수 없습니다.나중에 수천 개의 DNS 기록을 만들었는데, 우리는 이것이 문제가 아니라고 생각한다.
  • 라우팅 53이 DNS 변경 요청을 제한할까 봐 걱정됩니다.우리는 이미 이런 상황이 몇 차례 발생한 것을 보았지만, 우리의 람보는 자동으로 다시 시도하고 최종적으로 통과할 것이다.
  • 음수 슬라이스:
  • E2E 테스트에서 일부 결함을 보았습니다. 때때로 브라우저는 TTL 밖에서 기다리더라도 갱신하기 전에 새로운 DNS 기록을 사용하지 않습니다.우리는 반드시 자동으로 이 문제를 해결해야 한다.
  • 단일 ECS 작업을 관리할 때 서버 배열 논리가 훨씬 복잡합니다.
  • 좋은 웹페이지 즐겨찾기