Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ConfigMap vs Secret #k8sjp

089fe44e41bb1fa2d9421f919a99173c?s=47 Shimpei Otsubo
November 08, 2018
800

ConfigMap vs Secret #k8sjp

089fe44e41bb1fa2d9421f919a99173c?s=128

Shimpei Otsubo

November 08, 2018
Tweet

Transcript

  1. ©2018 Wantedly, Inc. ConfigMap vs Secret ίʔυ͔ΒಡΈղ͘ ConfigMap ͱ4FDSFUͷҧ͍ Kubernetes

    Meetup Tokyo #14 8.Nov.2018 - Shimpei Otsubo - @potsbo
  2. ConfigMapͱSecret͸Կ͕ҧ͏ʁ ͲͬͪΛ࢖͏͔͸͙͢Θ͔Δ ڍಈ͸͘͢͝ࣅͯΔ άάͬͯ΋Α͘Θ͔Βͳ͍ ࢖͍෼͚͸ॻ͍͍ͯͯ΋໌֬ͳҧ͍͸ॻ͍ͯͳ͍ Ұൠͷ։ൃऀʹݟΒΕͯΑ͍͔ʁ ContainerʹԿ͔Λinject͢Δ ©2018 Wantedly, Inc.

  3. ຊ࣭తͳҧ͍͸ίʔυΛಡ·ͳ͍ͱΘ͔Βͳ͍ ©2018 Wantedly, Inc.

  4. /* Copyright 2015 The Kubernetes Authors. Licensed under the Apache

    License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package configmap import ( "fmt" "github.com/golang/glog" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/util/strings" "k8s.io/kubernetes/pkg/volume" volumeutil "k8s.io/kubernetes/pkg/volume/util" ) // ProbeVolumePlugins is the entry point for plugin detection in a package. func ProbeVolumePlugins() []volume.VolumePlugin { return []volume.VolumePlugin{&configMapPlugin{}} } const ( configMapPluginName = "kubernetes.io/configmap" ) // configMapPlugin implements the VolumePlugin interface. type configMapPlugin struct { host volume.VolumeHost /* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package secret import ( "fmt" "github.com/golang/glog" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/util/strings" "k8s.io/kubernetes/pkg/volume" volumeutil "k8s.io/kubernetes/pkg/volume/util" ) // ProbeVolumePlugins is the entry point for plugin detection in a package. func ProbeVolumePlugins() []volume.VolumePlugin { return []volume.VolumePlugin{&secretPlugin{}} } const ( secretPluginName = "kubernetes.io/secret" ) // secretPlugin implements the VolumePlugin interface. type secretPlugin struct { host volume.VolumeHost ©2018 Wantedly, Inc.
  5. fileProjection.Mode = *defaultMode } payload[ktp.Path] = fileProjection } } return

    payload, nil } func totalSecretBytes(secret *v1.Secret) int { totalSize := 0 for _, value := range secret.Data { totalSize += len(value) } return totalSize } // secretVolumeUnmounter handles cleaning up secret volumes. type secretVolumeUnmounter struct { *secretVolume } var _ volume.Unmounter = &secretVolumeUnmounter{} func (c *secretVolumeUnmounter) TearDown() error { return c.TearDownAt(c.GetPath()) } func (c *secretVolumeUnmounter) TearDownAt(dir string) error { return volumeutil.UnmountViaEmptyDir(dir, c.plugin.host, c.volName, wrappedVolumeSpec(), c.podUID) } func getVolumeSource(spec *volume.Spec) (*v1.SecretVolumeSource, bool) { var readOnly bool var volumeSource *v1.SecretVolumeSource if spec.Volume != nil && spec.Volume.Secret != nil { volumeSource = spec.Volume.Secret readOnly = spec.ReadOnly } return volumeSource, readOnly } } return payload, nil } func totalBytes(configMap *v1.ConfigMap) int { totalSize := 0 for _, value := range configMap.Data { totalSize += len(value) } for _, value := range configMap.BinaryData { totalSize += len(value) } return totalSize } // configMapVolumeUnmounter handles cleaning up configMap volumes. type configMapVolumeUnmounter struct { *configMapVolume } var _ volume.Unmounter = &configMapVolumeUnmounter{} func (c *configMapVolumeUnmounter) TearDown() error { return c.TearDownAt(c.GetPath()) } func (c *configMapVolumeUnmounter) TearDownAt(dir string) error { return volumeutil.UnmountViaEmptyDir(dir, c.plugin.host, c.volName, wrappedVolumeSpec(), c.podUID) } func getVolumeSource(spec *volume.Spec) (*v1.ConfigMapVolumeSource, bool) { var readOnly bool var volumeSource *v1.ConfigMapVolumeSource if spec.Volume != nil && spec.Volume.ConfigMap != nil { volumeSource = spec.Volume.ConfigMap readOnly = spec.ReadOnly } return volumeSource, readOnly } ©2018 Wantedly, Inc.
  6. fileProjection.Mode = *defaultMode } payload[ktp.Path] = fileProjection } } return

    payload, nil } func totalSecretBytes(secret *v1.Secret) int { totalSize := 0 for _, value := range secret.Data { totalSize += len(value) } return totalSize } // secretVolumeUnmounter handles cleaning up secret volumes. type secretVolumeUnmounter struct { *secretVolume } var _ volume.Unmounter = &secretVolumeUnmounter{} func (c *secretVolumeUnmounter) TearDown() error { return c.TearDownAt(c.GetPath()) } func (c *secretVolumeUnmounter) TearDownAt(dir string) error { return volumeutil.UnmountViaEmptyDir(dir, c.plugin.host, c.volName, wrappedVolumeSpec(), c.podUID) } func getVolumeSource(spec *volume.Spec) (*v1.SecretVolumeSource, bool) { var readOnly bool var volumeSource *v1.SecretVolumeSource if spec.Volume != nil && spec.Volume.Secret != nil { volumeSource = spec.Volume.Secret readOnly = spec.ReadOnly } return volumeSource, readOnly } } return payload, nil } func totalBytes(configMap *v1.ConfigMap) int { totalSize := 0 for _, value := range configMap.Data { totalSize += len(value) } for _, value := range configMap.BinaryData { totalSize += len(value) } return totalSize } // configMapVolumeUnmounter handles cleaning up configMap volumes. type configMapVolumeUnmounter struct { *configMapVolume } var _ volume.Unmounter = &configMapVolumeUnmounter{} func (c *configMapVolumeUnmounter) TearDown() error { return c.TearDownAt(c.GetPath()) } func (c *configMapVolumeUnmounter) TearDownAt(dir string) error { return volumeutil.UnmountViaEmptyDir(dir, c.plugin.host, c.volName, wrappedVolumeSpec(), c.podUID) } func getVolumeSource(spec *volume.Spec) (*v1.ConfigMapVolumeSource, bool) { var readOnly bool var volumeSource *v1.ConfigMapVolumeSource if spec.Volume != nil && spec.Volume.ConfigMap != nil { volumeSource = spec.Volume.ConfigMap readOnly = spec.ReadOnly } return volumeSource, readOnly } ©2018 Wantedly, Inc. ίϐϖͬΆ͍
  7. 52c52 < Volume: &v1.Volume{VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}}, --- > Volume: &v1.Volume{VolumeSource:

    v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{Medium: v1.StorageMediumMemory}}}, 55a56,59 > func getPath(uid types.UID, volumeName string, host volume.VolumeHost) string { > return host.GetPodVolumeDir(uid, strings.EscapeQualifiedNameForDisk(configmapPluginName), volumeName) > } > 72,75c76 < return fmt.Sprintf( < "%v/%v", < spec.Name(), < volumeSource.Name), nil --- > return volumeSource.Name, nil 101c102 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(pod.UID, spec.Name(), plugin.host))), 117c118 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(podUID, volumeName, plugin.host))), 126c127,129 < configmap: &v1.configmapVolumeSource{}, --- > configmap: &v1.configmapVolumeSource{ > Name: volumeName, > }, 137c140 < volume.MetricsNil --- > volume.MetricsProvider 143c146 < return sv.plugin.host.GetPodVolumeDir(sv.podUID, strings.EscapeQualifiedNameForDisk(configmapPluginName), sv.volumeName) --- > return getPath(sv.podUID, sv.volumeName, sv.plugin.host) 206c209 < len(configmap.Data)+len(configmap.BinaryData), --- > len(configmap.Data), 265c268 < payload := make(map[string]volumeutil.FileProjection, (len(configmap.Data) + len(configmap.BinaryData))) --- > payload := make(map[string]volumeutil.FileProjection, len(configmap.Data)) 274,278d276 < for name, data := range configmap.BinaryData { < fileProjection.Data = data < fileProjection.Mode = *defaultMode < payload[name] = fileProjection < } 281,285c279,280 < if stringData, ok := configmap.Data[ktp.Key]; ok { < fileProjection.Data = []byte(stringData) < } else if binaryData, ok := configmap.BinaryData[ktp.Key]; ok { < fileProjection.Data = binaryData < } else { --- > content, ok := configmap.Data[ktp.Key] > if !ok { 289c284,286 < return nil, fmt.Errorf("configmap references non-existent config key: %s", ktp.Key) --- > errMsg := "references non-existent configmap key" > glog.Errorf(errMsg) > return nil, fmt.Errorf(errMsg) 291a289 > fileProjection.Data = []byte(content) 300d297 < 309,311d305 < for _, value := range configmap.BinaryData { < totalSize += len(value) < } ˞EJGG͕গͳ͘ͳΔΑ͏ʹগ͍ͬͯ͠͡·͢
  8. 52c52 < Volume: &v1.Volume{VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}}, --- > Volume: &v1.Volume{VolumeSource:

    v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{Medium: v1.StorageMediumMemory}}}, 55a56,59 > func getPath(uid types.UID, volumeName string, host volume.VolumeHost) string { > return host.GetPodVolumeDir(uid, strings.EscapeQualifiedNameForDisk(configmapPluginName), volumeName) > } > 72,75c76 < return fmt.Sprintf( < "%v/%v", < spec.Name(), < volumeSource.Name), nil --- > return volumeSource.Name, nil 101c102 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(pod.UID, spec.Name(), plugin.host))), 117c118 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(podUID, volumeName, plugin.host))), 126c127,129 < configmap: &v1.configmapVolumeSource{}, --- > configmap: &v1.configmapVolumeSource{ > Name: volumeName, > }, 137c140 < volume.MetricsNil --- > volume.MetricsProvider 143c146 < return sv.plugin.host.GetPodVolumeDir(sv.podUID, strings.EscapeQualifiedNameForDisk(configmapPluginName), sv.volumeName)
  9. 52c52 < Volume: &v1.Volume{VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}}, --- > Volume: &v1.Volume{VolumeSource:

    v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{Medium: v1.StorageMediumMemory}}}, 55a56,59 > func getPath(uid types.UID, volumeName string, host volume.VolumeHost) string { > return host.GetPodVolumeDir(uid, strings.EscapeQualifiedNameForDisk(configmapPluginName), volumeName) > } > 72,75c76 < return fmt.Sprintf( < "%v/%v", < spec.Name(), < volumeSource.Name), nil --- > return volumeSource.Name, nil 101c102 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(pod.UID, spec.Name(), plugin.host))), 117c118 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(podUID, volumeName, plugin.host))), 126c127,129 < configmap: &v1.configmapVolumeSource{}, --- > configmap: &v1.configmapVolumeSource{ > Name: volumeName, > }, 137c140 < volume.MetricsNil --- > volume.MetricsProvider 143c146 < return sv.plugin.host.GetPodVolumeDir(sv.podUID, strings.EscapeQualifiedNameForDisk(configmapPluginName), sv.volumeName) 4FDSFU͸ϝϞϦ಺
  10. 52c52 < Volume: &v1.Volume{VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}}, --- > Volume: &v1.Volume{VolumeSource:

    v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{Medium: v1.StorageMediumMemory}}}, 55a56,59 > func getPath(uid types.UID, volumeName string, host volume.VolumeHost) string { > return host.GetPodVolumeDir(uid, strings.EscapeQualifiedNameForDisk(configmapPluginName), volumeName) > } > 72,75c76 < return fmt.Sprintf( < "%v/%v", < spec.Name(), < volumeSource.Name), nil --- > return volumeSource.Name, nil 101c102 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(pod.UID, spec.Name(), plugin.host))), 117c118 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(podUID, volumeName, plugin.host))), 126c127,129 < configmap: &v1.configmapVolumeSource{}, --- > configmap: &v1.configmapVolumeSource{ > Name: volumeName, > }, 137c140 < volume.MetricsNil --- > volume.MetricsProvider 143c146 < return sv.plugin.host.GetPodVolumeDir(sv.podUID, strings.EscapeQualifiedNameForDisk(configmapPluginName), sv.volumeName) ಉҰͷ$POGJH.BQෳ਺ճBUUBDIग़དྷΔΑ͏ʹ IUUQTHJUIVCDPNLVCFSOFUFTLVCFSOFUFTQVMM ˞4FDSFUͰ͸मਖ਼͞Ε͍ͯͳ͍͕͜ͷ໰୊͸ى͖ͳ͔ͬͨ
  11. 52c52 < Volume: &v1.Volume{VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}}, --- > Volume: &v1.Volume{VolumeSource:

    v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{Medium: v1.StorageMediumMemory}}}, 55a56,59 > func getPath(uid types.UID, volumeName string, host volume.VolumeHost) string { > return host.GetPodVolumeDir(uid, strings.EscapeQualifiedNameForDisk(configmapPluginName), volumeName) > } > 72,75c76 < return fmt.Sprintf( < "%v/%v", < spec.Name(), < volumeSource.Name), nil --- > return volumeSource.Name, nil 101c102 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(pod.UID, spec.Name(), plugin.host))), 117c118 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(podUID, volumeName, plugin.host))), 126c127,129 < configmap: &v1.configmapVolumeSource{}, --- > configmap: &v1.configmapVolumeSource{ > Name: volumeName, > }, 137c140 < volume.MetricsNil --- > volume.MetricsProvider 143c146 < return sv.plugin.host.GetPodVolumeDir(sv.podUID, strings.EscapeQualifiedNameForDisk(configmapPluginName), sv.volumeName) 4FDSFU͸.FUSJDT͕औΕΔ
  12. > return host.GetPodVolumeDir(uid, strings.EscapeQualifiedNameForDisk(configmapPluginName), volumeName) > } > 72,75c76 <

    return fmt.Sprintf( < "%v/%v", < spec.Name(), < volumeSource.Name), nil --- > return volumeSource.Name, nil 101c102 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(pod.UID, spec.Name(), plugin.host))), 117c118 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(podUID, volumeName, plugin.host))), 126c127,129 < configmap: &v1.configmapVolumeSource{}, --- > configmap: &v1.configmapVolumeSource{ > Name: volumeName, > }, 137c140 < volume.MetricsNil --- > volume.MetricsProvider 143c146 < return sv.plugin.host.GetPodVolumeDir(sv.podUID, strings.EscapeQualifiedNameForDisk(configmapPluginName), sv.volumeName) --- > return getPath(sv.podUID, sv.volumeName, sv.plugin.host) 206c209 < len(configmap.Data)+len(configmap.BinaryData), --- > len(configmap.Data), 265c268 < payload := make(map[string]volumeutil.FileProjection, (len(configmap.Data) + len(configmap.BinaryData))) ʁ
  13. 117c118 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(podUID, volumeName, plugin.host))), 126c127,129 <

    configmap: &v1.configmapVolumeSource{}, --- > configmap: &v1.configmapVolumeSource{ > Name: volumeName, > }, 137c140 < volume.MetricsNil --- > volume.MetricsProvider 143c146 < return sv.plugin.host.GetPodVolumeDir(sv.podUID, strings.EscapeQualifiedNameForDisk(configmapPluginName), sv.volumeName) --- > return getPath(sv.podUID, sv.volumeName, sv.plugin.host) 206c209 < len(configmap.Data)+len(configmap.BinaryData), --- > len(configmap.Data), 265c268 < payload := make(map[string]volumeutil.FileProjection, (len(configmap.Data) + len(configmap.BinaryData))) --- > payload := make(map[string]volumeutil.FileProjection, len(configmap.Data)) 274,278d276 < for name, data := range configmap.BinaryData { < fileProjection.Data = data < fileProjection.Mode = *defaultMode < payload[name] = fileProjection < } 281,285c279,280 < if stringData, ok := configmap.Data[ktp.Key]; ok { < fileProjection.Data = []byte(stringData) < } else if binaryData, ok := configmap.BinaryData[ktp.Key]; ok { < fileProjection.Data = binaryData < } else { --- 4FDSFU͸.FUSJDT͕औΕΔ
  14. 117c118 < volume.MetricsNil{}, --- > volume.NewCachedMetrics(volume.NewMetricsDu(getPath(podUID, volumeName, plugin.host))), 126c127,129 <

    configmap: &v1.configmapVolumeSource{}, --- > configmap: &v1.configmapVolumeSource{ > Name: volumeName, > }, 137c140 < volume.MetricsNil --- > volume.MetricsProvider 143c146 < return sv.plugin.host.GetPodVolumeDir(sv.podUID, strings.EscapeQualifiedNameForDisk(configmapPluginName), sv.volumeName) --- > return getPath(sv.podUID, sv.volumeName, sv.plugin.host) 206c209 < len(configmap.Data)+len(configmap.BinaryData), --- > len(configmap.Data), 265c268 < payload := make(map[string]volumeutil.FileProjection, (len(configmap.Data) + len(configmap.BinaryData))) --- > payload := make(map[string]volumeutil.FileProjection, len(configmap.Data)) 274,278d276 < for name, data := range configmap.BinaryData { < fileProjection.Data = data < fileProjection.Mode = *defaultMode < payload[name] = fileProjection < } 281,285c279,280 < if stringData, ok := configmap.Data[ktp.Key]; ok { < fileProjection.Data = []byte(stringData) < } else if binaryData, ok := configmap.BinaryData[ktp.Key]; ok { < fileProjection.Data = binaryData < } else { --- ݁ہҰॹʹͳΔ΍ͭ
  15. > return getPath(sv.podUID, sv.volumeName, sv.plugin.host) 206c209 < len(configmap.Data)+len(configmap.BinaryData), --- >

    len(configmap.Data), 265c268 < payload := make(map[string]volumeutil.FileProjection, (len(configmap.Data) + len(configmap.BinaryData))) --- > payload := make(map[string]volumeutil.FileProjection, len(configmap.Data)) 274,278d276 < for name, data := range configmap.BinaryData { < fileProjection.Data = data < fileProjection.Mode = *defaultMode < payload[name] = fileProjection < } 281,285c279,280 < if stringData, ok := configmap.Data[ktp.Key]; ok { < fileProjection.Data = []byte(stringData) < } else if binaryData, ok := configmap.BinaryData[ktp.Key]; ok { < fileProjection.Data = binaryData < } else { --- > content, ok := configmap.Data[ktp.Key] > if !ok { 289c284,286 < return nil, fmt.Errorf("configmap references non-existent config key: %s", ktp.Key) --- > errMsg := "references non-existent configmap key" > glog.Errorf(errMsg) > return nil, fmt.Errorf(errMsg) 291a289 > fileProjection.Data = []byte(content) 300d297 < 309,311d305 < for _, value := range configmap.BinaryData { < totalSize += len(value) < } $POGJH.BQ͸#JOBSZ%BUBʹରԠ
  16. Secret͸Metrics͕औΕΔ ©2018 Wantedly, Inc. ConfigMap͸string΋औΕΔ

  17. ޙ͸Ұॹʂʂ ©2018 Wantedly, Inc.

  18. ͡Ό͋ͳΜͰ෼͔ΕͯΔͷ͔ʁ ໊લ͕෼͔Ε͍ͯΔͱศརͳ͜ͱ͕͋Δ etcd ͰϦιʔε୯ҐͰ҉߸Խ͢Δ͔ܾΊΒΕΔʁ ©2018 Wantedly, Inc. 3#"$ͰݖݶΛ෼͚΍͍͢Α͏ʹʁ

  19. ConfigMap Λίϐϖͨͬ͠Ά͍৔ॴ ©2018 Wantedly, Inc. 143c146 < return sv.plugin.host.GetPodVolumeDir(sv.podUID, strings.EscapeQualifiedNameForDisk(configmapPluginName),

    sv.volumeName) --- > return getPath(sv.podUID, sv.volumeName, sv.plugin.host) sv ͬͯ secret volume? // configMapVolumeMounter handles retrieving secret from the API server // and placing them into the volume on the host. type configMapVolumeMounter struct { *configMapVolume source v1.ConfigMapVolumeSource pod v1.Pod opts *volume.VolumeOptions getConfigMap func(namespace, name string) (*v1.ConfigMap, error) } secret ͬͯ configmap.go ʹॻ͍ͯ͋Δ ༨ஊ1
  20. Secret͚ͩMetricsऔΕΔͷͳΜͰʁ ©2018 Wantedly, Inc. 1/15 2016 branch 2/6 2016 Secret

    ʹ Metrics ෇༩ merge 1/26 2016 branch Secret ͔Β ConfigMap ࡞੒ 2/15 2016 merge PR #19741 PR #20114 PR ͷλΠϛϯάͷ໰୊Ͱ͸ʁ ༨ஊ2
  21. ·ͱΊ $POGJH.BQͱ4FDSFU͸ΊͪΌࣅͯΔ ػೳతʹ΄ͱΜͲҧ͍͸ͳ͍ ίʔυಡΈղ͘ͱ͓΋͠Ζࣄ࣮͕ग़ͯ͘Δ ©2018 Wantedly, Inc.