8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

CDKでEC2を立ち上げたときに初期設定コマンドを実行する方法

Last updated at Posted at 2023-06-23

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 })

  }
}
8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?