AWS CDK Asset 活用によるユーザーデータ拡張 (Windows Server 編)

Amazon EC2のユーザーデータには16KBという制限があり、インフラ構築案件等での複雑なOS設定には不十分です。
本記事では、AWS CDK Assetを活用して、この制限を回避するアーキテクチャの実装方法を解説します。

ユーザーデータ入力を使用して EC2 インスタンスを起動するときにコマンドを実行する - Amazon Elastic Compute Cloud
ユーザーデータスクリプトを入力として渡すことで、インスタンスの起動時にコマンドを実行して設定タスクを実行できます。

 

概要図

OS設定スクリプトをcdk bootstrap実行時に作成されるS3 assetに配置することでユーザーデータのサイズ制限を回避して、OS設定スクリプトをEC2にダウンロードさせています。

 

 

 

 

 

Assets and the AWS CDK - AWS Cloud Development Kit (AWS CDK) v2
Assets are local files, directories, or Docker images.

 

aws-cdk-lib.aws_s3_assets module · AWS CDK
Language | Package

 

処理フロー

 

実装方法

必要最低限のリソース記述のみしていますので、実際に必要な情報などは補完してご利用ください。
処理の補足はコメントアウトに記載しています。

プロジェクト構造

project/
├── lib/
│ ├── infra-stack.ts
│ └── scripts/
│ ├── bootstrap.ps1 # ユーザーデータ(16KB以内)
│ └── userdata.ps1 # OS設定スクリプト(16KB超可能)
├── bin/
│ └── app.ts
└── package.json

CDKファイル

 
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as assets from 'aws-cdk-lib/aws-s3-assets';
import * as fs from 'fs';
import * as path from 'path';
import { Construct } from 'constructs';

export class InfraStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);


    // ===========================================
    // 1. OS設定スクリプトAssetの作成
    // ===========================================
    
    // 【CDK Asset の仕組み】
    // - CDKデプロイ時に指定されたファイルを自動的にS3にアップロード
    // - バケット名・オブジェクトキーは CDK が自動生成

    const osScriptAsset = new assets.Asset(this, 'OSScriptAsset', {
      path: path.join(__dirname, 'scripts', 'userdata.ps1'),  // スクリプトのパス
      description: 'OS configuration script for EC2 instances'
    });

    // ===========================================
    // 2. EC2用IAMロールと権限設定
    // ===========================================
    
    // EC2インスタンスが使用するIAMロール
    const ec2Role = new iam.Role(this, 'EC2Role', {
      assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
      description: 'EC2 role for OS script execution',
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
      ]
    });

    // CDK Assetへの読み取り権限を自動付与
    osScriptAsset.grantRead(ec2Role);

    // ===========================================
    // 3. ユーザーデータ作成関数
    // ===========================================

    // 【ホスト名設定】
    // 1つのスクリプトで複数の異なるホスト名を設定できるように関数の引数としてホスト名を受け取る設計にしている
    // 例:Web-Server-01, DB-Server-01 など用途別のホスト名が可能
    const createUserData = (hostname: string) => {
      // Windows用のユーザーデータオブジェクトを作成
      const userData = ec2.UserData.forWindows();
      
      // Asset情報とホスト名を環境変数として設定
      // これらの環境変数はEC2起動時にユーザーデータスクリプトから参照される
      userData.addCommands(
        // CDKが自動解決するAsset情報を環境変数に設定
        `$env:SCRIPT_S3_BUCKET="${osScriptAsset.s3BucketName}"`,      // Assetが保存されたバケット名
        `$env:SCRIPT_S3_KEY="${osScriptAsset.s3ObjectKey}"`,          // Assetのオブジェクトキー(パス)
        `$env:SCRIPT_S3_REGION="${this.region}"`,                     // 現在のAWSリージョン
        `$env:TARGET_HOSTNAME="${hostname}"`,                         // 設定したいホスト名
        `$env:LOGS_S3_BUCKET="${logsBucket.bucketName}"`,            // ログ出力先バケット
      );

      // bootstrap.ps1(ユーザーデータスクリプト)をユーザーデータに埋め込み
      userData.addCommands(fs.readFileSync(
        path.join(__dirname, 'scripts', 'bootstrap.ps1'), //ユーザーデータスクリプトのパス
        'utf8'  // テキストファイルとして読み込み
      ));
      return userData;
    };

    // ===========================================
    // 4. EC2インスタンス作成
    // ===========================================

    // 既存のキーペアを参照(事前にAWSコンソールで作成が必要)
    const keyPair = ec2.KeyPair.fromKeyPairName(
      this, 'KeyPair', 
      `test-key`  // 実際のキーペア名に置き換える
    );

    //EC2インスタンス作成
    const instance = new ec2.Instance(this, 'EC2Instance', {
      vpc,
      vpcSubnets: { 
        subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS
      },
      instanceType: ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.MEDIUM
      ),
      machineImage: ec2.MachineImage.latestWindows(
        ec2.WindowsVersion.WINDOWS_SERVER_2022_JAPANESE_FULL_BASE
      ),
      securityGroup,
      keyPair,
      // Asset情報を含むユーザーデータを設定
      // ここで設定されたホスト名がWindows側に反映される
      userData: createUserData('dev-server-01'),  // ホスト名を指定
      role: ec2Role
      requireImdsv2: true,
      blockDevices: [{
        deviceName: '/dev/sda1',
        volume: ec2.BlockDeviceVolume.ebs(50, {
          encrypted: true,
          volumeType: ec2.EbsDeviceVolumeType.GP3,
        }),
      }],
    });
  }
}

ユーザーデータスクリプト(PowerShell)

 
# ログ設定
$logDir = "C:\temp\logs"
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$logFile = "$logDir\bootstrap.log"

function Write-Log {
    param([string]$Message)
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "[$timestamp] $Message"
    Add-Content -Path $logFile -Value $logMessage
    Write-Host $logMessage
}

Write-Log "Bootstrap started"

# ホスト名変更
if ($env:TARGET_HOSTNAME) {
    Write-Log "Setting hostname to: $($env:TARGET_HOSTNAME)"
    try {
        if ($env:COMPUTERNAME -ne $env:TARGET_HOSTNAME) {
            Rename-Computer -NewName $env:TARGET_HOSTNAME -Force
            Write-Log "Hostname changed successfully"
            $script:restartRequired = $true
        }
    } catch {
        Write-Log "Hostname change failed: $_"
    }
}

# AWS CLI インストール
Write-Log "Installing AWS CLI"
try {
    $client = New-Object System.Net.WebClient
    $client.DownloadFile(
        "https://awscli.amazonaws.com/AWSCLIV2.msi", 
        "C:\temp\AWSCLIV2.msi"
    )
    Start-Process -FilePath "msiexec.exe" -ArgumentList "/i C:\temp\AWSCLIV2.msi /quiet" -Wait
    Write-Log "AWS CLI installed"
} catch {
    Write-Log "AWS CLI installation failed: $_"
}

# OS設定スクリプトのダウンロードと実行
Write-Log "Downloading OS configuration script"
try {
    # CDKから渡された環境変数
    $bucket = $env:SCRIPT_S3_BUCKET
    $key = $env:SCRIPT_S3_KEY
    $region = $env:SCRIPT_S3_REGION
    
    if (-not $bucket -or -not $key) {
        throw "Missing S3 information"
    }
    
    Write-Log "Downloading from s3://$bucket/$key"
    
    $scriptPath = "C:\temp\userdata.ps1"
    & aws s3 cp "s3://$bucket/$key" $scriptPath --region $region
    
    if (Test-Path $scriptPath) {
        Write-Log "Script downloaded successfully"
        
        # セキュリティブロック解除
        Unblock-File -Path $scriptPath
        
        # スクリプト実行
        Write-Log "Executing OS configuration script"
        & powershell.exe -ExecutionPolicy Bypass -File $scriptPath
        Write-Log "OS configuration completed"
        
    } else {
        throw "Script download failed"
    }
    
} catch {
    Write-Log "OS script execution failed: $_"
}

Write-Log "Bootstrap completed"

注意事項

  • EC2インスタンスからインターネットアクセスが必要になります(NAT Gateway経由)

 

まとめ

こちらの実装パターンの活用によりCDKでのEC2インスタンス構築時にOS設定まですることができ、テンプレートとなるOS設定スクリプトを作成することで再利用性のメリットを享受することが可能になります。

著者について

技術だけじゃダメですか?

なべをフォローする

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

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

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