踏み台経由で接続するためのTeraTermマクロ

WindowsからAWSなんかで踏み台サーバのSSHポートフォワードで中のホストに接続する際、TeraTermの[設定]-[SSH転送]に相手先ホストを設定しているのだけれど面倒くさい。

~/.ssh/config のProxyCommandはインスタンスの再生成などで相手先がコロコロ変わるケースでは使いにくい。

ということで、踏み台は固定だけれど相手は色々でも相手の秘密鍵は共通という妙にニッチなケースのためにTeraTermマクロ書いた。詳細は当ファイル冒頭参照。適当に.ttlという拡張子でTeraTermマクロと紐付けて実行するよろし。

複数台並行接続にも対応したいところ。

; =======================================================
; connect to internal hosts via bastion (TeraTerm Macro)
; =======================================================

; 手順1:同じフォルダに踏み台の秘密鍵を置いて当マクロの変数BASTION_USERとBASTION_KEYを書き換える。
; 手順2:同じフォルダに「bastion-template.ini」という名前でTERATERM.INIのコピーを置く。
; 手順3:「bastion-template.ini」の[TTSSH]のDefaultForwardingの値を「__BASTION_REPLACE__」にする。
; 手順4:当マクロを実行する(接続先ホスト、接続先ポート、ローカルポートを入力する)。
; 上記により適当なツールでlocalhostのローカルポートに接続するとSSH転送される。

;; マクロのディレクトリ
getdir MACRO_DIR

;; 踏み台情報(個人ごとに変更)
BASTION_HOST = 'bastion.dev.mycompany.info'
BASTION_PORT = '22'
BASTION_USER = 'tarou_yamada'
sprintf2 BASTION_KEY '%s\%s' MACRO_DIR 'tarou.yamada'

;; 接続先情報
inputbox '接続先ホスト' '接続先情報'
TARGET_HOST = inputstr
inputbox '接続先ポート' '接続先情報'
TARGET_PORT = inputstr
inputbox 'ローカルポート' '接続先情報'
TARGET_LOCAL_PORT = inputstr

;; 置換対象)DefaultForwarding=__BASTION_REPLACE__
;; 例)L2222:aaa:23;L12222:xxx:22
TRAGET_STR = 'L'
strconcat TRAGET_STR TARGET_LOCAL_PORT
strconcat TRAGET_STR ':'
strconcat TRAGET_STR TARGET_HOST
strconcat TRAGET_STR ':'
strconcat TRAGET_STR TARGET_PORT

;; 専用INIファイル
INI_TEMPLATE_FILE = 'bastion-template.ini'
sprintf2 INI_FILE '%s\%s' MACRO_DIR 'bastion.ini'

;; 設定ファイル書き出し
fileopen FH0 INI_TEMPLATE_FILE 0
fileopen FH1 INI_FILE 0
while 1
  filereadln FH0 LINE
  if result = 1 then
    break
  endif
  strreplace LINE 1 '__BASTION_REPLACE__' TRAGET_STR
  filewriteln FH1 LINE
endwhile
fileclose FH1
fileclose FH0

;; 踏み台接続
COMMAND = BASTION_HOST
strconcat COMMAND ':'
strconcat COMMAND BASTION_PORT
strconcat COMMAND ' /ssh 2 /auth=publickey /user='
strconcat COMMAND BASTION_USER
strconcat COMMAND ' /keyfile='
strconcat COMMAND BASTION_KEY
strconcat COMMAND ' /F='
strconcat COMMAND INI_FILE
connect COMMAND

CircleCIとterraformとGitHubのプライベートリポジトリで3環境を構成する


表題の件、結局以下のようにしてみた。当初は単一のアクセスキー/シークレットキーを使ってクロスアカウントのAssumeRoleで切り分けようかと思ったものの、AssumeRoleの切り替えに一時セッショントークンの取得などが必要で余計にややこしくなることから、システムユーザについてはAWSアカウント別に相当するユーザを作ってしまうことにした(人間のユーザはAssumeRoleで運用している)。


prodは本番用AWSアカウント、stgとdevは開発用AWSアカウント。各環境は別VPC。それぞれPowerUserポリシーのterraformerというIAMユーザを定義済み。
通常CircleCIでAWSと連携する場合は「AWS Permissions」にアクセスキーとシークレットキーを書くが、今回はそこには何も書かずに「Environment Variables」に「DEV_AWS_ACCESS_KEY_ID」「DEV_AWS_SECRET_ACCESS_KEY」のような感じでdev/stg/prodの分のアクセスキーを記述。


S3バケットとして対応するAWSアカウントに「company01-dev-terraform」「company01-stg-terraform」「company01-prod-terraform」というのを用意してそこで各環境のtfstateを管理(初回はバケットだけ作っておけば実際に何も入ってない状態でもremote pullとかは正しく動作する)。


git-flowとはちょっと違う運用で、developにpushすると開発環境のplan、release/developmentにpush(developをマージ)すると開発環境へのapply、release/stagingにpush(developをマージ)するとステージング環境へのapply、masterにpush(developをマージ)すると本番環境のplan、release/productionにpushする(masterをマージ)と本番環境へのapply。


GitHub上はOrganizationでプライベートリポジトリを作って運用しているが、当該リポジトリの[Settings]-[Branches]-[Protected branches]から、「master」「release/production」へのpush権限を別途作成したチーム「Administrators」に絞っている。


ちなみにアクセスキーを環境変数で与えている割に「provider "aws"」に「profile」を設定しているが、これは同じリソースを使って人間が手動で流すときに自身の「~/.aws/config」の設定で切り替えられるようにしたもの。CircleCI自体では使っていない。

  • ci/
    • terraform-build.sh
    • terraform-install.sh
    • terraform-validate.sh
  • env/
    • dev/
      • main.tf
    • prod/
      • main.tf
    • stg/
      • main.tf
  • module/
    • vpc/
      • main.tf
      • outputs.tf
      • variables.tf
    • web_cluster/
      • main.tf
      • outputs.tf
      • variables.tf
    • ...
  • .gitignore
  • circle.yml
  • README.md

ci/terraform-build.sh

#!/bin/bash

set -xe

TF_ENV_DIR=$1
TF_BUCKET=$2

ROOT_DIR=$(git rev-parse --show-toplevel)

cd "${ROOT_DIR}/${TF_ENV_DIR}"

terraform get -update
terraform remote config -backend=s3 -backend-config="bucket=${TF_BUCKET}" -backend-config="key=common-infra/terraform.tfstate" -backend-config="region=ap-northeast-1"
terraform apply
terraform remote push

ci/terraform-install.sh

#!/bin/bash

set -xe

cd ~/bin

TF_VERSION=0.6.16

if [ ! -e ~/bin/terraform ]; then
  wget https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip
  unzip terraform_${TF_VERSION}_linux_amd64.zip
  rm terraform_${TF_VERSION}_linux_amd64.zip
fi

ci/terraform-validate.sh

#!/bin/bash

set -xe

TF_ENV_DIR=$1
TF_BUCKET=$2

ROOT_DIR=$(git rev-parse --show-toplevel)

cd "${ROOT_DIR}/${TF_ENV_DIR}"

terraform get -update
terraform remote config -backend=s3 -backend-config="bucket=${TF_BUCKET}" -backend-config="key=common-infra/terraform.tfstate" -backend-config="region=ap-northeast-1"
terraform plan

env/dev/main.tf

provider "aws" {
  profile = "closed-aws-account"
  region = "ap-northeast-1"
}

module "vpc_dev" {
  source = "../../module/vpc"
  env = "dev"
  service = "common"
  cidr = "10.x.x.x/x"
  public_subnets = "10.x.x.x/x,10.x.x.x/x"
  private_subnets = "10.x.x.x/x,10.x.x.x/x"
  azs = "ap-northeast-1a,ap-northeast-1c"
}

module "service01_dev" {
  source = "../../module/web_cluster"
  env = "dev"
  service = "service01"
  ...
}

module "service02_dev" {
  source = "../../module/web_cluster"
  env = "dev"
  service = "service02"
  ...
}

env/prod/main.tf

provider "aws" {
  profile = "open-aws-account"
  region = "ap-northeast-1"
}

module "vpc_prod" {
  source = "../../module/vpc"
  env = "prod"
  service = "common"
  cidr = "10.x.x.x/x"
  public_subnets = "10.x.x.x/x,10.x.x.x/x"
  private_subnets = "10.x.x.x/x,10.x.x.x/x"
  azs = "ap-northeast-1a,ap-northeast-1c"
}

module "service01_prod" {
  source = "../../module/web_cluster"
  env = "prod"
  service = "service01"
  ...
}

module "service02_prod" {
  source = "../../module/web_cluster"
  env = "prod"
  service = "service02"
  ...
}

env/stg/main.tf

provider "aws" {
  profile = "closed-aws-account"
  region = "ap-northeast-1"
}

module "vpc_stg" {
  source = "../../module/vpc"
  env = "stg"
  service = "common"
  cidr = "10.x.x.x/x"
  public_subnets = "10.x.x.x/x,10.x.x.x/x"
  private_subnets = "10.x.x.x/x,10.x.x.x/x"
  azs = "ap-northeast-1a,ap-northeast-1c"
}

module "service01_stg" {
  source = "../../module/web_cluster"
  env = "stg"
  service = "service01"
  ...
}

module "service02_stg" {
  source = "../../module/web_cluster"
  env = "stg"
  service = "service02"
  ...
}

.gitignore

.terraform
*.tfstate
*.tfstate.backup

circle.yml

general:
  branches:
    only:
      - release/development
      - release/staging
      - release/production
      - develop
      - master

machine:
  environment:
    PATH: "${HOME}/bin:${PATH}"

dependencies:
  cache_directories:
    - "~/bin"
  pre:
    - |
      mkdir -p ~/bin
      bash ./ci/terraform-install.sh

test:
  override:
    - |
      if [ "${CIRCLE_BRANCH}" = "release/development" ] || [ "${CIRCLE_BRANCH}" = "develop" ]; then
        AWS_ACCESS_KEY_ID=${DEV_AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY=${DEV_AWS_SECRET_ACCESS_KEY} AWS_DEFAULT_REGION=ap-northeast-1 bash ./ci/terraform-validate.sh env/dev company01-dev-terraform
      elif [ "${CIRCLE_BRANCH}" = "release/staging" ]; then
        AWS_ACCESS_KEY_ID=${STG_AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY=${STG_AWS_SECRET_ACCESS_KEY} AWS_DEFAULT_REGION=ap-northeast-1 bash ./ci/terraform-validate.sh env/stg company01-stg-terraform
      elif [ "${CIRCLE_BRANCH}" = "release/production" ] || [ "${CIRCLE_BRANCH}" = "master" ]; then
        AWS_ACCESS_KEY_ID=${PROD_AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY=${PROD_AWS_SECRET_ACCESS_KEY} AWS_DEFAULT_REGION=ap-northeast-1 bash ./ci/terraform-validate.sh env/prod company01-prod-terraform
      fi

deployment:
  development:
    branch: release/development
    commands:
      - AWS_ACCESS_KEY_ID=${DEV_AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY=${DEV_AWS_SECRET_ACCESS_KEY} AWS_DEFAULT_REGION=ap-northeast-1 bash ./ci/terraform-build.sh env/dev company01-dev-terraform
  staging:
    branch: release/staging
    commands:
      - AWS_ACCESS_KEY_ID=${STG_AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY=${STG_AWS_SECRET_ACCESS_KEY} AWS_DEFAULT_REGION=ap-northeast-1 bash ./ci/terraform-build.sh env/stg company01-stg-terraform
  production:
    branch: release/production
    commands:
      - AWS_ACCESS_KEY_ID=${PROD_AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY=${PROD_AWS_SECRET_ACCESS_KEY} AWS_DEFAULT_REGION=ap-northeast-1 bash ./ci/terraform-build.sh env/prod company01-prod-terraform

AWSアカウント間のIAMロールを使用したアクセスの委任をしているときのクレデンシャル設定

AWS CLI上のお話

本番環境と開発環境でAWSアカウントを分けることがある。

で、以下のような感じでsts:AssumeRoleを使って開発環境のIAMユーザのまま本番環境のIAMロールで振る舞えるようにすることがある(あまりサクサク切り替えられるとAWSアカウントを分けた意味がないので実際には色々と調整)。

チュートリアル: AWS アカウント間の IAM ロールを使用したアクセスの委任
http://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html

このときに手元のPCからAWS CLIのコマンドを開発環境用と本番環境用で呼び分けたいと思ったのでそのときのメモ(defaultはまた別のアカウント用なので気にしない)。
まあ実際にはAWS CLIコマンドではなくterraformの適用先の切り替えをしたくてやっているのですが(resourceのproviderが変数を受け付けてくれないのでmoduleと併用だといろいろ厳しい)。

~/.aws/credentials

[default]
aws_access_key_id=xxx
aws_secret_access_key=xxx

[development]
aws_access_key_id=yyy
aws_secret_access_key=yyy

~/.aws/config

[default]
region=ap-northeast-1

[profile development]
region=ap-northeast-1

[profile production]
source_profile=development
role_arn=arn:aws:iam::xxx:role/role-xxx
region=ap-northeast-1

これで以下のように切り替えて使うことができる。めでたし。

aws ec2 describe-instances --profile development
aws ec2 describe-instances --profile production

terraform上のお話

なお、現時点のterraform(0.6.16)ではproviderを複数指定できるものの、resourceのproviderには変数が使えないため、provider汎用のmoduleを作って呼び元で指定しわけるということができない。
同じようなresourceでproviderをハードコードしたのを複数作らないとダメかなぁ。tjたんも「しかたないのでコピペしてる」って言ってるし。
https://github.com/hashicorp/terraform/issues/1819

追記01:

結局、ハードコードしてもproviderのaliasがmoduleに伝わらないことが分かったのでダミーのproviderをトップに置きつつ(「No valid credential sources found for AWS Provider.」って怒られるので)、実際にはaws_workaround.tfなるものを各moduleの中に入れた。うーん。

aws_workaround.tf

# TODO:
# 本当はprovider汎用のmoduleにproviderのaliasを渡してハンドリングしたいが
# terraform 0.6.16ではresourceのproviderに変数が使えず動的に変えられない。
# https://github.com/hashicorp/terraform/issues/1819
# かつ、そもそもmoduleにaliasしたproviderが伝達されない。
# https://github.com/hashicorp/terraform/issues/4789
# 仕方ないのでproviderを個々のmoduleで定義しているがいつ使えなくなるのか分からないので注意。

variable "profile" { }
variable "region" { }

provider "aws" {
  profile = "${var.profile}"
  region = "${var.region}"
}
追記02:

トップのが効いてダメそうだ。うーん。
とりあえず横断的にやるのは諦めて、
env/development/main.tf
env/production/main.tf
module/xxx/main.tf
module/yyy/main.tf
的にやって各envの中(provider定義こみ)でコマンド発行する方向にした。

ただ、今度はterraform remote configでS3にアクセスする際にassume roleが効かないというのが……。
https://github.com/hashicorp/terraform/issues/7014

以下の感じだとそもそもaws-sdk-go自体が対応していないのかな。
https://github.com/aws/aws-sdk-go/issues/472