CDKでEC2を立ち上げるのはよいのだけれど、インスタンスが立ち上がった後にSSHやセッションマネージャーを使って初期設定作業を毎回するのは面倒だし、せっかくインフラのコード化(IaC)したのにこの作業のみ手作業なんてイケてない。
絶対にCDKで何かしらの方法があるはずだ、ということで調べてみました。
調べた結果以下の2箇所で設定できることがわかった。
-
方法(1) ec2.UserDataのaddCommandsにストリング配列で設定し、userDataプロパティに設定する方法
const userData = ec2.UserData.forLinux({ shebang: '#!/bin/bash' }) userData.addCommands( '何かしらのコマンド' 'sudo apt-get update', 'echo test' ) // EC2インスタンスを作成する const ec2Instance = new ec2.Instance(this, 'hogehogeInstance', { vpc, instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, userData: userData, //<=== この行が必要 ・ ・ ・ });
-
方法(2) 実行したいコマンドをシェルにして、アセット化しS3にアップロード後、デプロイ時にダウンロードして実行する方法。
-
作成例
src
フォルダ内に実行したいコマンドをシェル(例えば config.sh)
にして配置する。(アセット)#!/bin/bash # src/config.sh として作成する # このシェルはrootユーザで実行されるため ubuntu ユーザで実行したい場合は # su コマンドを利用する apt-get install curl sudo su - ubuntu <<EOF curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh EOF
-
上記で作成したアセットをCDKでインスタンス作成時に実行するようにするコード
// s3にアップロードされる const asset = new Asset(this, 'Asset', { path: path.join(__dirname, '../src/config.sh') }); // s3からダウンロードする asset.grantRead(ec2Instance.role); const localPath = ec2Instance.userData.addS3DownloadCommand({ bucket: asset.bucket, bucketKey: asset.s3ObjectKey, }); // EC2インスタンスを作成する const ec2Instance = new ec2.Instance(this, 'hogehogeInstance', { vpc, instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ・ ・ ・ }); // インスタンス作成時にコマンドとして実行する。 ec2Instance.userData.addExecuteFileCommand({ filePath: localPath, });
-
どちらの方法でも初期設定コマンドが実行される、また両方設定しても問題はなく、両方設定した場合は方法(1)が実行されたあとに方法(2)が実行される
実行ログは /var/log/cloud-init-output.log
に吐き出されているのでエラーが発生した場合はこのファイルをみると確認できる
注意点
-
あくまで初期設定(EC2のインスタンスが作成されるときに一回コッキリで実行される)なのでEC2が置き換わらない変更をCDKに加えた場合は初期設定コマンドは実行されない。
-
アセット(この場合 src/config.sh)を書き換えてもEC2のリプレースは発生しないので上記と同様に変更したシェルの内容は実行されない。(s3のアップロードのみ行われる)
いきなりcdk deploy
するのではなく、一旦 cdk diff
してみて EC2インスタンスが置き換わるかチェックするのをおすすめします。
最後に
ElasticIPを取得、SSHの設定、セッションマネージャーからの接続が可能なCDKのサンプルコードを記述しておきます。
お好きなように改変してお使いください。
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam'
import * as path from 'path';
import { Asset } from 'aws-cdk-lib/aws-s3-assets';
import { Construct } from 'constructs';
import { RemovalPolicy, Token } from "aws-cdk-lib";
export class Sample extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const stackName = 'Sample'
// VPC
const vpc = new ec2.Vpc(this, `${stackName}VPC`, {
natGateways: 0,
subnetConfiguration: [{
cidrMask: 24,
name: `${stackName}PublicSubnet`,
subnetType: ec2.SubnetType.PUBLIC
}],
maxAzs: 1
});
const ami = ec2.MachineImage.genericLinux({
//Ubuntu Server 20.04 LTS (HVM), SSD Volume Type
'ap-northeast-1': 'ami-09b18720cb71042df',
});
// セッションマネージャーから接続できるように
const role = new iam.Role(this, `${stackName}ec2Role`, {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com')
})
role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'))
// あとでログなどをClowdWatchで出力できるように
role.addToPolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
//https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/QuickStartEC2Instance.html
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams"
],
resources: ["*"],
}));
// ユーザーデータを S3 にアップロードする
const asset = new Asset(this, 'Asset', { path: path.join(__dirname, '../src/config.sh') });
// https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/user-data.html#user-data-shell-scripts
// cat /var/log/cloud-init-output.log に debug情報あり
const userData = ec2.UserData.forLinux({ shebang: '#!/bin/bash' })
userData.addCommands(
'sudo apt-get update',
`echo test`
)
// キーペア作成
const cfnKeyPair = new ec2.CfnKeyPair(this, `hogehogeCfnKeyPair`, {
keyName: `hogehogekey-pair`,
})
cfnKeyPair.applyRemovalPolicy(RemovalPolicy.DESTROY)
// SSH (TCP Port 22)
const securityGroup = new ec2.SecurityGroup(this, `hogehogeSecurityGroup`, {
vpc,
description: 'outbound all',
allowAllOutbound: true
});
securityGroup.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22), 'Allow SSH Access')
const ec2Instance = new ec2.Instance(this, `hogehogeInstance`, {
vpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.NANO),
machineImage: ami,
securityGroup: securityGroup,
keyName: Token.asString(cfnKeyPair.ref),
role: role,
userData: userData,
});
asset.grantRead(ec2Instance.role);
const localPath = ec2Instance.userData.addS3DownloadCommand({
bucket: asset.bucket,
bucketKey: asset.s3ObjectKey,
});
ec2Instance.userData.addExecuteFileCommand({ filePath: localPath, });
//Elastic IPを設定
let ec2Assoc = new ec2.CfnEIPAssociation(this, `hogehogeEc2Association`, {
eip: 'xxx.xxx.xxx.xxx',
instanceId: ec2Instance.instanceId
});
new cdk.CfnOutput(this, `hogehoge IP Address`, { value: ec2Instance.instancePublicIp });
new cdk.CfnOutput(this, `hogehogeGetSSHKeyCommand`, {
value: `aws ssm get-parameter --name /ec2/keypair/${cfnKeyPair.getAtt('KeyPairId')} --region ${this.region} --with-decryption --query Parameter.Value --output text`,
})
new cdk.CfnOutput(this, `hogehoge ssh command`, { value: 'ssh -i cdk-key.pem -o IdentitiesOnly=yes ec2-user@' + ec2Instance.instancePublicIp })
}
}