今回はサウナじゃないです。。すみません。。
GCP環境で開発を行い始めてから1年が経過しました。
今ではAWSよりもGCPの方がわかるようになり、うれしいのやら悲しいのやら。。
さて今回は、
“みんな大好きcloud functions”から”まだまだ現役GCE”へsshコマンドを飛ばす方法
ことの始まりは、
SSL証明書を定期更新したかったのですが、インスタンスグループ化しているので、
グループの更新のcolud functionsの間にSSL証明書更新のコマンドを叩かなくてはいけない。というシーン
cloud functionsでsshでコマンドなんて叩けるの??
調べてみたところ、
OS Loginをを行えばできる方法は見つかったけど、
OS Loginを許可してしまうと他のサービス(今回だとcloud build)からsshできなくなることがある。
うーん、どうしたものか、、
つくるかー となって作りました。
【環境】
- cloud functions 第一世代
- python 3.8ランタイム
pythonはできる限り3.8ランタイムを使用してください。
※3.10を使用したらsubprocessでsshが使えなくて困りました。
※これは3.10からsshのパスが通ってないから起こるらしいです。
【実装】
全体の流れはこんな感じ
- GCE側にsshパブリックキーを配置
- シークレットマネージャーに秘密鍵を登録
- cloud functionsの構成を組む
- シークレットマネージャーの値を呼び出す
- その他の設定
- コーディング
箇条書きだととっても簡単ですね✨
1.GCE側にsshパブリックキーを配置
何かしらの方法でsshのパブリックキー、プライベートキーを発行してください。
今回は≪PuTTYgenで両キーを発行→プライベートキーをOpenSSH形式に変換≫で行いました。
わからない人はPuTTYgenで両キーを発行とプライベートキーをOpenSSH形式に変換を読んでください。
このパブリックキーをGCEに配置します。
インスタンスの編集画面を開いて
「セキュリティとアクセス > SSH 認証鍵」
ここに貼り付け。
もちろんGCE全体のメタデータのSSH認証鍵に登録しても大丈夫です。
2.シークレットマネージャーに秘密鍵を登録
Secret Managerを開いてシークレットを作成ボタン
適当に名前を付けてシークレットを作成します。
赤枠部分からSSHプライベートキーをアップロードしてください。
3.cloud functionsの構成を組む
関数の作成を押していただいて、
適当に名前を付けていただいて、
第1世代を選択。
ランタイムは
ランタイムサービスアカウントに、
Secret Manager のシークレット アクセサーの権限をもつアカウントをセットしてください。
これが無いとシークレットマネージャーの値が読み込めません。
ちなみにこちらタイムアウトも伸ばしています。
sshがつながりにくい場合を考えてます。
次に、シークレットとイメージのリポジトリを開いて、
先に登録したシークレットを呼び出す。
今回は環境変数「SSH_PRIVATE_KEY」に値を載せています。
あとやらなくてはいけないのは接続の設定。
GCEと同じVPCネットワークにあるアクセスコネクタを設定してあげてください。
4.コーディング
基本的には先のOS loginのコードを参考にしています。
まずrequirements.txt
# package>=version
google-api-python-client==2.47.0
google-auth==2.6.2
google-auth-httplib2==0.1.0
google-cloud-compute==1.6.1
requests==2.27.1
次にmain.py(エントリーポイントはmain)
import argparse
import subprocess
import time
from typing import List, Optional, Tuple
import uuid
import requests
import os
SSH_KEY_PATH = '/tmp/dev-user'
def execute(
cmd: List[str],
cwd: Optional[str] = None,
capture_output: Optional[bool] = False,
env: Optional[dict] = None,
raise_errors: Optional[bool] = True
) -> Tuple[int, str]:
print(f'Executing command: {cmd}')
process = subprocess.run(
cmd,
cwd=cwd,
stdout=subprocess.PIPE if capture_output else subprocess.DEVNULL,
stderr=subprocess.STDOUT,
text=True,
env=env,
check=raise_errors
)
output = process.stdout
returncode = process.returncode
if returncode:
print(f'Command returned error status {returncode}')
if capture_output:
print(f"With output: {output}")
return returncode, output
def run_ssh(cmd: str, private_key_file: str, username: str, hostname: str) -> str:
ssh_command = [
'ssh',
'-i', private_key_file,
'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
f'{username}@{hostname}',
cmd,
]
print(f"Executing ssh command: {' '.join(ssh_command)}")
tries = 0
while tries < 3:
try:
ssh = subprocess.run(
ssh_command,
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=True,
env={'SSH_AUTH_SOCK': ''},
)
except subprocess.CalledProcessError as e:
print('ERROR_output:', e.output)
print('ERROR_stderr:', e.stderr)
print('ERROR_stdout:', e.stdout)
time.sleep(5)
tries += 1
else:
print(ssh.stdout)
return ssh.stdout
def main(
request,
) -> str:
cmd = 'uname -a'
project = 'xxx-yyyy' #プロジェクト名
instance = 'instance-centos' # インスタンス名
zone = 'asia-northeast1-b' # zone名
username = 'dev-user' # ssh接続ユーザー名
hostname = f'{instance}.{zone}.c.{project}.internal'
private_key_file = SSH_KEY_PATH
# プライベートキーを指定パスに配置
with open(private_key_file, 'w') as f:
f.write(os.environ.get('SSH_PRIVATE_KEY', 'Specified environment variable is not set.'))
# 権限を変更
os.chmod(private_key_file, 0o400)
# ssh実行
result = run_ssh(cmd, private_key_file, username, hostname)
print('result:',result)
# プライベートキーを削除
execute(['rm', private_key_file])
return 'OK'
【実行etc..】
実行するとこんな感じ
ssh初回接続のWarningが出ているけどこれで動く。
これが行えばcloud pub/subとschedulerを使用して定期実行動かせます。
何かGCE外で定期実行させたいコマンドがある場合はご利用ください。