【PHPバージョン共存】特定のバーチャルドメインのみPHPをバージョンアップする

Apachephp環境の複数のバーチャルドメインを運用されている環境で、特定のバーチャルドメインだけPHPのバージョンアップをしたいという相談を受ける機会があったので検証してみました。 php-fpmを利用してバーチャルドメインごとにphp-fpmのソケットを別にして稼働中の環境はさほど困ることはないかと思いますが、モジュール版でphpを稼働している環境は大変かなと思いましたので、モジュール版からphp-fpm(FastCGI版)へ変更するところ含めて記載します。

前提

以下の環境において、PHPをモジュール版からphp-fpm(Fast CGI)に変更し、「sub2.tekunote.com」のみPHP7.3へ変更します。「sub1.tekunote.com」はPHP5.4のままとします。なお、PHPyumで導入することとします。 ◆前提構成概要

項目 内容
OS CentOS Linux release 7.8
PHP 5.4 (baseリポジトリ)、モジュール版で稼働
PHPモジュール版用設定先 /etc/httpd/conf.d/php.conf
バーチャルドメイン sub1.tekunote.com,sub2.tekunote.com

◆前提バーチャル設定詳細

<VirtualHost *:80>
  ServerName sub1.tekunote.com
  ServerAdmin root@localhost
  DocumentRoot /var/www/vhosts/sub1.tekunote.com/WWW
  ErrorLog /var/www/vhosts/sub1.tekunote.com/LOG/httpd/error_log
  CustomLog /var/www/vhosts/sub1.tekunote.com/LOG/httpd/access_log   combined
</VirtualHost>

<VirtualHost *:80>
  ServerName sub2.tekunote.com
  ServerAdmin root@localhost
  DocumentRoot /var/www/vhosts/sub2.tekunote.com/WWW
  ErrorLog /var/www/vhosts/sub2.tekunote.com/LOG/httpd/error_log
  CustomLog /var/www/vhosts/sub2.tekunote.com/LOG/httpd/access_log   combined
</VirtualHost>

やってみた

php-fpm(5.4 base)のインストール

// インストール
$ sudo yum install --enablerepo=base php-fpm

// php-fpm自動起動
$ systemctl enable php-fpm.service

php73,php73-php-fpm(7.3 remi)のインストール

// remiリポジトリのインストール
$ yum install http://rpms.famillecollet.com/enterprise/remi-release-7.rpm

// php73,php73-php-fpmのインストール
$ yum install --enablerepo=remi-php73 php73 php73-php-fpm

// php73-php-fpm自動起動
$ systemctl enable php73-php-fpm.service

php-fpm,php73-php-fpmデフォルト設定の退避

// 5.4系
$ cd /etc/php-fpm.d/
$ mv www.conf www.conf_org

// 7.3系
$ cd /etc/opt/remi/php73/php-fpm.d/
$ mv www.conf www.conf_org

php-fpmの設定ファイルの作成

以下の通り、sub1.tekunote.com、sub2.tekunote.comの設定を作成します。 作成先はOS標準の5.4系は/etc/php-fpm.d/、remiの7.3系は/etc/opt/remi/php73/php-fpm.d/となります。 ◆sub1.tekunote.com用(5.4) /etc/php-fpm.d/sub1.tekunote.com.conf

[sub1.tekunote.com]

listen = /var/run/php-fpm/sub1.tekunote.com.sock
listen.allowed_clients = 127.0.0.1
listen.owner = apache
listen.group = apache
listen.mode = 0666

user = apache
group = apache

pm = ondemand
pm.max_children = 50
pm.max_requests = 1000

slowlog = /var/www/vhosts/sub1.tekunote.com/LOG/php-fpm/sub1.tekunote.com-slow.log

php_admin_value[error_log] = /var/www/vhosts/sub1.tekunote.com/LOG/php-fpm/sub1.tekunote.com-error.log
php_admin_flag[log_errors] = on

php_value[session.save_handler] = files
php_value[session.save_path] = /var/lib/php/session

◆sub2.tekunote.com用(7.3) /etc/opt/remi/php73/php-fpm.d/sub2.tekunote.com.conf

[sub2.tekunote.com]

listen = /var/run/php-fpm/sub2.tekunote.com.sock
listen.allowed_clients = 127.0.0.1
listen.owner = apache
listen.group = apache
listen.mode = 0666

user = apache
group = apache

pm = ondemand
pm.max_children = 50
pm.max_requests = 1000

slowlog = /var/www/vhosts/sub2.tekunote.com/LOG/php-fpm/sub2.tekunote.com-slow.log

php_admin_value[error_log] = /var/www/vhosts/sub2.tekunote.com/LOG/php-fpm/sub2.tekunote.com-error.log
php_admin_flag[log_errors] = on

php_value[session.save_handler] = files
php_value[session.save_path] = /var/lib/php/session

ポイントはlisten = ソケット名です。listenでバーチャルドメインごとに異なるソケットを指定することで、対応するバージョンへの接続を振り分けることができます。また、今回はソケットを採用しましたがSetHandler "proxy:fcgi://127.0.0.1:ポート番号"のようにポート番号を個別に割り当てることでも振り分けができます。子プロセスの制御については、複数のドメインを1サーバに搭載する場合、子プロセスを必要に応じて立ち上げるpm = ondemandが不要なリソースを抑えられるという判断で、ここではそのように設定しています。その他の設定についても必要最低限を記載していますので、環境に合わせて適宜修正ください。php-fpmの設定については下記をご参考ください。 ◆php-fpm設定の参考サイト PHP: 設定 - Manual

apacheのモジュール版設定の削除

$ cd /etc/httpd/conf.d
$ mv php.conf php.conf_org

phpインストール時のデフォルトでは上記のファイルでモジュール版用の設定がされています。もし、個別に設定している場合は、該当の設定を削除してください。php.confの説明は割愛しますが、DirectoryIndex index.phpが必要な方は、この設定だけ残すか、移動させましょう。

apachephp-fpm,php73-php-fpmの連携

以下のようにバーチャルドメインの設定を行います。FilesMatchディレクティブで囲まれたところが追記する個所です。

<VirtualHost *:80>
  ServerName sub1.tekunote.com
  ServerAdmin root@localhost
  DocumentRoot /var/www/vhosts/sub1.tekunote.com/WWW
  ErrorLog /var/www/vhosts/sub1.tekunote.com/LOG/httpd/error_log
  CustomLog /var/www/vhosts/sub1.tekunote.com/LOG/httpd/access_log   combined

  # php-fpm
  <FilesMatch \.php
 
gt; SetHandler "proxy:unix:/run/php-fpm/sub1.tekunote.com.sock|fcgi://localhost" </FilesMatch> </VirtualHost> <VirtualHost *:80> ServerName sub2.tekunote.com ServerAdmin root@localhost DocumentRoot /var/www/vhosts/sub2.tekunote.com/WWW ErrorLog /var/www/vhosts/sub2.tekunote.com/LOG/httpd/error_log CustomLog /var/www/vhosts/sub2.tekunote.com/LOG/httpd/access_log combined # php73-php-fpm <FilesMatch \.php
 
gt; SetHandler "proxy:unix:/run/php-fpm/sub2.tekunote.com.sock|fcgi://localhost" </FilesMatch> </VirtualHost>

php-fpm,php73-php-fpmの起動とapacheの設定反映

$ systemctl start php-fpm.service
$ systemctl start php73-php-fpm.service
$ systemctl reload httpd.service

動作確認

phpinfoで各バーチャルドメインPHPバージョンの確認と、php-fpmで動作していることの確認を行いました。結果は以下の通りです。php-fpmで動作していることはServer APIの項目でFPM/FastCGIとなっていることで確認できます。もし、モジュール版で動作している際はApache 2.0 Handlerと表示されます。 ◆sub1.tekunoto.com ◆sub2.tekunoto.com

最後に

PHPドメイン個別のバージョンアップの要望に柔軟に対応できそうです。また、Plesk等のサーバ管理ツールでは、ドメインごとにPHPのバージョンを容易に管理できますが、スケールアウトやパフォーマンスのチューニングのことを考えるとサーバ管理ツールを利用することがネックとなる場合もあるかと思います。そういったところからの移行の時にも便利だなと思いました。

【AWS Lambda for Ruby】eventパラメータの取り出し方

Cloudwatch AlarmSNSトピックをLambdaの起動トリガとした際に、eventパラメータの取り出し方に手間取ってしまったのでここに残しておこうと筆をとりました。

やりたかったこと

以下の構成で、SNSトピックのイベントデータから、アラート対象のロードバランサおよびターゲットグループを特定したい。

前提のeventパラメータ

以下のテストイベントを利用します。実際のSNSトピックから渡されるeventパラメータと構成は同じです。伏せたいところを伏せただけです。

{
  "Records": [
    {
      "EventSource": "aws:sns",
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:ap-northeast-1:123456789012:event-test:abcdefghijklmnopqrstuvwxyz",
      "Sns": {
        "Type": "Notification",
        "MessageId": "abcdefghijklmnopqrstuvwxyz",
        "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:event-test",
        "Subject": "ALARM: \"alarm-event-test\" in Asia Pacific (Tokyo)",
        "Message": "{\"AlarmName\":\"alarm-event-test\",\"AlarmDescription\":null,\"AWSAccountId\":\"123456789012\",\"NewStateValue\":\"ALARM\",\"NewStateReason\":\"Threshold Crossed: 1 out of the last 1 datapoints [0.0 (06/06/20 14:04:00)] was less than the threshold (1.0) (minimum 1 datapoint for OK -> ALARM transition).\",\"StateChangeTime\":\"2020-06-06T14:05:17.378+0000\",\"Region\":\"Asia Pacific (Tokyo)\",\"AlarmArn\":\"arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:alarm-event-test\",\"OldStateValue\":\"OK\",\"Trigger\":{\"MetricName\":\"HealthyHostCount\",\"Namespace\":\"AWS/ApplicationELB\",\"StatisticType\":\"Statistic\",\"Statistic\":\"MINIMUM\",\"Unit\":null,\"Dimensions\":[{\"value\":\"targetgroup/tg-event-test/abcdefghijklmnopqrstuvwxyz\",\"name\":\"TargetGroup\"},{\"value\":\"ap-northeast-1c\",\"name\":\"AvailabilityZone\"},{\"value\":\"app/lb-test-event/abcdefghijklmnopqrstuvwxyz\",\"name\":\"LoadBalancer\"}],\"Period\":60,\"EvaluationPeriods\":1,\"ComparisonOperator\":\"LessThanThreshold\",\"Threshold\":1.0,\"TreatMissingData\":\"- TreatMissingData:                    missing\",\"EvaluateLowSampleCountPercentile\":\"\"}}",
        "Timestamp": "2020-06-06T14:05:17.430Z",
        "SignatureVersion": "1",
        "Signature": "abcdefghijklmnopqrstuvwxyz",
        "SigningCertUrl": "https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-abcdefghijklmnopqrstuvwxyz.pem",
        "UnsubscribeUrl": "https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-northeast-1:123456789012:event-test:abcdefghijklmnopqrstuvwxyz",
        "MessageAttributes": {}
      }
    }
  ]
}

eventパラメータの型の確認

目的のパラメータは、eventパラメータ内のMessageの中に含まれています。まずは、Messageまでの型を確認してみます。 型の確認 MessageはString扱いです。

def event_test(event:, context:)
  p event.class
  p event['Records'].class
  p event['Records'][0].class
  p event['Records'][0]['Sns'].class
  p event['Records'][0]['Sns']['Message'].class
end

=> Hash
=> Array
=> Hash
=> Hash
=> String

MessageをHashへ変換して出力

def event_test(event:, context:)
  p JSON.parse(event['Records'][0]['Sns']['Message'])
end

Messageの出力結果 大分スッキリMessageの中身が確認できます。

{
  "AlarmName": "alarm-event-test",
  "AlarmDescription": null,
  "AWSAccountId": "123456789012",
  "NewStateValue": "ALARM",
  "NewStateReason": "Threshold Crossed: 1 out of the last 1 datapoints [0.0 (06/06/20 14:04:00)] was less than the threshold (1.0) (minimum 1 datapoint for OK -> ALARM transition).",
  "StateChangeTime": "2020-06-06T14:05:17.378+0000",
  "Region": "Asia Pacific (Tokyo)",
  "AlarmArn": "arn:aws:cloudwatch:ap-northeast-1:123456789012:alarm:alarm-event-test",
  "OldStateValue": "OK",
  "Trigger": {
    "MetricName": "HealthyHostCount",
    "Namespace": "AWS/ApplicationELB",
    "StatisticType": "Statistic",
    "Statistic": "MINIMUM",
    "Unit": null,
    "Dimensions": [
      {
        "value": "targetgroup/tg-event-test/abcdefghijklmnopqrstuvwxyz",
        "name": "TargetGroup"
      },
      {
        "value": "ap-northeast-1c",
        "name": "AvailabilityZone"
      },
      {
        "value": "app/lb-test-event/abcdefghijklmnopqrstuvwxyz",
        "name": "LoadBalancer"
      }
    ],
    "Period": 60,
    "EvaluationPeriods": 1,
    "ComparisonOperator": "LessThanThreshold",
    "Threshold": 1,
    "TreatMissingData": "- TreatMissingData:                    missing",
    "EvaluateLowSampleCountPercentile": ""
  }
}

ロードバランサとターゲットグループ名の取り出し

def event_test(event:, context:)
  message = JSON.parse(event['Records'][0]['Sns']['Message'])
  p message['Trigger']['Dimensions'].select { |k| k['name'] == 'LoadBalancer' }[0]['value']
  p message['Trigger']['Dimensions'].select { |k| k['name'] == 'TargetGroup' }[0]['value']
end

=> "app/lb-test-event/abcdefghijklmnopqrstuvwxyz"
=> "targetgroup/tg-event-test/abcdefghijklmnopqrstuvwxyz"

最後に

eventパラメータの取り出しでno implicit conversion of String into Integerが発生した際に、ご参考いただければ幸いです。

【VSCode】bash: __git_ps1: command not foundが出たときの対処方法

git-prompt.sh(gitのプロンプト表示のカスタマイズ用のシェル)、git-completion.bash(gitコマンド補完用のシェル)を利用しているLinux環境で、VSCodeの結合ターミナル利用時に下記の通り『bash: __git_ps1: command not found』が表示されることがあったので、対象方法を調べてみました。なお、TerraTerm等のターミナルソフトでは、該当のbashのエラーは表示されないことを前提としています。 もし、TerraTerm等のターミナルソフトでも表示される場合は、__git_ps1git-completion.bash内で定義されているので、git-completion.bashを読み込めていない、もしくは読み込む前に__git_ps1を呼び出しているかのいずれかだと思います。

VSCodeの結合ターミナル上の原

VSCodeのデフォルト設定では、結合ターミナルはログインシェルとしては動作せず、~/.bash_profileも実行されない。 参考 Integrated terminal can't find __git_ps1 · Issue #9484 · microsoft/vscode · GitHub

対処

settings.jsonに以下を設定します。terminal.integrated.shell.linuxは利用するシェルを指定します。デフォルトはbashなので、bashを利用する場合は省略可能です。terminal.integrated.shellArgs.linuxは、~/.bash_profileを実行するための設定です。

    "terminal.integrated.shell.linux": "/bin/bash",
    "terminal.integrated.shellArgs.linux": [
        "-l"
    ]

参考 Integrated Terminal in Visual Studio Code

【Key Management Service】Lambda Rubyで環境変数を暗号化する

KMS(Key Management Service)を利用して、Lambdaの環境変数の暗号化・復号化を試してみます。なお、利用するランタイムはRubyを利用します。

Lambdaの環境変数の暗号化・復号化をやってみよう

KMSのカスタマーマスターキーを作成

カスタマーマスターキーをCLIから作成し、エイリアス名も設定します。キーポリシーは今回はデフォルトで作成されたままのポリシーを使用します。

カスタマーマスターキーの作成

$ aws kms create-key
{
    "KeyMetadata": {
        "Origin": "AWS_KMS",
        "KeyId": "<カスタマーマスターキーID>",
        "Description": "",
        "KeyManager": "CUSTOMER",
        "Enabled": true,
        "KeyUsage": "ENCRYPT_DECRYPT",
        "KeyState": "Enabled",
        "CreationDate": 1557209546.076,
        "Arn": "arn:aws:kms:ap-northeast-1:<AWSアカウントID>:key/<カスタマーマスターキーのキーID>",
        "AWSAccountId": "<AWSアカウントID>"
    }
}

カスタマーマスターキーにエイリアスを設定

エイリアスdemo-lambdaで設定しています。--target-key-idには上で作成したカスタマーマスターキーのキーIDを指定します。

$ aws kms create-alias --alias-name alias/demo-lambda --target-key-id <カスタマーマスターキーのキーID>

Lambda関数へKMSの復号化権限を付与

Lambda関数が暗号化した環境変数を復号化できるようにロールにkms:Decrypt権限を付与する必要があります。権限の追加に関しては、割愛します。ポリシー例は以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DemoKms0",
            "Effect": "Allow",
            "Action": "kms:Decrypt",
            "Resource": "arn:aws:kms:ap-northeast-1:<AWSアカウントID>:key/<カスタマーマスターキーID>"
        }
    ]
}

Lambda関数へ環境変数を設定

コンソール上から以下の通り環境変数を設定します。ここで登録した環境変数USERPASSWDを暗号化した後、Lambda関数上で復号化します。

環境変数を暗号化

暗号化の設定のプルダウンを開きます。伝送中の暗号化のためのヘルパーの有効化にチェックを入れ、伝送中に暗号化する AWS KMS キーに、初めに作成したカスタマーマスターキーのARNを設定します。ARNは、AWSコンソールのKMSのページ等から確認できます。

環境変数USERPASSWDの欄に暗号化のボタンが表示されるのでクリックし、暗号化します。下記は暗号化後の表示です。

Lambda関数の作成

暗号化された環境変数と、復号化した環境変数を表示する関数を作成してみます。

require 'aws-sdk'

def demo_kms(event:, context:)
  kms_client = Aws::KMS::Client.new

  # 環境変数USERの復号化
  user = kms_client.decrypt(ciphertext_blob: Base64.decode64(ENV['USER']))[:plaintext]
  # 環境変数PASSWDの復号化
  passwd = kms_client.decrypt(ciphertext_blob: Base64.decode64(ENV['PASSWD']))[:plaintext]

  puts "USER(暗号化):#{ENV['USER']}"
  puts "PASSWD(暗号化):#{ENV['PASSWD']}"
  puts "USER(復号化):#{user}"
  puts "PASSWD(復号化):#{passwd}"
end

実行結果

Lambdaのログ出力の結果です。暗号化した環境変数と復号化した環境変数が表示されました。

Lambdaの環境変数の暗号化・復号化まとめ

CloudFormationでLambdaを作成する際、AWS::Lambda::Function リソースのKmsKeyArnプロパティで環境変数の暗号化に利用するAWS Key Management Service キーは指定できるようです。ドキュメントはこちら。ただ、実際に環境変数を暗号化するところを、CloudFormationで対応するようなプロパティは見受けられませんでした。自動化して利用する際は、今回のように平文を登録してLambda上で暗号化するのではなく、暗号化済みのものをLambda上に登録する必要があるってことなのですかね。

【Serverless Framework】EC2インスタンス起動時に動的パブリックIPをLambda Rubyでroute53に登録

CloudWatch EventsのEC2インスタンスの起動をトリガーにLambda関数を実行し、起動したEC2インスタンスの動的パブリックIPをroute53のレコードに登録します。また、EC2の停止をトリガーに対象のレコードを削除します。 VPNが接続できない、したくない環境、Elastic IPを節約したいといような個人ユーザにはちょっぴりうれしいかもしれません。興味本位で以下を触りたかったというのが本音で、お題はおまけです。

  • Lambdaでrubyを使ってみたい
  • Serverless Frameworkを使ってみたい

前提条件

前提条件

  • CentOS 7系利用
  • credentialが設定済みであること
  • Serverless Frameworkインストール済み
  • ruby2.5(Lambdaのサポートバージョン)インストール済み

構成図

機能詳細

EC2インスタンスの起動時に以下の2つのタグを利用して、route53のホストゾーンにAレコードを登録します。SetRoute53タグの値がONの時、[Nameタグの値].[ドメイン名]で、動的パブリックIPをroute53のAレコードに登録します。また、EC2インスタンスの停止時に登録されているAレコードの削除を行います。

利用するタグ

  • Nameタグ
  • SetRoute53タグ(値にONを記載)

設定例

項目 設定値
Nameタグ demo-machine
SetRoute53タグ ON ※その他の値やタグ未設定の場合は登録しない
ドメイン tekunote.com
登録されるAレコードのFQDN demo-machine.tekunote.com

やってみよう

Serverless FrameworkでLambda関数の作成とデプロイ

Lambda関数の雛型作成

サービス名、作成先のパス共にdynamic-dnsAWS Lambda のRuby環境の雛型を作成し、対象のパスへ移動します。

$ sls create --template aws-ruby --path dynamic-dns --name dynamic-dns
$ cd dynamic-dns

dynamic-dnsディレクトリ内に作成されたserverless.ymlhandler.rbの2ファイル利用します。

$ ls -la
total 16
drwxrwxr-x.  3 demo-user demo-user   83 May  9 17:04 .
drwx------. 15 demo-user demo-user 4096 May  9 17:04 ..
-rw-r--r--.  1 demo-user demo-user 1124 May  6 23:23 .gitignore
-rw-rw-r--.  1 demo-user demo-user 1552 May  9 09:50 handler.rb
drwxrwxr-x.  2 demo-user demo-user  157 May  9 16:00 .serverless
-rw-r--r--.  1 demo-user demo-user 1011 May  9 15:56 serverless.yml

環境変数の設定

次の手順で、serverless.ymlを設定しますが、以下のパラメータはべた書きせず、環境変数から読み込むようにしていますので設定しておきます。

$ export AWS_ACCOUNT_ID=111122223333 # AWSアカウントID
$ export HOSTED_ZONE_ID=ABCDEFH012345 # 設定対象のホストゾーンID
$ export AWS_REGION=ap-northeast-1 # リージョン
$ export DOMAIN=tekunote.com # Aレコードを設定する対象のドメイン

serverless.yamlの設定

以下の通り設定します。環境変数HOSTED_ZONE_IDDOMAINについてはLambda関数の環境変数に登録されます。serverless.ymlのリファレンスはこちらです。

service: dynamic-dns

provider:
  name: aws
  stage: nonstage
  runtime: ruby2.5
  region: ${env:AWS_REGION}
  memorySize: 256
  timeout: 60
  iamRoleStatements:
    - Effect: Allow
      Action:
        - ec2:DescribeInstances
      Resource: "*"
    - Effect: Allow
      Action:
        - route53:ChangeResourceRecordSets
        - route53:ListResourceRecordSets
      Resource: "arn:aws:route53:::hostedzone/${self:functions.change_record.environment.HOSTED_ZONE_ID}"

functions:
  change_record:
    handler: handler.change_record
    environment:
     HOSTED_ZONE_ID: ${env:HOSTED_ZONE_ID}
     DOMAIN: ${env:DOMAIN}
    events:
      - cloudwatchEvent:
          event:
            source:
              - "aws.ec2"
            detail-type:
              - "EC2 Instance State-change Notification"
            detail:
              state:
                - running
                - stopped

serverless.yamlの設定内容について補足します。 Lambda関数に割り当てるIAMロールの設定はiamRoleStatements:内で設定しています。Lambda関数にec2:DescribeInstancesroute53:ChangeResourceRecordSetsroute53:ListResourceRecordSetsの権限を設定しています。CloudWatch Logsの操作関連の権限も必要ですが、Serverless Frameworkではデフォルトで付与されるためserverless.yaml内に記載する必要はありません。

  iamRoleStatements:
    - Effect: Allow
      Action:
        - ec2:DescribeInstances
      Resource: "*"
    - Effect: Allow
      Action:
        - route53:ChangeResourceRecordSets
        - route53:ListResourceRecordSets
      Resource: "arn:aws:route53:::hostedzone/${self:functions.change_record.environment.HOSTED_ZONE_ID}"

CloudWatch Eventのトリガ(EC2起動時に実行)はevents:内で設定しています。

    events:
      - cloudwatchEvent:
          event:
            source:
              - "aws.ec2"
            detail-type:
              - "EC2 Instance State-change Notification"
            detail:
              state:
                - running
                - stopped

handler.rbの作成

今回作成してみたLambda関数のソースは以下です。handler.rbに上書きします。

require 'aws-sdk'

def change_record(event:, context:)
  ttl = '60'
  record_type = 'A'
  filter_tag_key = 'SetRoute53'
  filter_tag_value = 'ON'

  ec2_client = Aws::EC2::Client.new
  route53_client = Aws::Route53::Client.new

  # タグとCloudWatchのイベントから取得したインスタンスIDでフィルタ
  ec2_resp = ec2_client.describe_instances(
    filters: [
      {
        name: "tag:#{filter_tag_key}",
        values: [filter_tag_value.to_s]
      }
    ],
    instance_ids: [(event['detail']['instance-id']).to_s]
  )

  instance = ec2_resp.reservations[0].instances[0]
  instance.tags.each do |tag|
    # レコードの更新にはNameタグの値を利用するので、Nameタグと一致するときのみ処理
    next unless tag[:key] == 'Name'

    route53_resp = route53_client.list_resource_record_sets(
      hosted_zone_id: (ENV['HOSTED_ZONE_ID']).to_s,
      start_record_type: record_type,
      start_record_name: "#{tag[:value]}.#{ENV['DOMAIN']}.",
      max_items: 1
    )

    record_name = route53_resp.resource_record_sets[0].name
    record_value = route53_resp.resource_record_sets[0].resource_records[0].value

    # 起動時は、EC2インスタンスに割り当てられたパブリックIPを利用して更新処理
    if instance.state.name == 'running'
      record_set_action = 'UPSERT'
      ip_address = instance.public_ip_address
    # 停止時は、既にAレコードに登録済みのIPを取得し削除処理
    elsif instance.state.name == 'stopped'
      record_set_action = 'DELETE'
      ip_address = record_name == "#{tag[:value]}.#{ENV['DOMAIN']}." ? record_value : nil
    end

    next if ip_address.nil?

    # Aレコードの更新、削除処理
    route53_client.change_resource_record_sets(
      change_batch: {
        changes: [
          action: record_set_action,
          resource_record_set: {
            name: "#{tag[:value]}.#{ENV['DOMAIN']}.",
            type: record_type,
            ttl: ttl.to_s,
            resource_records: [
              {
                value: ip_address
              }
            ]
          }
        ]
      },
      hosted_zone_id: ENV['HOSTED_ZONE_ID']
    )
    # CloudWatch Logに出力
    puts "#{record_set_action} Record Infomation: type=#{record_type} fqdn=#{tag[:value]}.#{ENV['DOMAIN']}. ip=#{ip_address}"
  end
end

デプロイ

CloudFormationスタックdynamic-dns-nonstageが作成され、CloudFormationから各リソースが作成されます。deploy時のオプションの詳細表示(-v)で、CloudFormationからどのリソースが作成されるか確認できますが、今回は指定せずに伏せています。

[demo-user@demo-machine dynamic-dns]$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service dynamic-dns.zip file to S3 (1.92 KB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
.....................
Serverless: Stack update finished...
Service Information
service: dynamic-dns
stage: nonstage
region: ap-northeast-1
stack: dynamic-dns-nonstage
resources: 7
api keys:
  None
endpoints:
  None
functions:
  change_record: dynamic-dns-nonstage-change_record
layers:
  None

動作確認

EC2インスタンスにタグを設定し起動

以下のようにEC2インスタンスのタグを設定し、起動してみます。

Route53のレコード確認

IPはマスクしていますが、このような形でレコードが登録されます。

CloudWatch Log確認

こちらもIPはマスクしていますが、このような形で表示されます。 EC2を停止した場合は、登録したAレコードが削除されます。

最後に

Serverless Frameworkが、CloudFormationでデプロイされることさえ、使ってみるまで知らなかったです。。。CloudFormationの変更セット適用するとき、差分チェックやら置換の有無やら結構たいへんだったなぁという思い出に浸りました。読んでいただきありがとうございました。