【Terraform】remote-execを使ったリモートサーバーのプロビジョニング
Terraformを使うと、インフラ構成をコード化しシングルコマンドでAWS等のクラウド環境に仮想サーバー(およびその他のリソース)を構築することができます。構築した仮想サーバーには、各種ミドルウェアなどをセットアップしてアプリケーションの実行環境を整えていくわけですが(所謂プロビジョニングと呼ばれる工程です)、TerraformではProvisionersの機能を使うとインフラ構築〜プロビジョニングまでを一気通貫で行うことができます。
Terraformで用意されているProvisionerは以下の5つです。
Provisioner | 説明 |
---|---|
chef | リモートサーバー上でChef Clientを実行 |
connection | リモートサーバーへの接続設定(SSH、WinRM)を定義するためのProvisioner |
file | Terraformを実行するローカルマシンからリモートサーバーへファイルをコピー |
local-exec | Terraformを実行するローカル端末上でコマンドを実行 |
remote-exec | リモートサーバー上でコマンドを実行 |
connection
は、chef
、file
、remote-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" ] } }
inline
、script
、scripts
は排他の関係にあるため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でリモートサーバーに接続する際のパスワードを指定する必要があるため)。
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
ブロックでは、type
をssh
からwinrm
に変更し、user
とpassword
を設定します。
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
を使うのもアリかと思います。
ある程度複雑なプロビジョニングの処理が必要な場合は、AnsibleやChef
などのプロビジョニングツールを使って、プロビジョニングの定義をTerrafromからは切り離して管理した方が良さそうです。(冒頭で触れたchef
プロビジョナを使うとChef Serverとの連携が可能なので、この手順についても別のエントリでご紹介したいと思います。)
または、同じHashiCorp製のプロダクトであるPackerを使ってプロビジョニング済みのマシンイメージを作成しておき、Terraformからはそのイメージを参照する、という方法も有力な選択肢の1つかと思います。