こんにちは。コーポレート本部 サイバーセキュリティ推進部の耿です。 当社は2024年1月に社名が「 電通国際情報サービス 」(ISID)から「 電通 総研」に変わりました。 当然、各種システムの変更も社名変更に合わせて行われました。 今回は AWS CDK を利用して構築したある社内向け Web アプリの ドメイン を切り替える際に、意外とサクッと終わった話をしたいと思います。 ドメイン の切り替えは緊張感のある作業ですが、結果的に本番環境での実作業時間はわずか30分で終わりました。 アプリのインフラ構成 切り替え手順 CDKによる切り替え作業: 切り替え前 CDKによる切り替え作業: 事前準備 CDKによる切り替え作業: 本番切り替え 所感 アプリのインフラ構成 今回 ドメイン 切り替えを行った社内向け Web アプリの大雑把なインフラ構成は次の図の通りです。関連する コンポーネント のみを抜き出しており、DB や WAF などは省略しています。また実際は ECS アプリケーションのブルーグリーンデプロイを行っているのですが、これも省略しています。 cool-app.isid.example ドメイン を利用し、ALB のターゲットグループとして ECS サービスを登録しています。 このWeb アプリの ドメイン を cool-app.dentsusoken.example に切り替えます。 切り替え手順 事前準備作業と、本番切り替え作業の2段階に分けて ドメイン 切り替えを実施しました。 事前準備作業として以下を行います。 新 ドメイン のホストゾーンの用意 リダイレクト用ALBの新規作成 新 ドメイン の ACM 証明書作成 新 ドメイン はこの段階ではリダイレクト用ALBを指しており、リダイレクト用ALBは旧 ドメイン に302リダイレクトします。 この時点でアプリケーションへの従来のアクセスには何も影響はありません。 続いて本番切り替え作業では、アプリケーション用ALBの ドメイン を新 ドメイン に変更し、旧 ドメイン はリダイレクト用ALBを指すように切り替えます。 リダイレクト用ALBも新 ドメイン に301リダイレクトするように変更します。 これによりブラウザによる旧 ドメイン へのアクセスは全て新 ドメイン にリダイレクトされるようになります。ところで ALBのURLリダイレクト機能 は元のパス、クエリパラメータを保持したままリダイレクトを実現できるので、ほぼユーザーが意識することなく ドメイン 切り替えを実現できるのは嬉しいです。 CDKによる切り替え作業: 切り替え前 ドメイン 切り替え前は、以下のサンプルコードのような構成でCDKを実装していました。 アプリケーション new MyAppStack ( app , "MyAppStack" , { hostedZoneId: "Z1111111OLDHOSTEDZONE" , hostedZoneName: "isid.example" , webAppRecordName: "cool-app" , } ); スタック(一部のプロパティのみ掲載) // アプリケーション用ALB const alb = new elb.ApplicationLoadBalancer ( this , "Alb" , { vpc , vpcSubnets: { subnetGroupName } , securityGroup , internetFacing: true , } ); // ターゲットグループ const targetGroup = new elb.ApplicationTargetGroup ( this , "TargetGroup" , { vpc , port: 3000 , protocol: elb.ApplicationProtocol.HTTP , targetType: elb.TargetType.IP , } ); // ホストゾーン const hostedZone = route53.PublicHostedZone.fromHostedZoneAttributes ( this , "HostedZone" , { hostedZoneId: props.hostedZoneId , zoneName: props.hostedZoneName , } ); // アプリケーション用ALBへのAレコード new route53.ARecord ( this , "WebARecord" , { zone: hostedZone , recordName: props.webAppRecordName , target: route53.RecordTarget.fromAlias (new route53Targets.LoadBalancerTarget ( alb )), } ); // ACM証明書 const certificate = new certificatemanager.Certificate ( this , "Certificate" , { domainName: ` ${ props.webAppRecordName } . ${ props.hostedZoneName } ` , validation: certificatemanager.CertificateValidation.fromDns ( hostedZone ), } ) // ALBリスナー alb.addListener ( "ApplicationListener" , { protocol: elb.ApplicationProtocol.HTTPS , port: 443 , certificates: [ certificate ] , sslPolicy: elb.SslPolicy.TLS13_RES , defaultTargetGroups: [ targetGroup ] , open: false , } ); CDKによる切り替え作業: 事前準備 事前準備段階として、リダイレクト用ALB、新 ドメイン のAレコード、 ACM 証明書などを新規に作成します。 CDKのコード上ではプロパティ名を変更し、新旧どちらの ドメイン を使用しているのか明確に分かるようにしました。 アプリケーション new MyAppStack ( app , "MyAppStack" , { // 旧ホストゾーンのプロパティ名を変更 oldHostedZoneId: "Z1111111OLDHOSTEDZONE" , oldHostedZoneName: "isid.example" , // 新ホストゾーンのプロパティを追加 newHostedZoneId: "Z2222222NEWHOSTEDZONE" , newHostedZoneName: "dentsusoken.example" , webAppRecordName: "cool-app" , } ); スタック // 【変更なし】アプリケーション用ALB const alb = new elb.ApplicationLoadBalancer ( this , "Alb" , { vpc , vpcSubnets: { subnetGroupName } , securityGroup , internetFacing: true , } ); // 【追加】リダイレクト用ALB // ドメイン切り替え前は新ドメインから旧ドメインへ、 // ドメイン切り替え後は旧ドメインから新ドメインへリダイレクトする const redirectAlb = new elb.ApplicationLoadBalancer ( this , "RedirectAlb" , { vpc , vpcSubnets: { subnetGroupName } , securityGroup , internetFacing: true , } ); // 【変更なし】ターゲットグループ const targetGroup = new elb.ApplicationTargetGroup ( this , "TargetGroup" , { vpc , port: 3000 , protocol: elb.ApplicationProtocol.HTTP , targetType: elb.TargetType.IP , } ); // 【変数名変更】旧ホストゾーン const oldHostedZone = route53.PublicHostedZone.fromHostedZoneAttributes ( this , "HostedZone" , { hostedZoneId: props.oldHostedZoneId , zoneName: props.oldHostedZoneName , } ); // 【追加】新ドメインのホストゾーン const newHostedZone = route53.PublicHostedZone.fromHostedZoneAttributes ( this , "NewHostedZone" , { hostedZoneId: props.newHostedZoneId , zoneName: props.newHostedZoneName , } ); // 【変数名変更】旧ドメインのAレコード new route53.ARecord ( this , "WebARecord" , { zone: oldHostedZone , recordName: props.webAppRecordName , target: route53.RecordTarget.fromAlias (new route53Targets.LoadBalancerTarget ( alb )), } ); // 【追加】新ドメインのAレコード new route53.ARecord ( this , "NewWebARecord" , { zone: newHostedZone , recordName: props.webAppRecordName , target: route53.RecordTarget.fromAlias (new route53Targets.LoadBalancerTarget ( redirectAlb )), } ); // 【変数名変更】旧ドメインのACM証明書 const oldCertificate = new certificatemanager.Certificate ( this , "Certificate" , { domainName: ` ${ props.webAppRecordName } . ${ props.oldHostedZoneName } ` , validation: certificatemanager.CertificateValidation.fromDns ( oldHostedZone ), } ) // 【追加】新ドメインのACM証明書 const newCertificate = new certificatemanager.Certificate ( this , "NewCertificate" , { domainName: ` ${ props.webAppRecordName } . ${ props.newHostedZoneName } ` , validation: certificatemanager.CertificateValidation.fromDns ( newHostedZone ), } ) // 【変数名変更】アプリケーション用ALBのリスナー alb.addListener ( "ApplicationListener" , { protocol: elb.ApplicationProtocol.HTTPS , port: 443 , certificates: [ oldCertificate ] , sslPolicy: elb.SslPolicy.TLS13_RES , defaultTargetGroups: [ targetGroup ] , open: false , } ); // 【追加】リダイレクト用ALBのリスナー redirectAlb.addListener ( "RedirectAlbListener" , { protocol: elb.ApplicationProtocol.HTTPS , port: 443 , certificates: [ newCertificate ] , sslPolicy: elb.SslPolicy.TLS13_RES , open: false , // ALB の機能で旧ドメインへ 302 リダイレクト defaultAction: elb.ListenerAction.redirect ( { host: ` ${ props.webAppRecordName } . ${ props.oldHostedZoneName } ` , permanent: false , } ), } ); CDKによる切り替え作業: 本番切り替え ドメイン 切り替え本番では、ホストゾーンのレコードの指す先と証明書がアタッチされるALBを変更し、リダイレクト用ALBのリダイレクト先 ドメイン も変更しました。 スタック // 【変更なし】アプリケーション用ALB const alb = new elb.ApplicationLoadBalancer ( this , "Alb" , { vpc , vpcSubnets: { subnetGroupName } , securityGroup , internetFacing: true , } ); // 【変更なし】リダイレクト用ALB const redirectAlb = new elb.ApplicationLoadBalancer ( this , "RedirectAlb" , { vpc , vpcSubnets: { subnetGroupName } , securityGroup , internetFacing: true , } ); // 【変更なし】ターゲットグループ const targetGroup = new elb.ApplicationTargetGroup ( this , "TargetGroup" , { vpc , port: 3000 , protocol: elb.ApplicationProtocol.HTTP , targetType: elb.TargetType.IP , } ); // 【変更なし】旧ホストゾーン const oldHostedZone = route53.PublicHostedZone.fromHostedZoneAttributes ( this , "HostedZone" , { hostedZoneId: props.oldHostedZoneId , zoneName: props.oldHostedZoneName , } ); // 【変更なし】新ドメインのホストゾーン const newHostedZone = route53.PublicHostedZone.fromHostedZoneAttributes ( this , "NewHostedZone" , { hostedZoneId: props.newHostedZoneId , zoneName: props.newHostedZoneName , } ); // 【変更】旧ドメインのAレコード new route53.ARecord ( this , "WebARecord" , { zone: oldHostedZone , recordName: props.webAppRecordName , // 旧ドメインはリダイレクト用ALBをターゲットに転送 target: route53.RecordTarget.fromAlias (new route53Targets.LoadBalancerTarget ( redirectAlb )), } ); // 【変更】新ドメインのAレコード new route53.ARecord ( this , "NewWebARecord" , { zone: newHostedZone , recordName: props.webAppRecordName , // 新ドメインはアプリケーション用ALBをターゲットに転送 target: route53.RecordTarget.fromAlias (new route53Targets.LoadBalancerTarget ( alb )), } ); // 【変更なし】旧ドメインのACM証明書 const oldCertificate = new certificatemanager.Certificate ( this , "Certificate" , { domainName: ` ${ props.webAppRecordName } . ${ props.oldHostedZoneName } ` , validation: certificatemanager.CertificateValidation.fromDns ( oldHostedZone ), } ) // 【変更なし】新ドメインのACM証明書 const newCertificate = new certificatemanager.Certificate ( this , "NewCertificate" , { domainName: ` ${ props.webAppRecordName } . ${ props.newHostedZoneName } ` , validation: certificatemanager.CertificateValidation.fromDns ( newHostedZone ), } ) // 【変更】アプリケーション用ALBのリスナー alb.addListener ( "ApplicationListener" , { protocol: elb.ApplicationProtocol.HTTPS , port: 443 , // 新ドメインの証明書に変更 certificates: [ newCertificate ] , sslPolicy: elb.SslPolicy.TLS13_RES , defaultTargetGroups: [ targetGroup ] , open: false , } ); // 【変更】リダイレクト用ALBのリスナー redirectAlb.addListener ( "RedirectAlbListener" , { protocol: elb.ApplicationProtocol.HTTPS , port: 443 , // 旧ドメインの証明書に変更 certificates: [ oldCertificate ] , sslPolicy: elb.SslPolicy.TLS13_RES , open: false , // ALB の機能で新ドメインへ 301 リダイレクト defaultAction: elb.ListenerAction.redirect ( { host: ` ${ props.webAppRecordName } . ${ props.newHostedZoneName } ` , permanent: true , } ), } ); 以上で難なく ドメイン 切り替え作業が完了しました。あとは DNS のキャッシュが切れたら新 ドメイン でアプリにアクセスできるようになります。 本番切り替え作業にかかった時間はCDKのデプロイから切り替え後の接続確認まで含めても30分ほどでした。(実際はもっと色々なイン フラリ ソースに関わる作業があったので、本記事に記載した構成だけならもっと早く終わっていたと思います) 所感 これまでの内容では省いていますが、各作業段階においてもちろんステージング環境での検証が重要です。 IaCでインフラを管理することのメリットは、ステージング環境で検証したことをその通りに本番環境にも適用できることです。 特にCDKでは旧 ドメイン が使われている場所を、コードエディタで簡単に洗い出し、変数として辿れるのが便利だと改めて思いました。 そもそも複数環境を構築するときに同じスタックを使い回す前提で実装することで、実装段階から環境に固有の変数( ドメイン 名が典型的な例ですね)を意識して切り出すような書き方になります。既にそのような書き方がされているCDKアプリにおいて、 ドメイン を変更する作業が簡単に終わったのも当然といえるかもしれません。 CDK最高です。 執筆: @kou.kinyo 、レビュー: @yamashita.tsuyoshi ( Shodo で執筆されました )