VSCode 導入済みで Web GUI アクセス可能な Amazon EC2 Ubuntu インスタンスを一発構築する [AWS CloudFormation]

こんにちは、広野です。

AWS Cloud9 亡き後の IDE 環境をいろいろと考えてまして。結局のところシンプルな構成に落ち着きました。作成した構成を紹介します。自分の PC に VSCode をインストールしている人には全く役に立たない記事ですのでご放念ください。

ざっくり要件

  • AWS Cloud9 の代わりとなる、クラウド上の IDE を構築したい。
  • リモートアクセスはブラウザで (HTTPS で) で通信できるようにしたい。(特殊なポートは NG)
  • HTTPS 通信で自己証明書を使用するのは NG。
  • OS は Windows 以外にしたい。ライセンス面や価格面など、いろいろと。

アーキテクチャ

  • VSCode をインストールした Ubuntu 24.04 arm64 にブラウザから GUI アクセスする環境です。
  • EC2 には Elastic IP アドレスを割り当て、それをあらかじめ用意してある Amazon Route 53 パブリックホストゾーンに A レコード登録します。そのドメインおよびサブドメインに合わせて、Let’s Encrypt で SSL 証明書を作成します。証明書の有効期限は 90 日なので、更新するためのスクリプトを EC2 内に作成します。(自動更新ではなく、必要になったら実行)
  • リモートアクセス機能は Amazon DCV が提供します。ただし 8443 ポートを使用するので、一応そのポートはそのまま利用できるようにしておき、8443 にアクセスできない環境のために 443 ポートでもアクセスできるよう、nginx でリバースプロキシを構築します。
  • SSL 証明書は、Amazon DCV および nginx にカスタム証明書として組み込みます。
  • 誰でもアクセスできるのはまずいので、とりあえず指定したソース IP アドレスでないと許可しないようセキュリティグループを設定します。セキュリティ面は引き続き強化を検討します。研修用途であれば現状で問題ないと思います。

要件から考えたこと

  • AWS Cloud9 の代わりに、クラウド上の IDE を使用したい。

やはり VSCode もしくは VSCode 準拠のツールにしたいです。まずは純正 VSCode 使用で試行錯誤しています。そのため Amazon EC2 上で動く環境を作りました。今後は code-server も検証したいです。

  • リモートアクセスはブラウザで (HTTPS で) で通信できるようにしたい。(特殊なポートは NG)

RDP や VNC ではなく、ブラウザでリモートのデスクトップを操作できる Amazon DCV を採用しました。会社のネットワーク通信の制約で、特殊ポートの通信ができない場合も 443 であれば通過できます。

  • HTTPS 通信で自己証明書を使用するのは NG。

会社のネットワークセキュリティ等で、自己証明書の Web サイトにはアクセスできないことがあり、正規の SSL 証明書を用意する必要がありました。EC2 に ALB や Amazon CloudFront をかぶせることも考えましたが、コスト面を気にしたのと、CloudFront だと自己証明書のオリジンをサポートしていないために結局オリジン (EC2) に正規の証明書を実装せねばならず、採用をやめました。

  • OS は Windows 以外にしたい。

OS が Windows であれば AWS Systems Manager Fleet Manager によるリモートアクセスはできたのですが、Windows だと高額だったり、会社のルールでセキュリティ対策などしないといけないので、Linux にしました。Linux の中でも、AWS で馴染み深い Amazon Linux 2023 を使用したかったのですが、本来サーバー用途での利用を想定された OS なのでデスクトップが使いにくく、次に馴染みのある Ubuntu にしました。Mint も考えましたが、純正 AMI が無かったのでやめました。インスタンスタイプは Graviton (arm64) を使用した t4g の方が t3 (x86_64) よりも若干安かったので、t4g を採用しています。幸い、使用するツールも arm64 に対応してました。

AWS CloudFormation テンプレート

補足はインラインコメントします。

EC2 ユーザーデータの中で以下のツールをインストールしているのですが、必要に応じて追加、削除しましょう。

  • VSCode
  • Amazon DCV
  • nginx
  • AWS CLI
  • Amazon Q Developer CLI (サインインが必要なのでインストーラダウンロードのみ)
  • Let’s Encrypt 証明書作成用モジュール
  • NVM (Node.js バージョン管理ツール)
AWSTemplateFormatVersion: "2010-09-09"
Description: The CloudFormation template that creates Ubuntu 24.04 ARM64 EC2 instance with GNOME 3, VSCode, and Amazon DCV.

# ------------------------------------------------------------#
# 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

  DomainName:
    Type: String
    Description: Domain name for URL. xxxxx.xxx
    Default: example.com
    MaxLength: 40
    MinLength: 5

  # VPC は既存にあるものを使用する想定です。
  VpcId:
    Type: AWS::EC2::VPC::Id
    Description: Choose a existing VPC ID you deploy the EC2 instance in.

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

  InstanceType:
    Type: String
    Description: EC2 instance type (ARM/Graviton)
    Default: t4g.large
    AllowedValues:
      - t4g.large
      - t4g.xlarge

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

  # アクセスを許可するソースIPアドレス(サブネット)です。
  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

  TimeZone:
    Type: String
    Description: The specified time zone.
    Default: Asia/Tokyo
    MaxLength: 30
    MinLength: 1

  # ubuntu ユーザーの初期パスワードです。Amazon DCV のパスワードにもなります。リソース作成後は変更しましょう。
  InitialPassword:
    Type: String
    Description: The initial OS password. You must change it after your first login.
    Default: Gx8jeTLc
    MaxLength: 128
    MinLength: 8

  # Route 53 パブリックホストゾーンは既存で持っている想定です。
  Route53HostZoneId:
    Type: String
    Description: Route 53 public host zone ID for the SSL certificate.
    Default: XXXXXXXXXXXXXXXXXXXXX
    MaxLength: 30
    MinLength: 1

  # Let's Encrypt は証明書作成時にメールアドレスが必要になります。
  YourEmail:
    Type: String
    Description: Your email address for SSL certificate application.
    Default: xxx@xxx.xxx
    MaxLength: 30
    MinLength: 1

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "General Configuration"
        Parameters:
          - SystemName
          - SubName
      - Label:
          default: "Domain Configuration"
        Parameters:
          - DomainName
          - Route53HostZoneId
          - YourEmail
      - Label:
          default: "Network Configuration"
        Parameters:
          - VpcId
          - InstanceSubnet
          - AllowedSubnet
      - Label:
          default: "EC2 Configuration"
        Parameters:
          - InstanceType
          - InstanceVolumeSize
      - Label:
          default: "OS Configuration"
        Parameters:
          - TimeZone
          - InitialPassword

Resources:
# ------------------------------------------------------------#
# EC2 Launch template
# ------------------------------------------------------------#
  EC2LaunchTemplate:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateName: !Sub vscode-${SystemName}-${SubName}
      LaunchTemplateData:
        InstanceType: !Ref InstanceType
        ImageId: >-
          {{resolve:ssm:/aws/service/canonical/ubuntu/server/24.04/stable/current/arm64/hvm/ebs-gp3/ami-id}}
        BlockDeviceMappings:
          - Ebs:
              VolumeSize: !Ref InstanceVolumeSize
              VolumeType: gp3
              DeleteOnTermination: true
              Encrypted: true
            DeviceName: /dev/sda1
        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 dcv-server access from specified subnets
      SecurityGroupIngress:
        - Description: Allow HTTPS (8443) from the specified client
          FromPort: 8443
          IpProtocol: tcp
          ToPort: 8443
          CidrIp: !Ref AllowedSubnet
        - Description: Allow QUIC from the specified client
          FromPort: 8443
          IpProtocol: udp
          ToPort: 8443
          CidrIp: !Ref AllowedSubnet
        - Description: Allow HTTPS from the specified client
          FromPort: 443
          IpProtocol: tcp
          ToPort: 443
          CidrIp: !Ref AllowedSubnet
      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-vscode-${SystemName}-${SubName}

# ------------------------------------------------------------#
# EC2 Role / Instance Profile (IAM)
# ------------------------------------------------------------#
  Ec2Role:
    Type: AWS::IAM::Role
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W11
            reason: CodeWhisperer requires '*' as a resource, reference https://docs.aws.amazon.com/codewhisperer/latest/userguide/cloud9-setup.html#codewhisperer-IAM-policies
    Properties:
      RoleName: !Sub Ec2Role-vscode-${SystemName}-${SubName}
      Description: This role allows EC2 instance to invoke S3 and SSM.
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      # EC2 インスタンスには、とりあえず私が必要だった権限しか付与していません。用途に応じて追加が必要です。
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
        - arn:aws:iam::aws:policy/AWSCodeCommitPowerUser
        - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
      Policies:
        - PolicyName: !Sub Ec2Policy-vscode-${SystemName}-${SubName}
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              # Amazon DCV のライセンスを読み取るために必要な権限です。
              - Effect: Allow
                Action: s3:GetObject
                Resource: !Sub arn:aws:s3:::dcv-license.${AWS::Region}/*
              # Amazon Q Developer 関連
              - Effect: Allow
                Action:
                  - codewhisperer:GenerateRecommendations
                Resource: "*"
              # 以下は Let's Encrypt への証明書申請のために必要です。DNSによるドメイン所有者確認をします。
              - Effect: Allow
                Action:
                  - route53:ListHostedZones
                  - route53:GetChange
                Resource:
                  - "*"
              - Effect: Allow
                Action:
                  - route53:ChangeResourceRecordSets
                Resource:
                  - !Sub arn:aws:route53:::hostedzone/${Route53HostZoneId}

  Ec2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: !Ref Ec2Role
      Path: /
      Roles:
        - !Ref Ec2Role
    DependsOn:
      - Ec2Role

# ------------------------------------------------------------#
# Elastic IP address for EC2 instance
# ------------------------------------------------------------#
  EipEc2:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}
        - Key: Name
          Value: !Sub eip-vscode-${SystemName}-${SubName}

# ------------------------------------------------------------#
# EIP Association
# ------------------------------------------------------------#
  EIPAssociation:
    Type: AWS::EC2::EIPAssociation
    Properties:
      AllocationId: !GetAtt EipEc2.AllocationId
      InstanceId: !Ref Ec2Instance
    DependsOn:
      - Ec2Instance
      - EipEc2

# ------------------------------------------------------------#
# EC2
# ------------------------------------------------------------#
  Ec2Instance:
    Type: AWS::EC2::Instance
    Properties:
      IamInstanceProfile: !Ref Ec2InstanceProfile
      LaunchTemplate:
        LaunchTemplateId: !Ref EC2LaunchTemplate
        Version: !GetAtt EC2LaunchTemplate.LatestVersionNumber
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          set -euxo pipefail
          export DEBIAN_FRONTEND=noninteractive
          cd /root
          # User password
          echo "ubuntu:${InitialPassword}" | chpasswd
          # Locale & timezone
          timedatectl set-timezone ${TimeZone}
          apt update -y
          apt upgrade -y
          apt install -y language-pack-ja-base language-pack-ja ibus-mozc unzip
          update-locale LANG=ja_JP.UTF-8
          sed -i 's/^XKBLAYOUT=".*"/XKBLAYOUT="jp"/' /etc/default/keyboard
          dpkg-reconfigure -phigh console-setup
          systemctl restart console-setup
          # AWS CLI
          curl https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip -o awscliv2.zip
          unzip awscliv2.zip
          ./aws/install
          rm awscliv2.zip
          rm -fr aws
          # ubuntu desktop
          apt install -y ubuntu-desktop gdm3 xserver-xorg-video-dummy mesa-utils
          sed -i -e "s/^#WaylandEnable=false/WaylandEnable=false/g" /etc/gdm3/custom.conf
          systemctl restart gdm3
          dpkg-reconfigure gdm3
          systemctl set-default graphical.target
          systemctl isolate multi-user.target
          systemctl isolate graphical.target
          tee /etc/X11/xorg.conf << EOF Section "Device" Identifier "DummyDevice" Driver "dummy" Option "UseEDID" "false" VideoRam 512000 EndSection Section "Monitor" Identifier "DummyMonitor" HorizSync 5.0 - 1000.0 VertRefresh 5.0 - 200.0 Option "ReducedBlanking" EndSection Section "Screen" Identifier "DummyScreen" Device "DummyDevice" Monitor "DummyMonitor" DefaultDepth 24 SubSection "Display" Viewport 0 0 Depth 24 Virtual 4096 2160 EndSubSection EndSection EOF systemctl isolate multi-user.target systemctl isolate graphical.target # VSCode apt install -y gpg apt-transport-https software-properties-common wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
          install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/
          sh -c 'echo "deb [arch=arm64] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'
          apt update -y
          apt install -y code
          # Amazon DCV
          wget https://d1uj6qtbmh3dt5.cloudfront.net/NICE-GPG-KEY
          gpg --import NICE-GPG-KEY
          wget https://d1uj6qtbmh3dt5.cloudfront.net/nice-dcv-ubuntu2404-aarch64.tgz
          tar -xvzf nice-dcv-ubuntu2404-aarch64.tgz
          cd nice-dcv-2024.0-19030-ubuntu2404-aarch64
          apt install -y ./nice-dcv-server_2024.0.19030-1_arm64.ubuntu2404.deb ./nice-dcv-web-viewer_2024.0.19030-1_arm64.ubuntu2404.deb ./nice-xdcv_2024.0.654-1_arm64.ubuntu2404.deb
          usermod -aG video dcv
          systemctl enable dcvserver
          cd /root
          rm nice-dcv-ubuntu2404-aarch64.tgz
          rm -fr nice-dcv-2024.0-19030-ubuntu2404-aarch64
          sed -i "/^\[session-management\/automatic-console-session/a owner=\"ubuntu\"\nstorage-root=\"%home%\"" /etc/dcv/dcv.conf
          sed -i "s/^#create-session/create-session/g" /etc/dcv/dcv.conf
          # Create SSL cert
          apt install -y software-properties-common certbot python3-certbot-dns-route53
          certbot certonly --dns-route53 -d vscode-${SystemName}-${SubName}.${DomainName} --agree-tos -m ${YourEmail} --non-interactive --preferred-challenges dns-01
          cp /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/fullchain.pem /etc/dcv/dcv.pem
          cp /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/privkey.pem /etc/dcv/dcv.key
          chown dcv:dcv /etc/dcv/dcv.pem /etc/dcv/dcv.key
          chmod 600 /etc/dcv/dcv.pem /etc/dcv/dcv.key
          # Install nginx
          apt install -y nginx
          systemctl enable --now nginx
          mkdir -p /etc/nginx/ssl
          chmod 700 /etc/nginx/ssl
          cp /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/fullchain.pem /etc/nginx/ssl/dcv-chain.crt
          cp /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/privkey.pem /etc/nginx/ssl/dcv.key
          chmod 600 /etc/nginx/ssl/*.key
          chmod 644 /etc/nginx/ssl/*.crt
          tee /etc/nginx/conf.d/dcv.conf << EOF
          server {
            listen 443 ssl;
            server_name vscode-${SystemName}-${SubName}.${DomainName};
            ssl_certificate_key /etc/nginx/ssl/dcv.key;
            ssl_certificate /etc/nginx/ssl/dcv-chain.crt;
            location / {
              proxy_pass https://127.0.0.1:8443;
              proxy_http_version 1.1;
              proxy_set_header Upgrade \$http_upgrade;
              proxy_set_header Connection "upgrade";
              proxy_set_header Host \$host;
              proxy_set_header X-Real-IP \$remote_addr;
              proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
              proxy_set_header X-Forwarded-Proto \$scheme;
              proxy_read_timeout 3600s;
              proxy_send_timeout 3600s;
              proxy_ssl_verify on;
              proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
              proxy_ssl_verify_depth 2;
              proxy_ssl_name vscode-${SystemName}-${SubName}.${DomainName};
              proxy_ssl_server_name on;
            }
          }
          EOF
          systemctl restart nginx
          # Create cert renew script
          cat << 'EOF' > /home/ubuntu/renew-cert.sh
          #!/bin/bash
          set -e
          sudo certbot renew --cert-name "vscode-${SystemName}-${SubName}.${DomainName}"
          sudo cp -f /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/fullchain.pem /etc/dcv/dcv.pem
          sudo cp -f /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/privkey.pem /etc/dcv/dcv.key
          sudo chown dcv:dcv /etc/dcv/dcv.pem /etc/dcv/dcv.key
          sudo chmod 600 /etc/dcv/dcv.pem /etc/dcv/dcv.key
          sudo cp -f /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/fullchain.pem /etc/nginx/ssl/dcv-chain.crt
          sudo cp -f /etc/letsencrypt/live/vscode-${SystemName}-${SubName}.${DomainName}/privkey.pem /etc/nginx/ssl/dcv.key
          sudo chmod 600 /etc/nginx/ssl/dcv.key
          sudo chmod 644 /etc/nginx/ssl/dcv-chain.crt
          sudo systemctl restart nginx
          echo "Certificate renewed."
          EOF
          chmod +x /home/ubuntu/renew-cert.sh
          chown ubuntu:ubuntu /home/ubuntu/renew-cert.sh
          # Install nvm
          sudo -u ubuntu bash -c "curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash"
          # Download Amazon Q Developer CLI
          sudo -i -u ubuntu bash << EOF
          curl --proto '=https' --tlsv1.2 -sSf "https://desktop-release.q.us-east-1.amazonaws.com/latest/q-aarch64-linux.zip" -o "q.zip"
          unzip q.zip
          EOF
          # Finally, reboot
          reboot
      Tags:
        - Key: Cost
          Value: !Sub ${SystemName}-${SubName}
        - Key: Name
          Value: !Sub vscode-${SystemName}-${SubName}
    DependsOn:
      - Ec2InstanceProfile
      - Route53RecordA
      - S3BucketMyDocuments

# ------------------------------------------------------------#
# Route 53
# ------------------------------------------------------------#
  Route53RecordA:
    Type: AWS::Route53::RecordSet
    Properties:
      HostedZoneName: !Sub ${DomainName}.
      Name: !Sub vscode-${SystemName}-${SubName}.${DomainName}.
      Type: A
      TTL: 300
      ResourceRecords:
        - !GetAtt EipEc2.PublicIp
    DependsOn: EipEc2

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
# EIP
  DcvServerUrl8443:
    Value: !Sub "https://vscode-${SystemName}-${SubName}.${DomainName}:8443"
  DcvServerUrl:
    Value: !Sub "https://vscode-${SystemName}-${SubName}.${DomainName}"

 

リソース作成後の動作

出来上がりはこんな感じです。

AWS CloudFormation テンプレートの出力に、ブラウザアクセス用の URL があります。ubuntu ユーザーでログインします。ユーザーデータの実行に時間がかかるので、ブラウザアクセスはスタックの完了後 10 分程度待った方がいいです。気になる方は /var/log/cloud-init-output.log で状況を確認してください。

再度 ubuntu のログイン画面が表示されるので、ログインします。

VSCode も起動できました。クリップボードの共有もできます。

passwd コマンドで忘れずにパスワード変更しておきましょう。

ubuntu ユーザーのホームディレクトリに、Let’s Encrypt の証明書更新用スクリプト renew-cert.sh を作ってあります。これを実行すると証明書を更新してくれますが、有効期限が 30 日を切らないと更新できないようです。オプションで強制更新する設定もありましたが、そこまでするのはやめました。

 

まとめ

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

要件によってカスタマイズすることで、さらにいろいろなバリエーションを作れそうです。今後この環境を使って Kiro を試したいと思っています。

AWS Cloud9 は軽い点が良かったのですが、VSCode は重いですね・・・。インスタンスタイプが large 以上じゃないときついです。

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

著者について
広野 祐司

AWS サーバーレスアーキテクチャと React を使用して社内向け e-Learning アプリ開発とコンテンツ作成に勤しんでいます。React でアプリを書き始めたら、快適すぎて他の言語には戻れなくなりました。近年は社内外への AWS 技術支援にも従事しています。AWS サービスには AWS が考える IT 設計思想が詰め込まれているので、そこを理解できると他のことにも活かせるので、いつも AWS を通して勉強させて頂いてまます。
取得資格:AWS 認定は15資格、IT サービスマネージャ、ITIL v3 Expert 等
2020 - 2025 Japan AWS Top Engineer 受賞
2022 - 2025 AWS Ambassador 受賞
2023 当社初代フルスタックエンジニア認定
好きなAWSサービス:AWS AppSync Events / AWS Step Functions / AWS CloudFormation

広野 祐司をフォローする

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

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

AWSアプリケーション開発クラウドソリューション
シェアする
タイトルとURLをコピーしました