こんにちは!ニフクラエンジニアb:id:nifcloud-developersです!
以前、ニフクラ SDK for Goを公開した際に書いたニフクラ SDK for Go のご紹介 というブログ記事では、簡単なサンプルコードを載せていましたが、今回は実運用フェーズ向けのニフクラ SDK for Goを使ったプログラムをご紹介したいと思います。
概要
まず本記事のタイトルにあるLet's Encryptについて、軽く触れておきます。別のブログ記事になりますが、Let's EncryptのDNS-01方式をニフクラDNSで認証して無料のSSL証明書を取得し自動更新するというものがあります。 この記事では、ニフクラDNSで管理しているFQDNのLet's Encrypt証明書を取得する方法が紹介されています。
このブログ記事に記載された方法で発行した証明書をニフクラ ロードバランサーに設定したい場合は、 発行した証明書をコントロールパネルよりアップロードした上でロードバランサーに設定する という追加の 手順 が発生してしまします。
そこで今回はニフクラ SDK for Goを利用し、Go言語のプログラムから Let's Encrypt証明書の発行 と ロードバランサーの設定 までを 完全自動化 するプログラムを作成してみました。
Go言語のプログラムからLet's Encrypt証明書を発行する方法
Go言語で書かれたACMEクライアントlegoというオープンソースソフトウェアを利用します。
legoならば、DNS-01で認証する場合ニフクラ DNSを含む様々なDNSサービスが対応しているので、Let's Encryptの証明書を簡単に発行できます。
証明書を発行しロードバランサーに適用する
以下の手順で証明書の発行とロードバランサーへの適用を実施します。
Step1. DNSゾーン登録
まずはニフクラDNSを利用するため、こちらを参考に証明書を取得したいドメインをゾーン登録しておきます。
Step2. 証明書を発行するプログラムコード作成
Go言語のプログラムからlegoのニフクラDNSで認証するプロバイダーを利用し、証明書を発行します。
legoをライブラリとして利用するには、サンプルコードとニフクラDNSプロバイダーのドキュメントを参考に実装します。
Step3. 証明書をニフクラへアップロードするプログラムコード作成
ニフクラ SDK for Goを利用してStep2 で発行した証明書をニフクラにアップロードします。
ニフクラへのアップロードは以下のサンプルのようにUploadSslCertificateを呼び出すことで実行できます。
ニフクラ SDK for Go を利用して証明書をアップロードするサンプル
cfg := nifcloud.NewConfig( "YOUR_ACCESS_KEY_ID", "YOUR_SECRET_ACCESS_KEY", "jp-east-1", ) svc := computing.New(cfg) req := svc.UploadSslCertificateRequest( &computing.UploadSslCertificateInput{ Certificate: "CERTIFICATE", Key: "KEY", CertificateAuthority: "CA", }, ) resp, err := req.Send(context.TODO())
Step4. 証明書をロードバランサーに適用するプログラムコード作成
ニフクラ SDK for Goを利用して Step3 でアップロードした証明書をロードバランサーに適用します。
ロードバランサーへの適用は以下のサンプルのようにSetLoadBalancerListenerSSLCertificateを呼び出すことで実行できます。
リクエストパラメータに指定するSSLCertificateIdは、 Step3 のレスポンスのFqdnIdから取得できます。
ニフクラ SDK for Go を利用して証明書をロードバランサーへ適用するサンプル
cfg := nifcloud.NewConfig( "YOUR_ACCESS_KEY_ID", "YOUR_SECRET_ACCESS_KEY", "jp-east-1", ) svc := computing.New(cfg) req := svc.SetLoadBalancerListenerSSLCertificateRequest( &computing.SetLoadBalancerListenerSSLCertificateInput{ LoadBalancerName: "LB", LoadBalancerPort: 443, InstancePort: 80, SSLCertificateId: "FqdnID", }, ) resp, err := req.Send(context.TODO())
Step5. 作成したプログラムを実行する
プログラムを実行すると、証明書が発行されロードバランサーへの適用まで実施されます。
あとはこのプログラムをsystemdのtimerなどで2か月毎に実行させることで、
Let's Encryptの証明書の有効期限を迎える前に、ロードバランサーの証明書更新が自動で実施されます。
まとめ
今回はニフクラSDK for Goの実用的な使い方として、 証明書のアップロード と ロードバランサーへの適用 のサンプルコードを紹介しました。
ニフクラSDK for Goはロードバランサーだけでなくマルチロードバランサーにも対応しているので、 マルチロードバランサーに証明書を適用したい場合も同じように自動化できます。
是非ニフクラSDK for Goを利用して様々なニフクラの操作を自動化してみてください。
最後に今回作成したプログラムコードの全体を載せておきます。
Let's Encryptの証明書発行とロードバランサーへの設定を自動化するプログラム
package main import ( "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "log" "time" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/providers/dns/nifcloud" "github.com/go-acme/lego/v4/registration" sdk "github.com/nifcloud/nifcloud-sdk-go/nifcloud" "github.com/nifcloud/nifcloud-sdk-go/service/computing" kingpin "gopkg.in/alecthomas/kingpin.v2" ) var ( email = kingpin.Flag("email", "Email address for let's encrypt account").Envar("NEOSKIVE_EMAIL").String() domain = kingpin.Flag("domain", "Domain of the certificate").Envar("NEOSKIVE_DOMAIN").String() lbName = kingpin.Flag("lb-name", "Name of the LB that sets the certificate").Envar("NEOSKIVE_LB_NAME").String() lbPort = kingpin.Flag("lb-port", "LB Port of the LB that sets the certificate").Envar("NEOSKIVE_LB_PORT").Int64() instancePort = kingpin.Flag("instance-port", "Instance Port of the LB that sets the certificate").Envar("NEOSKIVE_INSTANCE_PORT").Int64() region = kingpin.Flag("region", "Region of the LB that sets the certificate").Envar("NEOSKIVE_REGION").String() computingAccessKey = kingpin.Flag("computing-access-key", "NIFCLOUD API AccessKey for computing").Envar("NEOSKIVE_COMPUTING_NIFCLOUD_ACCESS_KEY_ID").String() computingSecretKey = kingpin.Flag("computing-secret-key", "NIFCLOUD API SecretKey for computing").Envar("NEOSKIVE_COMPUTING_NIFCLOUD_SECRET_ACCESS_KEY").String() dnsAccessKey = kingpin.Flag("dns-access-key", "NIFCLOUD API AccessKey for dns").Envar("NEOSKIVE_DNS_NIFCLOUD_ACCESS_KEY_ID").String() dnsSecretKey = kingpin.Flag("dns-secret-key", "NIFCLOUD API SecretKey for dns").Envar("NEOSKIVE_DNS_NIFCLOUD_SECRET_ACCESS_KEY").String() svc *computing.Client ) type User struct { Email string Registration *registration.Resource key crypto.PrivateKey } func (u *User) GetEmail() string { return u.Email } func (u User) GetRegistration() *registration.Resource { return u.Registration } func (u *User) GetPrivateKey() crypto.PrivateKey { return u.key } func main() { kingpin.Parse() svc = computing.New( sdk.NewConfig( sdk.StringValue(computingAccessKey), sdk.StringValue(computingSecretKey), sdk.StringValue(region), ), ) ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second) defer cancelFn() certificates, err := requestCertificate() if err != nil { log.Fatalf("[ERROR] request certificate failed: %s", err) } fqdnID, err := uploadCertificate(ctx, certificates) if err != nil { log.Fatalf("[ERROR] upload certificate failed: %s", err) } err = setCertificate(ctx, fqdnID) if err != nil { log.Fatalf("[ERROR] set certificate failed: %s", err) } } func requestCertificate() (*certificate.Resource, error) { privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, err } user := User{ Email: sdk.StringValue(email), key: privateKey, } config := lego.NewConfig(&user) config.Certificate.KeyType = certcrypto.RSA2048 client, err := lego.NewClient(config) if err != nil { return nil, err } providerConfig := nifcloud.NewDefaultConfig() providerConfig.AccessKey = sdk.StringValue(dnsAccessKey) providerConfig.SecretKey = sdk.StringValue(dnsSecretKey) provider, err := nifcloud.NewDNSProviderConfig(providerConfig) if err != nil { return nil, err } err = client.Challenge.SetDNS01Provider(provider) if err != nil { return nil, err } reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) if err != nil { return nil, err } user.Registration = reg request := certificate.ObtainRequest{ Domains: []string{sdk.StringValue(domain)}, Bundle: true, } certificates, err := client.Certificate.Obtain(request) if err != nil { return nil, err } return certificates, nil } func uploadCertificate(ctx context.Context, certificates *certificate.Resource) (*string, error) { resp, err := svc.UploadSslCertificateRequest( &computing.UploadSslCertificateInput{ Certificate: sdk.String(string(certificates.Certificate)), Key: sdk.String(string(certificates.PrivateKey)), CertificateAuthority: sdk.String(string(certificates.IssuerCertificate)), }, ).Send(ctx) if err != nil { return nil, err } return resp.FqdnId, nil } func setCertificate(ctx context.Context, fqdnID *string) error { _, err := svc.SetLoadBalancerListenerSSLCertificateRequest( &computing.SetLoadBalancerListenerSSLCertificateInput{ LoadBalancerName: lbName, LoadBalancerPort: lbPort, InstancePort: instancePort, SSLCertificateId: fqdnID, }, ).Send(ctx) return err }