全力で怠けたい

怠けるために全力を尽くしたいブログ。

CDK で EC2 のユーザーデータを構築する方法。

EC2 はインスタンスを起動するときにユーザーデータをインスタンスに渡してツールをセットアップしたりスクリプトを実行できるけど、CDK で EC2 のユーザーデータを構築する方法をメモしておく。

CDK で EC2 のユーザーデータを構築する

CDK で EC2 のユーザーデータを構築する方法はいくつかあるのでそれぞれを書いていく。

CDK で EC2 のユーザーデータを構築する方法はいくつかあるけど、先に結論を書いちゃうと今のとこは UserData.addCommands() と fs モジュールを組み合わせる方法がベストプラクティスのような気がしてる。

UserData.addCommands()

ユーザーデータを UserData.addCommands() で追加していく。 UserData.addCommands()で追加したユーザーデータは EC2 インスタンス/var/lib/cloud/instances/<instance-id>/user-data.txt にコピーされて EC2 インスタンスの起動時に実行される。

この方法は簡単だけどユーザーデータでやることが増えていくと UserData.addCommands() のとこをたくさん書くことになるのが少し面倒くさい。

import * as cdk from '@aws-cdk/core'
import * as ec2 from '@aws-cdk/aws-ec2'

const userData = ec2.UserData.forLinux({shebang: '#!/bin/bash'})
userData.addCommands(
  'echo userData doing!',
  'echo userData done!',
)

Asset construct

aws-s3-assets の Asset construct を使うとローカルのファイルをそのままユーザーデータとして構築して EC2 インスタンスの起動時に実行する。

import * as cdk from '@aws-cdk/core'
import * as ec2 from '@aws-cdk/aws-ec2'
import {Asset} from '@aws-cdk/aws-s3-assets'
import * as path from 'path'

// ユーザーデータを S3 にアップロードする
const asset = new Asset(this, 'userdata-asset', {
  path: path.join(__dirname, 'assets', 'userdata.sh')
})

// なんか EC2 インスタンスを作る
const instance = new ec2.Instance(this, 'ec2', {
  // 省略
})

// ユーザーデータを S3 からダウンロードする
asset.grantRead(instance.role)
const localPath = instance.userData.addS3DownloadCommand({
  bucket: asset.bucket,
  bucketKey: asset.s3ObjectKey,
})

// S3 からダウンロードしたユーザーデータを実行する
instance.userData.addExecuteFileCommand({
  filePath: localPath,
})

CDK アプリはこんな感じのディレクトリ構成を想定してる。

lib/userdata-stack.ts がスタック定義で lib/assets/userdata.sh がユーザーデータ。

├── bin
│   └── practice_of_cdk.ts
├── cdk.json
├── cdk.out
├── lib
│   ├── assets
│   │   └── userdata.sh
│   └── userdata-stack.ts
│
(省略)

この方法はユーザーデータのシェルスクリプトを普通にエディターとかでいじれるのが楽。

#!/bin/bash
echo userData doing!
echo userData end!

ただ、この方法は aws-s3-assets が CDK 1.46.0 時点では experimental であるため将来的に破壊的な変更が入る可能性がある。

UserData.addCommands() + fs モジュール

今のところは UserData.addCommands() と fs モジュールを組み合わせる方法がベストプラクティスのような気がしてる。

この方法はユーザーデータのシェルスクリプトを普通にエディターとかでいじれるのが楽なのと experimental な API を使ってないので CDK のバージョンを上げるときに破壊的な変更が入ることを気にしなくていい。

import * as cdk from '@aws-cdk/core'
import * as ec2 from '@aws-cdk/aws-ec2'
import * as path from 'path'

const userData = ec2.UserData.forLinux({shebang: '#!/bin/bash'})

// ローカルのユーザーデータのファイルを読み込む
const script = fs.readFileSync(
  path.join(__dirname, 'assets', 'userdata.sh'),
  {encoding: 'utf8'})

// 読み込んだユーザーデータを改行コードで split して userData.addCommands() に追加する
userData.addCommands(...script.split('\n'))

ユーザーデータの実行ログとか

ユーザーデータの実行ログは /var/log/cloud-init-output.log に出力する。

ユーザーデータがやることが増えていくと EC2 インスタンスが起動してもユーザーデータの実行がなかなか終わらないとか増えてくると思うので tail -f /var/log/cloud-init-output.log みたいなコマンドでユーザーデータのログが流れていくのを見たりしてる。

参考ページ