【Terraform】remote-execを使ったリモートサーバーのプロビジョニング

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Terraformを使うと、インフラ構成をコード化しシングルコマンドでAWS等のクラウド環境に仮想サーバー(およびその他のリソース)を構築することができます。構築した仮想サーバーには、各種ミドルウェアなどをセットアップしてアプリケーションの実行環境を整えていくわけですが(所謂プロビジョニングと呼ばれる工程です)、TerraformではProvisionersの機能を使うとインフラ構築〜プロビジョニングまでを一気通貫で行うことができます。

Terraformで用意されているProvisionerは以下の5つです。

Provisioner 説明
chef リモートサーバー上でChef Clientを実行
connection リモートサーバーへの接続設定(SSH、WinRM)を定義するためのProvisioner
file Terraformを実行するローカルマシンからリモートサーバーへファイルをコピー
local-exec Terraformを実行するローカル端末上でコマンドを実行
remote-exec リモートサーバー上でコマンドを実行

connectionは、cheffileremote-execとの組み合わせで使います。リモートサーバーに対してプロビジョニングを実行する方法としては、大きくはchefまたはremote-execの2択で、これにfileを組み合わせて使う、という理解でよいかと思います。

今回はこれらのうち、remote-execを使って、Amazon EC2に対してプロビジョニングを実行する手順をご紹介します。

なお、今回のエントリでは、AWSでTerraformに入門 | Developers.IOで作成したTerraformの定義ファイル(*.tf)にremote-execでのプロビジョニングの定義を追加する形をとっています。事前準備としてこちらのエントリにも目を通しておいていただければと思います。

前提とする環境

  • ローカルマシンのOS : OS X Yosemite
  • プロビジョニング対象サーバーのOS : Amazon LinuxとWindows 2012 R2
  • Terraform : v0.6.3

Terraformの定義ファイルは以下の構成としています。

ファイル名 説明
main.tf メインの定義ファイル(プロバイダーと各種リソースの定義)
variables.tf 変数の定義
terraform.tfvars 変数の値の設定
output.tf アウトプットの定義

remote-execの概要

remote-execを使うと、リモートサーバーに接続して、リモートサーバー上で各種コマンドを実行することが出来ます。 リモート接続には、Linux環境であればssh、Windows環境であればWinRMが使われます。

Terraformの定義ファイル(*.tf)に定義する内容は大きくは以下の2つです。

  • リモートサーバーへの接続の設定(ユーザー名、鍵ファイル、など。)
  • 実行するコマンドやスクリプト群

connectionブロックでssh or WinRMの接続方法を定義したら、あとは実行するコマンドを並べるだけ、という感じです。コマンド間の依存関係などが考慮されるわけではないので、コマンドの実行順序はユーザー側で考慮する必要があります。

fileと組み合わせると、ローカルマシンからリモートサーバーにスクリプトをコピーして、そのスクリプトを実行するといったことも可能です。

以上を踏まえて、リモートサーバーがLinux環境の場合とWindows環境の場合に分けて、remote-execの使い方を見ていきたいと思います。

remote-execの定義(Linux環境)

provisionerの定義

プロビジョニング対象のEC2のresourceブロック内にprovisionerブロックを追加します。provisionerにはremote-execを指定します。

resource "aws_instance" "cm-test" {
    ami = "${var.images.ap-northeast-1}"
    instance_type = "t2.micro"
    
    (中略)
    
    provisioner "remote-exec"  {
    }
}

connectionの定義

provisionerブロック内にconnectionブロックを追加し、その中にssh接続の設定を記述します。 ssh接続の設定では鍵ファイルを指定する必要ありますので、まず鍵ファイルのパスを変数で定義しておきましょう。

variable "aws_access_key" {}
variable "aws_secret_key" {}

(中略)

variable "ssh_key_file" {}
aws_access_key = "XXXXXXXXXXXXXXXXXXXX"
aws_secret_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
ssh_key_file = "~/.ssh/cm-yawata.yutaka.pem"

sshの接続設定には、接続タイプ(type)、ユーザー名(user)、鍵ファイル(key_file)を指定します。

resource "aws_instance" "cm-test" {
    ami = "${var.images.ap-northeast-1}"
    instance_type = "t2.micro"
    
    (中略)

    provisioner "remote-exec" {
        connection {
            type = "ssh"
            user = "ec2-user"
            key_file = "${var.ssh_key_file}"
        }
    }
}

typeのデフォルトはsshなので、この指定は省略可能です。その他connectionブロックで設定可能なパラメータについては公式リファレンスを参照下さい。

実行コマンドの定義

実行コマンドの定義にはinlineを使って、実行するコマンドを配列形式で並べていきます。今回は、

  • nginxをインストール
  • nginxを起動
  • nginxの自動起動をON

の3つのコマンドを実行してみます。

resource "aws_instance" "cm-test" {
    ami = "${var.images.ap-northeast-1}"
    instance_type = "t2.micro"
    
    (中略)

    provisioner "remote-exec" {
        connection {
            type = "ssh"
            user = "ec2-user"
            key_file = "${var.ssh_key_file}"
        }
        inline = [
            "sudo yum -y install nginx",
            "sudo service nginx start",
            "sudo chkconfig nginx on"
        ]
    }
}

nginxの動作確認ができるように、セキュリティグループを追加して80番ポートを開放しておきます。

resource "aws_security_group" "web-server" {
    name = "web-server"
    description = "Allow HTTP inbound traffic"
    vpc_id = "${aws_vpc.myVPC.id}"
    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
    egress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

(中略)

resource "aws_instance" "cm-test" {
    ami = "${var.images.ap-northeast-1}"
    instance_type = "t2.micro"
    key_name = "cm-yawata.yutaka"
    vpc_security_group_ids = [
      "${aws_security_group.admin.id}",
      "${aws_security_group.web-server.id}"
    ]

    (中略)
}

remote-execの実行(Linux環境)

リソースの作成とプロビジョニングの実行

provisionerの定義が追加できたので、terraform applyを実行しリソースの作成とプロビジョニングを実行してみます。

注意点として、Terraformのプロビジョニングは仮想マシン(EC2インスタンス)の作成時にしか実行されません(仮想マシンが作成済みの状態だと、provisionerの定義を追加してもプロビジョニングは実行されません)。既に仮想マシンが作成済みの場合は、terraform destroyを実行し、仮想マシンを含めたリソース一式を削除しておいて下さい。

$ terraform apply
...

aws_instance.cm-test: Provisioning with 'remote-exec'...
aws_instance.cm-test (remote-exec): Connecting to remote host via SSH...

...

aws_instance.cm-test (remote-exec): Connecting to remote host via SSH...
aws_instance.cm-test (remote-exec):   Host: 54.XX.XX.XX
aws_instance.cm-test (remote-exec):   User: ec2-user
aws_instance.cm-test (remote-exec):   Password: false
aws_instance.cm-test (remote-exec):   Private key: true
aws_instance.cm-test (remote-exec):   SSH Agent: true
aws_instance.cm-test (remote-exec): Connected!

...

aws_instance.cm-test (remote-exec): Installed:
aws_instance.cm-test (remote-exec):   nginx.x86_64 1:1.6.2-1.23.amzn1

aws_instance.cm-test (remote-exec): Dependency Installed:
aws_instance.cm-test (remote-exec):   GeoIP.x86_64 0:1.4.8-1.5.amzn1
aws_instance.cm-test (remote-exec):   gd.x86_64 0:2.0.35-11.10.amzn1
aws_instance.cm-test (remote-exec):   gperftools-libs.x86_64 0:2.0-11.5.amzn1
aws_instance.cm-test (remote-exec):   libXpm.x86_64 0:3.5.10-2.9.amzn1
aws_instance.cm-test (remote-exec):   libunwind.x86_64 0:1.1-2.1.amzn1

aws_instance.cm-test (remote-exec): Complete!
aws_instance.cm-test (remote-exec): Starting nginx:
aws_instance.cm-test (remote-exec):                        [  OK  ]
aws_instance.cm-test: Creation complete

Apply complete! Resources: 7 added, 0 changed, 0 destroyed.

...

Terraformの実行ログから、リモートサーバーへsshで接続しコマンドが定義した順番に実行された様子が伺えます。nginxが稼働しているか、ブラウザからアクセスして確認してみましょう。

プロビジョニングに失敗した場合

プロビジョニングに失敗してもリソースのロールバックは実行されません(作成したEC2インスタンスは削除されません)。 terraform showでEC2インスタンスの状態を確認するとステータスがtaintedと表示されます。

aws_instance.cm-test: (tainted)
  id = <not created>

この状態でterraform applyを実行すると、EC2インスタンスが再作成され(Delete&Create)、再度プロビジョニングが実行されます。

スクリプトファイルの実行

コマンドの実行結果によって処理を分岐させたい場合など、シングルコマンドの羅列ではこと足りない場合は、ローカル環境で作成したスクリプトファイルをリモートサーバーにコピー&実行することが可能です。この場合はinlineの代わりにscriptを使います(scriptにはローカルマシンにあるスクリプトファイルのパスを指定します)。

nginxインストール&自動起動ONのコマンドをスクリプトファイルに切り出してTerraformのディレクトリ配下(main.tfと同じ階層)に配置します。

#!/bin/sh
sudo yum -y install nginx,
sudo service nginx start,
sudo chkconfig nginx on
resource "aws_instance" "cm-test" {
    ami = "${var.images.ap-northeast-1}"
    instance_type = "t2.micro"
    
    (中略)

    provisioner "remote-exec" {
        connection {
            type = "ssh"
            user = "ec2-user"
            key_file = "${var.ssh_key_file}"
        }
        script = "nginx_install.sh"
    }
}

複数のスクリプトを実行したい場合はscriptの代わりにscriptsを使います。

resource "aws_instance" "cm-test" {
    ami = "${var.images.ap-northeast-1}"
    instance_type = "t2.micro"
    
    (中略)

    provisioner "remote-exec" {
        connection {
            type = "ssh"
            user = "ec2-user"
            key_file = "${var.ssh_key_file}"
        }
        scripts = [
            "nginx_install.sh", 
            "nginx_config.sh"
        ]
    }
}

inlinescriptscriptsは排他の関係にあるためprovisionerブロックの中ではいずれか1つしか指定できないのでご注意下さい。

続いてWindows環境でのremote-execについて見ていきますので、terrafrom destroyで一旦全リソースを削除しておきます。

remote-execの定義(Windows環境)

事前準備(カスタムAMIの作成)

基本的な流れはLinux環境と同様ですが、リモートサーバーへの接続方法がsshではなくWinRMとなるため、事前準備として以下設定済みのカスタムAMIを作成する必要があります。

  • WinRMの有効化
  • WinRMへの接続ポート:5985の開放(Windows FireWallの設定変更)
  • WinRM - httpでの接続の受付
  • WinRM - Basic認証の有効化
  • workディレクトリの作成(オプション)
  • Administratorのパスワード設定

今回は、カスタムAMIは「Windows_Server-2012-R2_RTM-Japanese-64Bit-Base-2015.08.12 - ami-fccc76fc」をベースに作成します。

WinRMの有効化

Windows 2012であればデフォルトでWinRMが有効になっていますが、Windows 2008/2008 R2ではWinRMをマニュアルで有効化しておく必要があります。有効化方法ですが、以下のPowershellのコマンドを実行すればOKです。

PS C:\Users\Administrator> Enable-PSRemoting -Force

このコマンドを実行すると、WinRMサービスの開始、サービスの自動起動設定、httpリスナーの作成、Windows FireWallの設定など、WinRMを使用するために必要なもの一式が自動的に設定されます。

WinRMへの接続ポート:5985の開放(Windows FireWallの設定変更)

Public IPを付与したWindows on EC2では、Windows Firewallのプロファイルは「パブリック」が有効になっています。 前述のEnable-PSRemotingを実行するとこのパブリック・プロファイルに対してWinRMへの接続ポートである5985(※Windows 2008(=WinRMのVersionが1.x)の場合は80)が開放されますが、リモートIPでの制限が有効になっており、同一のサブネット上のマシンからしかWinRMに接続できない状態となっています。

# 設定確認
C:\Users\Administrator> netsh advfirewall firewall show rule name="Windows リモート管理 (HTTP 受信)" profile=public

規則名:                               Windows リモート管理 (HTTP 受信)
----------------------------------------------------------------------
有効:                                 はい
方向:                                 入力
プロファイル:                         パブリック
グループ:                             Windows リモート管理
ローカル IP:                          任意
リモート IP:                          LocalSubnet
プロトコル:                           TCP
ローカル ポート:                      5985
リモート ポート:                      任意
エッジ トラバーサル:                  いいえ
操作:                                 許可
OK

Terraformを実行するローカルマシンからWinRMでリモートサーバーへ接続できるよう、リモートIPでの制限設定を変更し、ローカルマシンが利用するグローバルIP、または任意のIP(any)からの接続を許可しておきます。

# リモートIP制限をローカルマシンが利用するグローバルIPに変更
PS C:\Users\Administrator> netsh advfirewall firewall set rule name="Windows リモート管理 (HTTP 受信)" profile=public new remoteip=XX.XX.XX.XX

# リモートIP制限をanyに変更
PS C:\Users\Administrator> netsh advfirewall firewall set rule name="Windows リモート管理 (HTTP 受信)" profile=public new remoteip=any

httpでの接続の受付

WinRMは、デフォルトでは暗号化された通信(httpsでの通信)のみが許可されており、httpでの通信は許可されていません。httpsでの通信設定は証明書のインストール等が手間なので、今回はWinRMの設定を変更してhttpでの通信を許可する方法をとります。

# 設定確認(Undecryptedがfalseとなっている)
C:\Users\Administrator> winrm get winrm/config/service
Service
    RootSDDL = O:NSG:BAD:P(A;;GA;;;BA)(A;;GR;;;IU)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)
    MaxConcurrentOperations = 4294967295
    MaxConcurrentOperationsPerUser = 1500
    EnumerationTimeoutms = 240000
    MaxConnections = 300
    MaxPacketRetrievalTimeSeconds = 120
    AllowUnencrypted = false
(後略)

# 設定変更(Undecryptedをtrueに変更)
C:\Users\Administrator> winrm set winrm/config/service '@{AllowUnencrypted="true"}'
Service
    RootSDDL = O:NSG:BAD:P(A;;GA;;;BA)(A;;GR;;;IU)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)
    MaxConcurrentOperations = 4294967295
    MaxConcurrentOperationsPerUser = 1500
    EnumerationTimeoutms = 240000
    MaxConnections = 300
    MaxPacketRetrievalTimeSeconds = 120
    AllowUnencrypted = true
(後略)

Basic認証の有効化

Terraformでは、ローカルマシンからWinRMでのリモートサバーへの接続にはパスワード認証を使用します。WinRM側ではBasic認証を有効化しておきます。

# 設定確認(デフォルトではBasic認証はfalseになっている)
C:\Users\Administrator> winrm get winrm/config/service/auth
Auth
    Basic = false
    Kerberos = true
    Negotiate = true
    Certificate = false
    CredSSP = false
    CbtHardeningLevel = Relaxed

# 設定変更(Basic認証をtrue変更)
C:\Users\Administrator> winrm set winrm/config/service/auth '@{Basic="true"}'
Auth
    Basic = true
    Kerberos = true
    Negotiate = true
    Certificate = false
    CredSSP = false
    CbtHardeningLevel = Relaxed

workディレクトリの作成

これは必須ではありません。後述しますが、今回Windows環境ではremote-execの例としてFirefoxをインストールしてみます。使用するインストールファイルはリモートサーバーでダウンロードすることも出来ますが、今回はfileを使ってローカルマシンからリモートサーバーにコピーする方法をとります。この際のファイルのコピー先として、リモートサーバー上にworkディレクトリを作成しておきます。

C:\Users\Administrator> mkdir c:\terraform-work

Administratorのパスワード設定

ここまででTerraformからWinRMで接続するためのリモートサーバー側の準備が整いました。カスタムAMIを作成するため、EC2Configから「Shutdown with Sysprep」を実行してEC2インスタンスをシャットダウンします。

この際、Administrator Passwordの設定は、「Specify」を選択してSysprep実行時に任意のパスワードが設定されるようにしておきます(Terraformの定義ファイルで、WinRMでリモートサーバーに接続する際のパスワードを指定する必要があるため)。

ec2config-sysprep

Administrator Passwordの設定では「Keep Existing」も選択は可能ですが、Windows 2008以降のOSではこのオプションは動作しないのでご注意下さい。また、Administrator以外に、Terraformのプロビジョニング用のユーザーを作成しておく方法でも構いません。

EC2インスタンスがシャットダウンされたらAMIを作成し、Terrafromのリソース定義でamiの属性値を作成したAMIのidに置き換えます。

resource "aws_instance" "cm-test" {
    ami = "ami-e05xxxxx"
    instance_type = "t2.micro"
    
    (中略)
}

セキュリティグループの設定

管理用のセキュリティグループの設定を書換えて、WinRM(ポート:5985)での通信を許可しておきます。プロビジョニングの結果確認ができるように、合わせてRDPの3389も開放しておきます。

resource "aws_security_group" "admin" {
  name = "admin"
  description = "Allow RDP and WinRM inbound traffic"
  vpc_id = "${aws_vpc.myVPC.id}"
  ingress {
      from_port = 3389
      to_port = 3389
      protocol = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
      from_port = 5985
      to_port = 5985
      protocol = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
  }
  
  (中略)
}

connectionの定義

connectionブロックでは、typesshからwinrmに変更し、userpasswordを設定します。

resource "aws_instance" "cm-test" {
    ami = "ami-e05xxxxx"
    instance_type = "t2.micro"
    
    (中略)

    provisioner "remote-exec" {
        connection {
            user = "Administrator"
            type = "winrm"
            password = "${var.admin_password}"
        }

        (中略)
        
    }
}

Administratorのパスワードは、例によって変数で定義しておきます。

variable "admin_password" {}
admin_password = "XXXXXXXXXXXXXXXXXXXX"

実行コマンドの定義

fileを使ってFirefoxのインストールファイルをローカルマシンからリモートサーバーにコピーして、inlineでそのファイルを実行します(Firefoxのインストールファイルは予めダウンロード&Terraformのディレクトリ配下(main.tfと同じ階層)に配置しておきます)。

また、fileでもremote-exec同様にconnectionを定義しますが、timeoutの値を10分程度に設定しおきます(デフォルトは5分。インスタンスタイプ等の条件にもよるかと思いますが、EC2インスタンスが起動してWinRMで接続が可能になるまで5分以上かかる場合があります)。

resource "aws_instance" "cm-test" {
    ami = "ami-e05xxxxx"
    instance_type = "t2.micro"
    
    (中略)

    ### Firefoxのインストールファイルをコピー
    provisioner "file" {
        connection {
            user = "Administrator"
            type = "winrm"
            password = "${var.admin_password}"
            timeout = "10m"
        }
        source = "FirefoxSetup40.0.3.exe"
        destination = "C:\terraform-work\FirefoxSetup40.0.3.exe"
    }
    
    ### Firefoxのインストール
    provisioner "remote-exec" {
        connection {
            user = "Administrator"
            type = "winrm"
            password = "${var.admin_password}"
        }

        inline = [
            "C:\terraform-work\FirefoxSetup40.0.3.exe -ms"
        ]
    }
}

remote-execの実行(Windows環境)

remote-execの実行方法はLinux環境と同じです。terraform applyでリソースの作成とプロビジョニングを実行します。

$ terraform apply
...

aws_instance.cm-test: Provisioning with 'file'...
aws_instance.cm-test: Provisioning with 'remote-exec'...
aws_instance.cm-test (remote-exec): Connecting to remote host via WinRM...
aws_instance.cm-test (remote-exec):   Host: 54.65.52.128
aws_instance.cm-test (remote-exec):   Port: 5985
aws_instance.cm-test (remote-exec):   User: Administrator
aws_instance.cm-test (remote-exec):   Password: true
aws_instance.cm-test (remote-exec):   HTTPS: false
aws_instance.cm-test (remote-exec):   Insecure: false
aws_instance.cm-test (remote-exec):   CACert: false
aws_instance.cm-test (remote-exec): Connected!

aws_instance.cm-test (remote-exec): C:\Users\Administrator>C:\terraform-work\FirefoxSetup40.0.3.exe -ms
aws_instance.cm-test: Creation complete

Apply complete! Resources: 8 added, 0 changed, 0 destroyed.

...

RDPでリモートサーバーへ接続し、Firefoxがインストールされたことを確認してみましょう。

まとめ

リモートサーバーがWindowsの場合は事前準備が若干手間ですが、remote-execの定義自体は非常にシンプルで分かりやすいため、簡単なプロビジョニングであればremote-execを使うのもアリかと思います。

ある程度複雑なプロビジョニングの処理が必要な場合は、AnsibleChef などのプロビジョニングツールを使って、プロビジョニングの定義をTerrafromからは切り離して管理した方が良さそうです。(冒頭で触れたchefプロビジョナを使うとChef Serverとの連携が可能なので、この手順についても別のエントリでご紹介したいと思います。)

または、同じHashiCorp製のプロダクトであるPackerを使ってプロビジョニング済みのマシンイメージを作成しておき、Terraformからはそのイメージを参照する、という方法も有力な選択肢の1つかと思います。