Perforce Helix CoreとAWSサービス

## きっかけ

Amazon Web Services ブログで Perforce Helix Core を AWS 上に構築するという3パートになる記事が掲載されました。

* [Perforce Helix Core を AWS 上に構築する(Part1)](https://aws.amazon.com/jp/blogs/news/perforce-helix-core-on-aws-part1/)
* [Perforce Helix Core を AWS 上に構築する(Part2)](https://aws.amazon.com/jp/blogs/news/perforce-helix-core-on-aws-part2/)
* [Perforce Helix Core を AWS 上に構築する(Part3)](https://aws.amazon.com/jp/blogs/news/perforce-helix-core-on-aws-part3/)

執筆者である保里さんがこの記事を執筆中、KLabでもHelix Coreを利用すべく検証を行っていました。
今回は、KLab側で整備したHelix Coreの導入方や運用について記載します。

## Helix Coreとは

gitを始め、バージョン管理システムは多数ありますが、Helix CoreはPerforce社から提供されている有償(プロプライエタリソフトウェア)のバージョン管理システムです。
特に画像や音声などバイナリデータの管理が強いという特徴があり、ゲーム業界で多くの会社が採用しています。
元は提供している会社名と同じPerforceという製品名でしたが、ブランド名変更が行われバージョン管理システムはHelix Coreという名称に変わりました。

( 以後、Helix Coreのプロダクトは Helixと表記します。

## Helixの初期設定

保里さんの連載では、[Perforce Helix Core Studio Pack for AWS](https://www.perforce.com/vcs/aws-studio-pack) を利用していました。
Helixの初期設定内容が反映されているのですぐにバージョン管理システムとして利用できる便利なパッケージです。
しかし、KLabの検証では、`Perforce Helix Core Studio Pack for AWS`はまだなかったため、別途導入と初期設定について検証をおこないました。

### 初期設定

輸入代理店である東陽テクニカが翻訳した[Helixの初期設定手順](https://www.toyo.co.jp/files/user/img/product/ss/help/perforce/r19.1/manuals/p4sag/Content/P4SAG/install.linux.packages.configure.html)があるので、こちらを利用します。
Helixの導入作業ではパッケージ導入後 `configure-helix-p4d.sh` を実行します。
この初期化スクリプトでは、Helixのホスト名指定の他に、管理者ユーザ(super)のパスワードなどを設定します。
Helixの設定参照や変更の際は、この管理者ユーザの認証が必要になります。
AWSでは、管理者パスワードなど秘密情報を安全に保存するためのサービスである[Secrets Manager](https://aws.amazon.com/jp/secrets-manager/)があるので、これを利用してパスワードを管理しましょう。
次のような方法で利用するようにしました。

* Secrets Managerのシークレットを定義しておく。
  * 以下、CFnテンプレートの抜粋です。Secretの参照IAMロールと、Helixを導入するEC2インスタンスへのIAMロール割り当てが別途必要です。

```yaml
Resources:
  perforceSuperSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: perforce/admin
      GenerateSecretString:
        SecretStringTemplate: '{ "username": "super" }'
        GenerateStringKey: "password"
        PasswordLength: 22
        IncludeSpace: False
        ExcludePunctuation: True
```

* Helixの初期化スクリプトで定義したシークレットを参照し、管理者ユーザのパスワードとして設定する。
  * aws cliから参照している箇所の抜粋です。参照方法は[パラメータストアのSecrets Manager シークレットの参照](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/integration-ps-secretsmanager.html)を利用しています。

```bash
/opt/perforce/sbin/configure-helix-p4d.sh master -p helix.klab.example -r "/opt/perforce/servers/master" -u super -P $(aws ssm get-parameter --name /aws/reference/secretsmanager/perforce/admin --with-decryption --query "Parameter.Value" --output text | jq ".password" -r) --unicode -n
```

### 管理者パスワード参照

東陽テクニカのHelix導入手順通りの作業を行うと、認証はチケット方式になります。
そのため、認証した後はチケット有効期限内ならパスワードを再入力する必要がありません。
チケットの有効期限は `p4 login -s`で確認可能です。

```log
# p4 login -s
Perforce password (P4PASSWD) invalid or unset.
# p4 -u super login
Enter password:
User super logged in.
# p4 login -s
User super ticket expires in 7 hours 55 minutes.
```

設定作業の簡素化のため、ラッパースクリプトを準備しておきます。
チケットの有効期限が切れている場合にSecrets Managerのシークレットを参照し再度認証を行う前処理を記載します。

```bash
if [[ -z "$(p4 login -s | grep super)" ]]; then
  SUPER_PASSWORD=$(aws ssm get-parameter --name /aws/reference/secretsmanager/perforce/admin --with-decryption --query "Parameter.Value" --output text | jq ".password" -r)
  echo $SUPER_PASSWORD | p4 login
fi
```

p4コマンドでHelixのほぼすべての設定を行うことが可能です。
シークレットの参照権限のIAMロールをHelixが動作しているEC2インスタンスに限定し、すべての設定作業をEC2インスタンス上で行うようにしています。
これにより、管理者ユーザのパスワードを作業者が参照する必要がなくなります。

## Helixのユーザ管理

Helixは無料枠で最大5ユーザ、20ワークスペースが利用できます。
しかし、企業規模が大きくなると利用者も増え、Helixユーザの管理が大変になります。
また、参照可能な人や更新を許可する人、ディポ毎に権限を変えるなどユーザ毎の権限管理も必要になります。
管理者向けGUI( `p4admin` )も存在するのですが、利用者が増えるとGUIだけは管理が難しいと思います。

Helixには内部ユーザ管理データベースを外部のユーザ管理システムと連携させる機能が存在します。
ここでは、LDAPとの連携について説明します。
[AWS Directory Service](https://aws.amazon.com/jp/directoryservice/)を利用すれば、ユーザ管理情報をAWS環境外に出す必要もなく完全にHelix向けに閉じた環境を作ることが可能です。

LDAPと連携した場合のユーザ/グループ周りの設計ですが、次のステップを踏んでいけば良いかと思います。

1. Helix上の権限をグループ別に設計する
2. LDAPグループとHelixのグループのマッピングを設計する
3. LDAPグループに割り当てるLDAPユーザを設計する

最初に権限について設計します。
今回はHelixのグループ単位に権限を付与します。
グループは更新可能である staff と参照だけ可能である viewer に分けました。
また、グループに所属していないユーザには何も権限を付与しないようにしています。
LDAP側の設計と合わせて整理すると、以下のようになります。

| Helixグループ名 | Helix権限 | AD上グループのDN |
|:----|:---|:---|
| staff | write | CN=staff,OU=Helix,OU=Groups,OU=test,DC=perforce,DC=klab,DC=example |
| viewer | read | CN=viewer,OU=Helix,OU=Groups,OU=test,DC=perforce,DC=klab,DC=example |
| (所属なし) | 権限なし | |

上記設計をHelixに反映します。
権限の設計内容を`p4 protect` で処理できるフォーマットにしてファイルに準備します。

```txt
# cat helix-protect.txt
Protections:
        list user * * -//...
        write group staff * //...
        read group viewer * //...
(後略)
```

その後、設定内容のファイルを `p4 protect` で反映します。

```log
# p4 protect -i < helix-protect.txt
Protections saved.
```

次に、HelixのユーザとLDAPのユーザの関連付けを行います。
ここでは、ユーザの属性値 `memberOf`を条件に、LDAPフィルターで利用者を絞り込み `SearchFilter` に設定します。
この絞り込みを行わないと、LDAP上にいるすべてのユーザがHelixを利用可能になります。
ライセンス管理が大変な事になってしまうので注意が必要です。

* AWS Managed Microsoft Active Directoryなど、Active Directory(AD)の場合、グループのネスト構造が定義できます。
  * ここでは、helixグループを定義し、staffグループとviewerグループを子グループに定義ました。
  * `1.2.840.113556.1.4.1941` は拡張されたOID `LDAP_MATCHING_RULE_IN_CHAIN` のマッチングルールです。ネストされた子グループのメンバーがマッチするようになります。
* ADへの接続 `BindMethod` は `search` としました。そのため、Search bindのためのユーザを作成し、パスワードを設定する必要があります。パスワードはSecrets ManagerのシークレットやSSM Parameter storeのSecretsで共有するのが安全です。
* [p4 ldapのドキュメント](https://www.toyo.co.jp/files/user/img/product/ss/help/perforce/r19.1/manuals/cmdref/Content/CmdRef/p4_ldap.html)に設定内容の解説があります。参考にしてください。

```txt
# cat ldap-search.txt
Name:               search
Host:               ldap.perforce.klab.example
Port:               389
Encryption:         none
BindMethod:         search
Options:            nodowncase nogetattrs norealminusername
SearchBaseDN:       OU=Users,OU=test,DC=perforce,DC=klab,DC=example
SearchFilter:       (&(objectClass=user)(sAMAccountName=%user%)(memberOf:1.2.840.113556.1.4.1941:=CN=helix,OU=Helix,OU=Groups,OU=test,DC=perforce,DC=klab,DC=example))
SearchScope:        subtree
SearchBindDN:       helix-search@perforce.klab.example
SearchPasswd:       #####################
GroupSearchScope:   subtree
AttributeUid:       sAMAccountName
AttributeName:      displayName
AttributeEmail:     sAMAccountName
```

これを、`p4 ldap` で反映します。

```log
# p4 ldap -i < ldap-search.txt
LDAP configuration search saved.
```

さらに、定義したldap設定を有効化します。

```log
# p4 configure set auth.ldap.order.1=search
For server 'any', configuration variable 'auth.ldap.order.1' set to 'search'
# p4 configure set auth.default.method=ldap
For server 'any', configuration variable 'auth.default.method' set to 'ldap'
```

最後に、HelixのグループとLDAPのグループの権限を関連付けます。
LDAPフィルターで絞りこみ条件を `LdapSearchQuery` に記載し、関連付けをを行います。
そのユーザがグループに所属している場合は `memberOf` に対応するグループのDNが付与されます。

```txt
# cat group-staff.txt
Group:              staff
MaxResults:         unset
MaxScanRows:        unset
MaxLockTime:        unset
MaxOpenFiles:       unset
Timeout:            43200
PasswordTimeout:    unset
LdapConfig:         search
LdapSearchQuery:    (&(objectClass=user)(memberOf=CN=staff,OU=Helix,OU=Groups,OU=test,DC=perforce,DC=klab,DC=example))
LdapUserAttribute:  sAMAccountName
Owners:             super
# cat group-viewer.txt
Group:              viewer
MaxResults:         unset
MaxScanRows:        unset
MaxLockTime:        unset
MaxOpenFiles:       unset
Timeout:            43200
PasswordTimeout:    unset
LdapConfig:         search
LdapSearchQuery:    (&(objectClass=user)(memberOf=CN=viewer,OU=Helix,OU=Groups,OU=test,DC=perforce,DC=klab,DC=example))
LdapUserAttribute:  sAMAccountName
Owners:             super
```

これを`p4 group`で反映します。

```log
# p4 group -i < staff-group.txt
Group staff created.
# p4 group -i < viewer-group.txt
Group viewer created.
```

さらに、LDAPとHelixのユーザ/グループ情報の同期処理を自動化させましょう。
次のように、`p4 configure`の処理を足すと、LDAPでユーザ認証が成功した場合に自動的にhelixにユーザが作成されます。

```log
# p4 configure set auth.ldap.userautocreate=1
For server 'any', configuration variable 'auth.ldap.userautocreate' set to '1'
```

以下の`p4 configure`の処理を足すと、15分に一回、LDAPのユーザ/グループとHelixのユーザ/グループを同期するコマンド `p4 ldapsync` が実行されるようになります。

```log
# p4 configure set "startup.1=ldapsync -g -i 900"
For server 'any', configuration variable 'startup.1' set to 'ldapsync -g -i 900'
# p4 configure set "startup.2=ldapsync -u -c -i 900"
For server 'any', configuration variable 'startup.2' set to 'ldapsync -u -c -i 900'
```

## Ansibleの利用

今回説明した設定は、実際には[Ansible](https://docs.ansible.com/)を利用しています。
Ansibleのlookupプラグインには SSM Parameter storeを参照する[aws_ssm](https://docs.ansible.com/ansible/latest/collections/amazon/aws/aws_ssm_lookup.html)やSecrets Managerのシークレットを参照する[aws_secret](https://docs.ansible.com/ansible/latest/collections/amazon/aws/aws_secret_lookup.html)があり、AWSサービスとの連携で重用しました。

Helixの各種設定は `p4` コマンドを利用するのですが、このコマンドの `-o`, `-i` コマンドが構築自動化向きになっています。

* `p4 [サブコマンド] -o` で現行設定をサブコマンド毎のフォームで標準出力に出力します。
* `p4 [サブコマンド] -i` で標準入力からサブコマンドのフォームに従った設定内容を読み込み、差分がある場合反映します。

設定したい内容がある場合、以下のように準備しました。

1. `p4 [サブコマンド] -o` でフォームを出力し、変更箇所を変数化するなど修正してjinja2のテンプレートを定義
2. SSM Parameter storeや Secrets Managerから設定する内容を変数として定義
3. templateモジュールの処理をplaybookに記載し、設定ファイルを出力
4. `p4 [サブコマンド] -i` を実行し、設定を反映する。

前述した `p4 ldap` 向け設定のtemplateファイル、playbookの該当箇所を例示します。

```txt
# ldap-search.j2
Name:               search
Host:               {{ ad.ldapServer }}
Port:               389
Encryption:         none
BindMethod:         search
Options:            nodowncase nogetattrs norealminusername
SearchBaseDN:       {{ ad.searchBaseDn }}
SearchFilter:       (&(objectClass=user)(sAMAccountName=%user%)({{ ad.accessgroup }}))
SearchScope:        subtree
SearchBindDN:       {{ ad.binduser }}
SearchPasswd:       {{ ad.bindpasswd }}
GroupSearchScope:   subtree
AttributeUid:       sAMAccountName
AttributeName:      displayName
AttributeEmail:     sAMAccountName
```

```yaml
# playbook
- name: "Create Helix LDAP configration file"
  template:
    src: ldap-search.j2
    dest: "/etc/perforce/ldap-search"
    owner: root
    group: root
    mode: 0600
  vars:
    ad:
      ldapServer: ldap.perforce.klab.example
      searchBaseDn: OU=Users,OU=test,DC=perforce,DC=klab,DC=example
      accessgroup: "memberOf:1.2.840.113556.1.4.1941:=CN=helix,OU=Helix,OU=Groups,OU=test,DC=perforce,DC=klab,DC=example"
      binduser: helix-search@perforce.klab.example
      bindpasswd: "{{ lookup('aws_ssm', '/ad/helix-search', decrypt=True ) }}"
```

## まとめ

* `Perforce Helix Core Studio Pack for AWS` を利用せずにHelix Coreを利用する場合、設定しておく内容が多い。
* 設定には Helixの管理者ユーザのパスワードが必要。Secrets Managerに保存しておき、認証が必要なタイミングで参照できるようにすれば運用が楽になる。
* AWS Directory Serviceを利用して、Managed Active DirectoryをHelixのユーザ/グループ管理の元情報として利用すれば、ユーザ情報をAWS環境内に閉じられるのでセキュアな運用ができる。
* AnsibleのlookupプラグインにSSM Parameter StoreのパラメータやSecrets Managerのシークレットを参照するものがありtemplateモジュールを組み合わせるとHelixの設定をほぼ自動化できる。