code-server と ALB で AWS Cloud9 代替の研修用 IDE を提供する – 実装編 2 ALB

こんにちは、広野です。

AWS Cloud9 は研修用途では非常に使い勝手が良かったのですが、AWS が新規アカウントへの提供を終了してしまいました。今回は私が試みた代替ソリューションの実装編です。本記事は Application Load Balancer とその周辺設定を対象にします。デプロイ先の VPC が完成していることが前提になります。

 

アーキテクチャ

アーキテクチャ概要編で、以下の図を紹介しておりました。

  • code-server サーバを配置する VPC です。何の変哲もない一般的なサブネット構成にしています。
  • NAT Gateway は課金節約のため、1 つのパブリックサブネットにしか配置していません。
  • code-server サーバは 1 ユーザーあたり 1 台を割り当てます。図では 1 つしかありませんが、人数分作成される想定です。ALB はユーザー全体で共用します。
  • code-server のログインパスワードはインストール時に設定しますが、AWS Secrets Manager で生成したものを使用します。
  • ALB には HTTPS アクセスさせるため、図には書いていませんが独自ドメインを使用します。そのための SSL 証明書はそのリージョンの AWS Certificate Manager で作成されていることが前提になります。
  • ALB から EC2 インスタンスへの通信は 80 番ポート (HTTP) を使用します。
  • ALB はインターネットに公開されますが、研修用途を想定して会社のソース IP アドレスからのみアクセスできるようにセキュリティグループを設定しています。

 

使用する VPC

新たに VPC を作成する場合

前回記事を参考に、作成してください。

 

既存 VPC を使用する場合

以下の条件であれば、既存 VPC を使用することが可能です。

  • パブリックサブネットが 2 つある。(ALB 用)
  • プライベートサブネットからインターネットにアクセスできる。(NAT Gateway がある) ソフトウェアのインストールや、AWS Systems Manager, AWS Secrets Manager のエンドポイントへのアクセスに使用する。

 

AWS CloudFormation テンプレートの構成

テンプレートは 3 つに分けています。

  • VPC
  • Application Load Balancer ← 今回はここ
  • Amazon EC2 インスタンス

このうち、VPC と ALB はユーザー共用になるので 1 回のみ実行します。Amazon EC2 インスタンスはユーザーごとに実行が必要です。

作成する ALB は本体 1 つとリスナーのみ作成します。ターゲットグループやリスナールールはぶらさがる Amazon EC2 インスタンスごとに作成しますので、続編記事で紹介します。

ALB にはターゲットグループからの HTTP レスポンスヘッダーをオーバーライドする機能があります。今回の構成ではそれを使用して若干セキュリティを高めています。

Amazon EC2 インスタンスの共通設定となる起動テンプレートとセキュリティグループもここで共通設定として 1 回だけ作成します。

テンプレートの範囲を図にすると、以下のようになります。ここに書かれているリソースが作成されます。

 

AWS CloudFormation テンプレート

AWS CloudFormation テンプレートです。AWS Secrets Manager は、続編記事のテンプレートに入ります。

Amazon EC2 の起動テンプレートは、インスタンスの共通設定としてボリュームサイズを可変にしています。とりあえず 10 GB あれば余裕はありますが、インストールするモジュール等により変更は必要です。インスタンスタイプは t3.micro, t3.small からの 2 択にしていますが、必要に応じて変更してください。

その他細かいことはインラインのコメントで補足します。

AWSTemplateFormatVersion: "2010-09-09"
Description: The CloudFormation template that creates a S3 bucket for logging, an EC2 Security Group, a Launch template and an ALB for common code-server configurations.

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  SystemName:
    Type: String
    Description: System name. use lower case only. (e.g. example)
    Default: example
    MaxLength: 10
    MinLength: 1

  SubName:
    Type: String
    Description: System sub name. use lower case only. (e.g. prod or dev)
    Default: dev
    MaxLength: 10
    MinLength: 1

  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: Choose a existing VPC ID you deploy the EC2 instance in.

  AlbSubnet1:
    Type: AWS::EC2::Subnet::Id
    Description: Choose an existing Public Subnet ID you deploy the Application Load Balancer in.

  AlbSubnet2:
    Type: AWS::EC2::Subnet::Id
    Description: Choose an existing Public Subnet ID you deploy the Application Load Balancer in.

  InstanceSubnet:
    Type: AWS::EC2::Subnet::Id
    Description: Choose an existing Private Subnet ID you deploy the EC2 instance in.

  LogRetentionDays:
    Type: Number
    Description: The retention period (days) for entire log data. Enter an integer between 35 to 540.
    Default: 400
    MaxValue: 540
    MinValue: 35

  InstanceType:
    Type: String
    Description: EC2 instance type for code-server.
    Default: t3.small
    AllowedValues:
      - t3.micro
      - t3.small

  InstanceVolumeSize:
    Type: Number
    Description: The volume size in GB
    Default: 10

  # ALB を HTTPS アクセス可能とするため、独自ドメインを使用します。
  DomainName:
    Type: String
    Description: Domain name for URL. xxxxx.xxx
    Default: example.com
    MaxLength: 40
    MinLength: 5

  # ドメインの SSL 証明書を ACM で登録していることが前提です。証明書の ARN を記入します。
  CertificateArn:
    Type: String
    Description: ACM certificate ARN.
    Default: arn:aws:acm:ap-northeast-1:xxxxxxxxxxxx:certificate/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    MaxLength: 128
    MinLength: 10

  # ALB へのアクセスを許可するソース IPv4 サブネットを指定します。
  AllowedSubnet:
    Type: String
    Description: Allowed source IPv4 subnet and subnet mask. (e.g. xxx.xxx.xxx.xxx/32)
    Default: xxx.xxx.xxx.xxx/32
    MaxLength: 18
    MinLength: 9

Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
  S3BucketLogs:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${SystemName}-${SubName}-code-server-logs
      LifecycleConfiguration:
        Rules:
          - Id: AutoDelete
            Status: Enabled
            ExpirationInDays: !Ref LogRetentionDays
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}

  # for ap-northeast-1 region
  S3BucketPolicyLogs:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3BucketLogs
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              # 東京リージョン以外で使用するときは、以下のアカウント番号を変更する必要があります。AWS 指定のものです。
              AWS: "arn:aws:iam::582318560864:root"
            Action: s3:PutObject
            Resource: !Sub "${S3BucketLogs.Arn}/*"
    DependsOn:
      - S3BucketLogs

# ------------------------------------------------------------#
# EC2 Launch template
# ------------------------------------------------------------#
  EC2LaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: !Sub code-server-${SystemName}-${SubName}
      LaunchTemplateData:
        InstanceType: !Ref InstanceType
        # AMI は Amazon Linux 2023 の最新版を使用するようになっています。
        ImageId: >-
          {{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}
        BlockDeviceMappings:
          - Ebs:
              VolumeSize: !Ref InstanceVolumeSize
              VolumeType: gp3
              DeleteOnTermination: true
              Encrypted: true
            DeviceName: /dev/xvda
        NetworkInterfaces:
          - SubnetId: !Ref InstanceSubnet
            Groups:
              - !Ref Ec2SecurityGroup
            DeviceIndex: 0
            AssociatePublicIpAddress: false
        MetadataOptions:
          HttpTokens: required
        Monitoring:
          Enabled: true
        TagSpecifications:
          - ResourceType: volume
            Tags:
              - Key: Cost
                Value: !Sub ${SystemName}-${SubName}
    DependsOn:
      - Ec2SecurityGroup

# ------------------------------------------------------------#
# EC2 Security Group
# ------------------------------------------------------------#
  Ec2SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VpcId
      GroupDescription: Allow code-server access via ALB only
      SecurityGroupIngress:
        - Description: Allow HTTP from ALB
          FromPort: 80
          IpProtocol: tcp
          ToPort: 80
          SourceSecurityGroupId: !GetAtt AlbSecurityGroup.GroupId
      SecurityGroupEgress:
        - Description: Allow all outbound traffic
          IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}
        - Key: Name
          Value: !Sub SG-code-server-${SystemName}-${SubName}
    DependsOn:
      - AlbSecurityGroup

# ------------------------------------------------------------#
# ALB Security Group
# ------------------------------------------------------------#
  AlbSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VpcId
      GroupDescription: Allow access via HTTPS.
      SecurityGroupIngress:
        - CidrIp: !Ref AllowedSubnet
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}
        - Key: Name
          Value: !Sub SG-alb-${SystemName}-${SubName}

# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------#
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub alb-code-server-${SystemName}-${SubName}
      Type: application
      IpAddressType: ipv4
      LoadBalancerAttributes:
        - Key: deletion_protection.enabled
          Value: false
        - Key: access_logs.s3.enabled
          Value: true
        - Key: access_logs.s3.bucket
          Value: !Ref S3BucketLogs
        - Key: access_logs.s3.prefix
          Value: albAccessLogs
        - Key: idle_timeout.timeout_seconds
          Value: 60
        - Key: routing.http.desync_mitigation_mode
          Value: defensive
        - Key: routing.http.drop_invalid_header_fields.enabled
          Value: true
        - Key: routing.http.preserve_host_header.enabled
          Value: false
        - Key: routing.http.x_amzn_tls_version_and_cipher_suite.enabled
          Value: false
        - Key: routing.http.xff_client_port.enabled
          Value: false
        - Key: routing.http.xff_header_processing.mode
          Value: append
        - Key: routing.http2.enabled
          Value: true
        - Key: waf.fail_open.enabled
          Value: false
      Scheme: internet-facing
      SecurityGroups:
        - !GetAtt AlbSecurityGroup.GroupId
      Subnets:
        - !Ref AlbSubnet1
        - !Ref AlbSubnet2
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}
        - Key: Name
          Value: !Sub alb-code-server-${SystemName}-${SubName}
    DependsOn:
      - AlbSecurityGroup
      - S3BucketLogs

  # HTTPS listener for routing
  AlbListenerHttps:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref ApplicationLoadBalancer
      Port: 443
      Protocol: HTTPS
      SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
      Certificates:
        - CertificateArn: !Ref CertificateArn
      DefaultActions:
        - Type: fixed-response
          FixedResponseConfig:
            StatusCode: 404
            ContentType: text/plain
            MessageBody: Not Found
      ListenerAttributes:
        # frame タグは SAMEORIGIN でないと許可しないようにしています。DENY だと code-server でエラーが発生します。
        - Key: routing.http.response.x_frame_options.header_value
          Value: SAMEORIGIN
        - Key: routing.http.response.server.enabled
          Value: false
        - Key: routing.http.response.strict_transport_security.header_value
          Value: max-age=31536000; includeSubdomains; preload;
        - Key: routing.http.response.x_content_type_options.header_value
          Value: nosniff
        # CSP は code-server の動作に抵触しないよう調整しています。もしエラーが発生したら都度調整が必要です。
        - Key: routing.http.response.content_security_policy.header_value
          Value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; script-src-elem 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https://open-vsx.org https://marketplace.visualstudio.com; worker-src 'self' blob:; font-src 'self' data:; frame-src 'self';"
    DependsOn:
      - ApplicationLoadBalancer

# ------------------------------------------------------------#
# Route 53
# ------------------------------------------------------------#
  Route53RecordA:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName: !Sub ${DomainName}.
      Name: !Sub code-server-${SystemName}-${SubName}.${DomainName}.
      Type: A
      AliasTarget:
        HostedZoneId: !GetAtt ApplicationLoadBalancer.CanonicalHostedZoneID
        DNSName: !GetAtt ApplicationLoadBalancer.DNSName
    DependsOn: ApplicationLoadBalancer

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
# Ec2
  Ec2LaunchTemplateId:
    Value: !Ref EC2LaunchTemplate
    Export:
      Name: !Sub ec2LtId-code-server-${SystemName}-${SubName}
  Ec2LaunchTemplateVersion:
    Value: !GetAtt EC2LaunchTemplate.LatestVersionNumber
    Export:
      Name: !Sub ec2LtVersion-code-server-${SystemName}-${SubName}
# S3
  S3BucketLogsName:
    Value: !Ref S3BucketLogs
    Export:
      Name: !Sub S3BucketLogsName-code-server-${SystemName}-${SubName}
  S3BucketLogsArn:
    Value: !GetAtt S3BucketLogs.Arn
    Export:
      Name: !Sub S3BucketLogsArn-code-server-${SystemName}-${SubName}
# ALB
  AlbUrl:
    Value: !Sub https://code-server-${SystemName}-${SubName}.${DomainName}
    Export:
      Name: !Sub AlbUrl-${SystemName}-${SubName}
  AlbListenerArn:
    Value: !GetAtt AlbListenerHttps.ListenerArn
    Export:
      Name: !Sub AlbListenerArn-${SystemName}-${SubName}

 

続編記事

続編記事が出来次第、この章を更新します。

 

まとめ

いかがでしたでしょうか。

今回は実装編 2 Application Load Balancer でした。まだソリューションの核心には触れられていませんが、ベースはできてきています。続編記事で、パスベースルーティングや nginx に触れます。

本記事が皆様のお役に立てれば幸いです。

著者について
広野 祐司

AWS サーバーレスアーキテクチャを駆使して社内クラウド人材育成アプリとコンテンツづくりに勤しんでいます。React で SPA を書き始めたら快適すぎて、他の言語には戻れなくなりました。サーバーレス & React 仲間を増やしたいです。AWS は好きですが、それよりもバックエンド構築を簡単にしてくれたことに対する感謝の気持ちの方が強いです。
取得資格:AWS 認定は15資格、IT サービスマネージャ、ITIL v3 Expert 等
2020 - 2024 Japan AWS Top Engineer 受賞
2022 - 2024 AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS Amplify / AWS AppSync / Amazon Cognito / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする

クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(AWS)は、企業価値の向上につながるAWS 導入を全面支援するオールインワンサービスです。AWS最上位パートナーとして、多種多様な業界のシステム構築実績を持つSCSKが、お客様のDX推進を強力にサポートします。

AWSクラウド
シェアする
タイトルとURLをコピーしました